vocal_helper.pipeline module
vocal_helper.pipeline
The top-level producer/consumer orchestrator.
Stages, in order :
- [Source] → [VAD] → [Diar] → [ASR] → [LLM analyst]
q0 q1 q2 q3 q4
Every arrow is an asyncio.Queue. Each stage is a long-running
coroutine that await``s on its inbox, processes the event, and pushes
the result onto its outbox. Closing the upstream queue with a ``None
sentinel cascades cleanly through the rest of the chain.
The pipeline is configured at construction time but only starts when
Pipeline.start() is called. The caller can attach subscribers
to any intermediate queue (q1 for VAD events, q3 for ASR
output, etc.) for live UI / WebSocket fan-out.
- class vocal_helper.pipeline.OfflinePipeline(*, source, config=None, strict=True)[source]
Bases:
objectEnd-to-end batch chain : source → offline diar → ASR → LLM.
Trades the live VAD + per-segment online clustering for a single call to
OfflineDiarStageon the full PCM buffer (with long-form chunking baked in). This is the best-quality path when the input is fully available — typical use cases : meeting recordings, podcasts, voicemail batches, lecture archives.Usage
>>> import asyncio, vocal_helper as voh >>> >>> async def main(): ... pipeline = voh.OfflinePipeline( ... source=lambda: voh.sources.from_wav_file("meeting.wav", ... real_time=False), ... config=voh.OfflinePipelineConfig( ... diar={"backend": "pyannote"}, ... asr={"language": "en"}, ... llm={"engine": voh.resolve_engine()}, ... ), ... ) ... async for ev in pipeline.run(): ... print(ev) ... >>> asyncio.run(main())
The cadence trade
Because the four stages are decoupled by queues, each can run at its own pace : the diarizer waits for the entire PCM, the ASR streams through diarized segments as the diarizer finishes emitting them, the LLM analyst aggregates utterances as they land. The only end-to-end blocker is the offline diarizer itself.
- stage_failures: list[tuple[str, BaseException]]
(stage name, exception)for every stage that died this run — seeStageCrashed. Filled even whenstrict=False.
- subscribe_diarized(cb)[source]
Register an async callback fired for every
DiarizedSegment.- Parameters:
cb (Callable[[DiarizedSegment], Awaitable[None]])
- Return type:
None
- Parameters:
source (SourceFactory)
config (OfflinePipelineConfig | None)
strict (bool)
- class vocal_helper.pipeline.OfflinePipelineConfig(diar=<factory>, asr=<factory>, llm=None, qsize_pcm=200, qsize_seg=32)[source]
Bases:
objectConfiguration object for
OfflinePipeline.Same shape as
PipelineConfigminus the streaming-specificvadblock — the offline diarizer ingests the full audio and relies on the backend’s own VAD / segmentation.
- class vocal_helper.pipeline.Pipeline(*, source, config=None, metrics=None, strict=True)[source]
Bases:
objectEnd-to-end audio→text(→summary) producer/consumer chain.
Usage
>>> import asyncio, vocal_helper as voh >>> >>> async def main(): ... pipeline = voh.Pipeline( ... source=lambda: voh.sources.from_microphone(), ... config=voh.PipelineConfig( ... asr={"model": "large-v3-turbo-q5_0"}, ... llm={"engine": voh.resolve_engine()}, ... ), ... ) ... async for event in pipeline.run(): ... print(event) ... >>> asyncio.run(main())
Yielded events are a mix of
UtteranceandSummarySnapshot(the latter only whenllmis configured). For per-stage observation seePipeline.subscribe_voiced()/subscribe_diarized.- dropped_frames: int
Frames discarded under
source_policy="drop_oldest". Zero under back-pressure, by construction: that policy cannot lose a sample.
- stage_failures: list[tuple[str, BaseException]]
(stage name, exception)for every stage that died this run. Populated even whenstrict=False, so a lenient caller can still ask what went wrong.
- subscribe_diarized(cb)[source]
Async callback for every
DiarizedSegmentafter diarization.- Parameters:
cb (Callable[[DiarizedSegment], Awaitable[None]])
- Return type:
None
- subscribe_voiced(cb)[source]
Async callback for every
VoicedSegmentafter VAD.- Parameters:
cb (Callable[[VoicedSegment], Awaitable[None]])
- Return type:
None
- Parameters:
source (SourceFactory)
config (PipelineConfig | None)
metrics (LiveMetrics | None)
strict (bool)
- class vocal_helper.pipeline.PipelineConfig(vad=<factory>, eot=None, diar=<factory>, diar_latency_s=1.04, growing='auto', rediar=None, asr=<factory>, llm=None, qsize_pcm=200, qsize_seg=32, source_policy='backpressure')[source]
Bases:
objectConfiguration object for
Pipeline.Pass per-stage settings as dicts. The pipeline forwards them verbatim to each stage’s constructor.
- Parameters:
- exception vocal_helper.pipeline.StageCrashed(failures)[source]
Bases:
RuntimeErrorA pipeline stage died, so the result is incomplete.
Raised at the end of
Pipeline.run()/OfflinePipeline.run()when any stage crashed, because the alternative is worse than an exception: before this existed, a source that failed produced a normal, empty transcript. The crash was logged and the run returned successfully with nothing in it, so a caller saw silence and no reason for it — the same shape of failure the docstring of_drain_task()says once hid a broken pyannote API “undetected for hours”.Pass
strict=Falseto the pipeline to keep the lenient behaviour, which is a choice somebody then made on purpose rather than inherited by accident.- Parameters:
failures (list[tuple[str, BaseException]])
- Return type:
None
- vocal_helper.pipeline.choose_asr_engine(config, *, offline)[source]
Le moteur ASR à faire tourner :
config["engine"], ou le meilleur mesuré si"auto"."auto"(le défaut) suit les études de 2026-11, taux d’erreur sur les mots :langue à découvrir →
"nemotron", hors ligne comme en direct. Hors ligne (studies/asr_nemotron_vs_whisper_offline.py) : AMI 0,23 contre 0,38, voix mêlées 0,38 contre 0,54, Common Voice français 0,27 contre 0,69, trois fois plus vite. En direct, segment par segment après Silero (studies/asr_live_whisper_vs_nemotron.py) : AMI 0,24 contre 0,25, voix mêlées 0,38 contre 0,53, Common Voice français 0,27 contre 0,79, télévision 0,48 contre 0,83 — whisper devine mal la langue d’un segment court et le traduit en anglais ;langue donnée, prompt ou modèle whisper nommé →
"whisper": à qui l’on donne la langue, whisper rattrape Nemotron sur Common Voice français (0,28) et le dépasse sur la télévision française, et il reste devant en anglais lu (0,012 contre 0,067).
offlineest gardé pour les appelants ; la règle est la même des deux côtés.
- vocal_helper.pipeline.resolve_engine(*, endpoint=None)[source]
Resolve vocal_helper’s LLM engine descriptor via best-engine-ai-helper.
Loads
vocal_helper/llm.engine.yamlif present, else resolves it from the committedvocal_helper/llm.brief.yamlon first use (and writes it). The returned descriptor names the backend (Ollama / vLLM), base URL, and text model; it is what the LLM analyst and the semantic-EOT stage consume viabest_engine_ai_helper.llm.chat. No default model constant is involved — the model is read only from this resolved engine. Entry points call this once and thread the result into both stages.