Audio conversation pipeline#2182
Conversation
Signed-off-by: Ssofja <sofiakostandian@gmail.com>
Signed-off-by: Ssofja <sofiakostandian@gmail.com>
Signed-off-by: Ssofja <sofiakostandian@gmail.com>
Signed-off-by: Ssofja <sofiakostandian@gmail.com>
Signed-off-by: Ssofja <sofiakostandian@gmail.com>
Signed-off-by: Ssofja <sofiakostandian@gmail.com>
Signed-off-by: Ssofja <sofiakostandian@gmail.com>
Signed-off-by: Ssofja <sofiakostandian@gmail.com>
Signed-off-by: Ssofja <sofiakostandian@gmail.com>
Signed-off-by: Ssofja <sofiakostandian@gmail.com>
Signed-off-by: Ssofja <sofiakostandian@gmail.com>
# Conflicts: # nemo_curator/stages/audio/__init__.py
…re, tutorial Signed-off-by: Ssofja <sofiakostandian@gmail.com>
Signed-off-by: Ssofja <sofiakostandian@gmail.com>
# Conflicts: # nemo_curator/stages/audio/__init__.py # tutorials/audio/README.md
Integrate the three audio feature branches (chatterbox_tts, montreal_forced_aligner, merging_audios) and add the end-to-end data-generation tutorial that chains vLLM -> Chatterbox TTS -> MFA alignment -> conversation merge. - Add vLLMInference stage (nemo_curator/stages/audio/llm) that delegates to the existing nemo_curator.models.VLLMModel and emits per-turn AudioTasks. - Register MergeConversationSDPStage and vLLMInference in the audio package lazy exports. - Add tutorials/audio/data-generation (main.py, pipeline.yaml, prompts, topics, README) and register it in the audio tutorial index. - Resolve ruff findings surfaced by the merge (mfa_alignment.py and its tests) with behavior-preserving fixes.
Greptile SummaryThis PR wires four previously separate audio feature branches into a single end-to-end multi-speaker conversation generation pipeline: LLM conversation generation (
Confidence Score: 3/5Two defects on the hot path will cause failures for any user who follows the documented API: an AttributeError when a pre-initialized VLLMModel is passed, and missing output fields propagated to downstream stages for empty utterances. The nemo_curator/stages/audio/llm/vllm_inference.py (tokenizer/ensure-model logic) and nemo_curator/stages/audio/tts/chatterbox_tts.py (empty-text skip path) Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant MR as ManifestReader
participant LLM as vLLMInference
participant TTS as ChatterboxTTSStage
participant MFA as MFAAlignmentStage
participant Merge as MergeConversationSDPStage
participant MW as ManifestWriterStage
note over MR,MFA: Phase 1 (GPU — standard executor)
MR->>LLM: AudioTask(topic)
LLM->>LLM: VLLMModel.generate() + JSON validate
LLM-->>TTS: AudioTask×N (one per turn, shared conversation_id)
TTS->>TTS: ChatterboxTTS.generate() per turn
TTS-->>MFA: AudioTask + audio_filepath
MFA->>MFA: mfa align subprocess (batch)
MFA-->>Merge: AudioTask + rttm_filepath + ctm_filepath
note over Merge,MW: Phase 2 (CPU — grouped by conversation_id)
Merge->>Merge: group turns, _merge_audio_files()
Merge->>Merge: _compute_timeline() shared RTTM+CTM offsets
Merge->>Merge: _merge_rttm_files() + _merge_ctm_files()
Merge-->>MW: AudioTask(mixed.wav, all.rttm, all.ctm, seglst)
MW-->>MW: write merged_manifest.jsonl
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant MR as ManifestReader
participant LLM as vLLMInference
participant TTS as ChatterboxTTSStage
participant MFA as MFAAlignmentStage
participant Merge as MergeConversationSDPStage
participant MW as ManifestWriterStage
note over MR,MFA: Phase 1 (GPU — standard executor)
MR->>LLM: AudioTask(topic)
LLM->>LLM: VLLMModel.generate() + JSON validate
LLM-->>TTS: AudioTask×N (one per turn, shared conversation_id)
TTS->>TTS: ChatterboxTTS.generate() per turn
TTS-->>MFA: AudioTask + audio_filepath
MFA->>MFA: mfa align subprocess (batch)
MFA-->>Merge: AudioTask + rttm_filepath + ctm_filepath
note over Merge,MW: Phase 2 (CPU — grouped by conversation_id)
Merge->>Merge: group turns, _merge_audio_files()
Merge->>Merge: _compute_timeline() shared RTTM+CTM offsets
Merge->>Merge: _merge_rttm_files() + _merge_ctm_files()
Merge-->>MW: AudioTask(mixed.wav, all.rttm, all.ctm, seglst)
MW-->>MW: write merged_manifest.jsonl
|
| if not text or not text.strip(): | ||
| logger.warning(f"Skipping task {task.task_id}: no text") | ||
| output_tasks.append(task) | ||
| continue |
There was a problem hiding this comment.
Missing required output fields on skipped tasks
When text is empty the method appends the raw input task, which lacks the declared output keys audio_filepath, duration, and reference_voice. Any downstream stage that reads these keys (e.g. MFAAlignmentStage expecting audio_filepath) will raise a KeyError or FileNotFoundError for every such task.
| if not text or not text.strip(): | |
| logger.warning(f"Skipping task {task.task_id}: no text") | |
| output_tasks.append(task) | |
| continue | |
| if not text or not text.strip(): | |
| logger.warning(f"Skipping task {task.task_id}: no text") | |
| out_data = dict(data) | |
| out_data.setdefault("audio_filepath", "") | |
| out_data.setdefault("duration", 0.0) | |
| out_data.setdefault("reference_voice", "") | |
| output_tasks.append( | |
| AudioTask( | |
| data=out_data, | |
| task_id=task.task_id, | |
| dataset_name=task.dataset_name, | |
| ) | |
| ) | |
| continue |
| @staticmethod | ||
| def _get_audio_duration(audio_path: str) -> float: | ||
| with sf.SoundFile(audio_path) as f: | ||
| return len(f) / f.samplerate | ||
|
|
||
| @staticmethod | ||
| def _create_duration_fallback_rttm( | ||
| file_stem: str, speaker: str, duration: float, rttm_path: Path | ||
| ) -> None: | ||
| with open(rttm_path, "w", encoding="utf-8") as f: | ||
| f.write( | ||
| f"SPEAKER {file_stem} 1 0.000 {duration:.3f} " | ||
| f"<NA> <NA> {speaker} <NA> <NA>\n" |
There was a problem hiding this comment.
_is_node_local_mfa_root returns True without checking the actual path
When copy_models_to_local=True the method immediately returns True, regardless of whether _mfa_root was actually set to the local copy. If setup_on_node was never called (e.g. single-node local run) and setup() fell back to _effective_mfa_root (the shared path), this method returns True for a shared path. Consequently _run_mfa_align may delete command_history.yaml from the shared MFA root, corrupting state for any parallel worker still using it.
| tasks: list[AudioTask], | ||
| ) -> dict[str, list[AudioTask]]: | ||
| """Group tasks by ``conversation_id`` for the merge stage.""" | ||
| groups: dict[str, list[AudioTask]] = defaultdict(list) | ||
| for task in tasks: | ||
| conv_id = task.data.get("conversation_id", "unknown") | ||
| groups[conv_id].append(task) | ||
| return dict(groups) | ||
|
|
There was a problem hiding this comment.
Stage indices hard-coded; silent breakage if
pipeline.yaml adds/removes stages
merge_cfg = stage_cfgs[_PHASE1_STAGE_COUNT] and writer_cfg = stage_cfgs[_PHASE1_STAGE_COUNT + 1] work only when phase-1 has exactly 4 stages in positions 0–3 and the merge/writer stages are at fixed positions. Adding a stage to phase 1 (e.g. a normalisation step) or reordering stages in pipeline.yaml silently pulls the wrong stage config with no error until runtime. Consider identifying stage configs by _target_ class name rather than by index.
Description
This PR adds an end-to-end multi-speaker conversation data-generation pipeline for audio, wiring together four audio stages into a single runnable tutorial: LLM conversation generation → TTS synthesis → forced alignment → conversation merging.
It integrates three previously separate audio feature branches and adds the glue needed to run them as one pipeline:
vLLMInferencestage (nemo_curator/stages/audio/llm/vllm_inference.py): generates structured multi-turn conversations from topic prompts. It delegates to the existingnemo_curator.models.VLLMModel(already onmain) for engine management/inference rather than creatingvllm.LLMdirectly, and emits oneAudioTaskper conversation turn (sharedconversation_id,speaker,utterance,overlap). Includes JSON-schema validation with batch retry and per-turn expansion.ChatterboxTTSStage(from thechatterbox_ttswork): synthesizes per-turn audio with voice cloning.MFAAlignmentStage(from themontreal_forced_alignerwork): Montreal Forced Aligner producing word-level TextGrid → RTTM/CTM.MergeConversationSDPStage(from themerging_audioswork): merges per-turn audio into multi-speaker conversations (per-speaker WAVs, multichannel, mixed mono, combined RTTM/CTM/seglst).Supporting changes:
MergeConversationSDPStageandvLLMInferencein the audio package lazy exports (nemo_curator/stages/audio/__init__.py) so both are discoverable vianemo_curator.stages.audiowithout eagerly importing heavy optional deps.tutorials/audio/data-generation/tutorial:main.py(two-phase runner),pipeline.yaml(Hydra config),README.md, plus 2/3/4-speaker prompt templates and sample topic manifests. Registered the tutorial intutorials/audio/README.md.vllmoptional extra and theaudio_commonextra (which already carrieschatterbox-ttsandpraatio); no new dependency declarations were required.The pipeline runs in two phases: Phase 1 (GPU) runs
ManifestReader → vLLMInference → ChatterboxTTSStage → MFAAlignmentStagevia the standard executor (oneAudioTaskper turn); Phase 2 (CPU) groups turns byconversation_id, runsMergeConversationSDPStageper conversation, and writes the final manifest.Usage
Install the audio + vLLM extras and MFA models, then run the tutorial from the repo root:
Using the
vLLMInferencestage directly in a pipeline:Checklist
tests/stages/audio/tts,tests/stages/audio/alignment,tests/stages/audio/merging— 58 tests). ThevLLMInferencewrapper and the tutorial runner were verified via import/compile smoke checks; a full end-to-end run requires a GPU plus vLLM/Chatterbox/MFA models and was not run in CI.tutorials/audio/data-generation/README.mdand registered the tutorial intutorials/audio/README.md(tutorial selection, data availability, and system dependency tables).