vocal_helper.router module

vocal_helper.router

The aiguilleur — the study-grounded diarization backend router.

Diarization is the one pipeline stage with a genuine backend fork, and there is no single best backend: the right one depends on whether the audio is a live stream or a batch file, on its duration, on the speaker count, and on whether the deployment can afford a PyTorch install. This module turns those scenario conditions into one explicit, testable decision that carries both quality (DER) and speed (RTF) so the CLIs and downstream code never hard-code a backend, re-derive the trade-off, or hide the cost.

Quality × speed per scenario (the whole point)

Numbers were re-validated on this machine (2026-07-19, studies/router_profile_validation.py, pyannote.metrics collar 0.25, median DER + RTF) against ground truth — bagarre (30 short mixes) + AMI dev-slice (2 real meetings). The sherpa DER is from ADR 0002; its ONNX models (pyannote-3.0 segmentation + TitaNet-large embedding) now ship in the diarization-engines bundle, so the torch-free path runs with no HuggingFace and no PyTorch. DER = quality (lower is better); RTF = speed (< 1 = faster than real time):

mode

backend

DER

RTF

when the router picks it

offline

nemo

0.142

0.051

short (≤300 s), ≤4 speakers — dense interleaved turns

offline

pyannote

0.122

0.067

long / unknown length / >4 speakers — the robust default

offline

sherpa

0.174

0.58

torch-free deployment (no PyTorch) — ADR 0002

online

nemo

0.586

0.030

any live stream (the default online embedder)

online

sherpa

0.174

0.58

torch-free streaming = periodic offline re-diarization

Why a router, not a default

Two independent findings, both measured on this machine:

  1. Offline: a length crossover. On short dense turns (bagarre, ~30 s) NeMo Sortformer wins by ~2.3x — offline DER 0.142 vs pyannote 0.330 — its end-to-end slot attribution drives speaker-confusion to ~0. On long meetings (AMI) the verdict reverses: pyannote median DER 0.122 (inside Bredin 2023’s 0.188 band), and Sortformer hangs past ~25 min (no output on a 27-min meeting) — its 90 s / 4-speaker cap puts long/crowded form out of distribution. So offline needs a router: “ship nemo” or “ship pyannote” is wrong for one common workload.

  2. Online: nemo, always. vocal-helper’s OnlineDiarStage is a latency-bound cosine-clustering approximation — inherently ~3-4x the offline DER (it cannot model overlap). Across lengths the NeMo TitaNet embedder is the best online backend (bagarre 0.586, AMI 0.497) and beats pyannote/embedding online (0.590 / 0.844), matching the 2026-06-30 embedding sweep that made it the default. There is no online length crossover — streaming routes to nemo (refine_on_close roughly halves the DER on meetings that over-segment, a stage knob the router leaves to the stage).

The torch-free sherpa (ONNX TitaNet-large, DER 0.174/0.148, beats NeMo Sortformer 0.267, FR+EN validated — ADR 0002) is the portability pick either way.

Scope

This module decides the diarization backend (mode + backend), the one stage with a real fork. The other stages are single-backend by study verdict and are not routed: VAD is Silero v5 (32 ms cadence), ASR is pywhispercpp large-v3-turbo (no faster-whisper win on CPU/Apple in the study), the analyst is Gemma via Ollama. Language is discovered, never routed to a default (see vocal_helper.lid).

Usage example

>>> from vocal_helper.router import select_diarization
>>> plan = select_diarization(live=False, duration_s=45.0, max_speakers=3)
>>> plan.backend, plan.expected_der, plan.expected_rtf
('nemo', 0.142, 0.051)
>>> select_diarization(live=False, duration_s=1800.0).backend
'pyannote'

Author

Warith HARCHAOUI — https://linkedin.com/in/warith-harchaoui

class vocal_helper.router.BackendPlan(mode, backend, expected_der, expected_rtf, reason)[source]

Bases: object

One routing decision: which diarizer, and its quality + speed.

Parameters:
mode

"online" (streaming OnlineDiarStage) or "offline" (whole-buffer OfflineDiarStage).

Type:

str

backend

Diarization backend — "pyannote", "nemo" or "sherpa" — to pass straight to the stage’s diar={"backend": ...} config.

Type:

str

expected_der

Representative diarization error rate (quality — lower is better) for this scenario, from the pdbms study / ADR 0002.

Type:

float

expected_rtf

Representative real-time factor (speed — < 1 is faster than real time) for this scenario.

Type:

float

reason

Human-readable justification citing the deciding measurement, surfaced to the operator so the choice is never a black box.

Type:

str

backend: str
expected_der: float
expected_rtf: float
mode: str
reason: str
vocal_helper.router.select_diarization(*, live, duration_s=None, max_speakers=None, torch_free=False, pyannote_available=True, nemo_available=True)[source]

Route to the diarization backend that the experiments justify.

Encodes the pdbms unified-study crossover (see the module docstring) as a single, testable decision over the conditions that actually move DER (quality) and RTF (speed): live-vs-batch, duration, speaker count, and torch-availability. The returned plan carries both axes explicitly.

Parameters:
  • live (bool) – True for a live stream (streaming diarizer), False for a batch file (whole-buffer offline diarizer — the reliable default for files).

  • duration_s (float or None, optional) – Audio duration in seconds when known (a file’s length is cheap to read). None means “unknown” and is treated as long-form — the safe, robust branch. Default None.

  • max_speakers (int or None, optional) – Known upper bound on the number of speakers. Used only to keep audio with more than SORTFORMER_MAX_SPEAKERS speakers off NeMo Sortformer (its hard 4-speaker cap). None = unknown. Default None.

  • torch_free (bool, optional) – True when the deployment cannot install PyTorch — routes to the sherpa onnxruntime backend regardless of length. Default False.

  • pyannote_available (bool, optional) – Whether the pyannote backend can actually run (extra installed + bundle present). When a rule would pick pyannote but it is unavailable, the router falls back rather than choosing an unrunnable backend. Default True.

  • nemo_available (bool, optional) – Whether the NeMo Sortformer backend can actually run (the nemo extra is importable). When the short/dense rule would pick nemo but it is not installed, the router falls through to the robust pyannote branch rather than emitting an unrunnable backend. Default True.

Returns:

The chosen mode + backend, its expected_der / expected_rtf, and the reason.

Return type:

BackendPlan

Examples

>>> select_diarization(live=False, duration_s=45.0, max_speakers=3).backend
'nemo'
>>> select_diarization(live=False, duration_s=1800.0).backend
'pyannote'
>>> select_diarization(live=True, torch_free=True).backend
'sherpa'
>>> # a short clip still routes to pyannote when the nemo extra is absent
>>> select_diarization(live=False, duration_s=45.0, nemo_available=False).backend
'pyannote'

Notes

The router decides which diarizer and reports its scenario quality/speed; the online/offline stages keep their own tuned knobs (join threshold, refine pass, chunk ceiling). Numbers were re-validated on this machine (studies/router_profile_validation.py, 2026-07-19) against ground truth; sherpa is from ADR 0002.