Add NVIDIA Canary-1b-v2 ASR backend - #387
Conversation
LocalAgreement-driven `canary` backend around NeMo EncDecMultiTaskModel, with AmberNet (langid_ambernet) detect-once-then-lock auto language detection. Phase 2 (native AlignAtt streaming) deferred to a later spec.
Seven TDD tasks: config/CLI, pure timestamp+code-mapping helpers, CanarySessionASR auto-detect wrapper, CanaryASR/CanaryLID NeMo classes, core.py routing, optional extra + gated E2E test + docs.
Adds the canary_* fields to WhisperLiveKitConfig and a matching --canary-* argument group in parse_args, plus "canary" as a valid --backend choice. No model code yet -- config plumbing only, per Task 1 of the Canary-1b-v2 backend plan.
Adds the shared model holders that implement the LocalAgreement backend contract for Canary: CanaryASR wraps NeMo's ASRModel.transcribe() with word/segment timestamp extraction via the existing pure helpers, and CanaryLID wraps EncDecSpeakerLabelModel for spoken-language detection. Both import nemo/torch lazily so the module still imports cleanly without nemo_toolkit installed.
_do_init() now instantiates CanaryASR + CanaryLID when backend="canary", and online_factory() wraps sessions with CanarySessionASR (handling language=None by falling back to args.lan) before the generic SessionASRProxy wrap, so per-session LID/auto-detect works.
Adds the `canary` packaging extra (nemo-toolkit[asr]>=2.5.0), with uv conflict markers against voxtral-hf and qwen3-vllm-metal since both pin transformers ranges incompatible with NeMo's. Adds a NeMo-gated TestHarness end-to-end test that feeds a cached LibriSpeech sample through the full FFmpeg -> VAD -> Canary ASR -> LocalAgreement pipeline. Documents the backend in README.md alongside the other ASR backends (install, CLI flags, notes on the NeMo timestamp API).
…_prompt - core.py: run warmup_asr() in the canary branch of TranscriptionEngine._do_init so Canary gets the same startup warmup + fail-loudly-on-empty-output guard as every other LocalAgreement backend built through backend_factory. - canary_backend.py: log a warning in CanaryLID.detect() when a predicted label has no mapping into Canary's language set, instead of silently and permanently falling back to the default language. - canary_backend.py: document that CanaryASR.transcribe's init_prompt param is intentionally unused (Canary has no prompt-conditioning slot).
…committed text
Surfaced running the live server:
- LID model download failure (e.g. offline NGC) no longer aborts startup;
degrade to no auto-detect and use --canary-default-lang.
- Canary now emits space-prefixed word tokens with sep="" (faster-whisper
convention) so committed lines assembled via Segment.from_tokens (''.join)
are spaced instead of concatenated ("thequickbrown" -> "the quick brown").
- scripts/smoke_canary.py: load the Canary backend, transcribe a 16kHz clip, print word timestamps; runs on CPU (no GPU needed). Validates the backend end-to-end without a GPU. - Remove docs/superpowers/ (internal implementation-planning notes) from the contribution so the PR carries only the backend, tests, and docs.
9dca9e5 to
7ecbb5a
Compare
QuentinFuxa
left a comment
There was a problem hiding this comment.
Thanks for correcting the model id and for following the backend guide; the adapter structure is right and the smoke script is a good addition. Review of the current head:
Blockers:
- CI lint is red: ruff flags E402 twice in
tests/test_canary_backend.py(theimport importlib.utilandimport pytestat lines 194/196 must move to the top of the file; the_NEMO_AVAILABLE/requires_nemocomputation can stay where it is). - Language auto-detection looks wired to a config path NeMo does not expose: CanaryLID will not find the label list where it reads it (NeMo keeps it at
model._cfg['train_ds'].labels), so--language autolikely never detects and silently falls back. Please verify against a real checkpoint and make the no-labels case loud.
Asks:
3. A per-session ?language= outside CANARY_LANGS yields a silently dead session; validate it and surface the error like the config-time check does.
4. Missing NeMo raises a bare ModuleNotFoundError; wrap it with an actionable install hint like the other optional backends.
5. The README claims the canary extra is environment-incompatible with diarization-sortformer, but the two resolve together fine; drop or justify that claim. Also, the README says CUDA only while the PR body and the smoke script claim CPU validation; make them agree.
6. On Python 3.13 the canary extra resolves to nothing (NeMo's python pin), so users there get a confusing no-op install; add a config-time error or a clear README note.
7. Typography: several added comments and docs use em dashes; this repo does not use them, please replace with commas or colons.
On validation: every model-dependent test is NeMo-gated and skips in CI, so the accuracy and CPU claims rest entirely on your local runs. Before merging a 1B-model backend I would like one reproducible trace: please paste the full output of scripts/smoke_canary.py at the current head from a machine you control, with the exact command line.
|
Gentle ping on the July 22 review. The two blockers (ruff E402 in the test file, and the CanaryLID label-list path) are the only things standing between this and a merge; the rest can land as follow-ups if you prefer. Happy to re-review quickly once you push. |
… validation) Blockers: - tests: hoist importlib.util/pytest imports to top (ruff E402). - CanaryLID: resolve the class-label list across NeMo config layouts (cfg.labels and cfg.train_ds.labels) eagerly at load via resolve_lid_labels(); raise loudly if neither exists instead of indexing a possibly-missing cfg.labels in detect(). Follow-ups: - Reject per-session/config languages outside CANARY_LANGS: ValueError in CanarySessionASR.__init__, config-time check in core.py, and a client-facing websocket close(4400, reason) in basic_server.py. - Wrap the NeMo imports with an actionable install hint (_NEMO_INSTALL_HINT) that also names the Python 3.10-3.12 pin. - README: drop the false canary vs diarization-sortformer incompatibility (they co-resolve; both pull nemo-toolkit[asr]); CUDA -> CUDA/MPS/CPU; add a Python 3.10-3.12 note (the extra no-ops on 3.13). - Remove canary-added em dashes. - Add 4 non-NeMo-gated tests (label resolver + language rejection); 18 passed / 2 gated skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed Blockers1. ruff E402 — hoisted 2. CanaryLID label path — you were right to flag this, and it was worse than it looked: the
Asks
Added 4 non-NeMo-gated tests (label resolver + language rejection). 18 passed, 2 NeMo-gated skipped. Requested traceIsolated venv, NeMo 2.7.3 / torch 2.13 / Python 3.12, Darwin arm64, real 1B model from HF cache. I used a local clip because the bundled LibriSpeech sample download hits an SSL block on my network — the script's positional The one thing this trace does not cover is the LID/auto-detect path (blocker #2), for the network reason noted above. |
Fixed the issue, kindly check, thanks |
Summary
Adds a
canaryASR backend running NVIDIA Canary-1b-v2 on the existing LocalAgreement streaming policy (same mechanism as the Whisper backends). Canary-1b-v2 is a ~1B-param attention encoder-decoder (FastConformer encoder + Transformer decoder) loaded via NeMo'sEncDecMultiTaskModel; it emits native word/segment timestamps, covers 25 European languages, and runs on CUDA or CPU.The model is
nvidia/canary-1b-v2— model card: https://huggingface.co/nvidia/canary-1b-v2 (paper: https://arxiv.org/abs/2509.14128). There is no "Canny-qwen-1b"; that looks like a mix-up of Canary and Qwen. This backend is NVIDIA Canary 1B v2 only, unrelated to the Qwen3 backends. The id is the default everywhere (--canary-model,config.canary_model).Follows CLAUDE.md → "Adding a New ASR Backend"
whisperlivekit/canary_backend.py—CanaryASRimplementstranscribe(audio, init_prompt=""),ts_words(result),segments_end_ts(result),use_vad().sep,original_language,backend_choice,SAMPLING_RATE,confidence_validation,tokenizer,buffer_trimming,buffer_trimming_sec.core.py— anelif config.backend == "canary"branch inTranscriptionEngine._do_init()(before thebackend_policybranch), and a routing case inonline_factory()returningOnlineASRProcessorover a per-session wrapper."canary"added to--backendchoices inparse_args.py, plus a--canary-*option group.The module lazy-imports NeMo/torch, so it imports fine (and CI runs) without
nemo_toolkitinstalled.Extra pieces
CanaryLID(NeMolangid_ambernet) drives optional auto language detection when--language auto, via aCanarySessionASR(aSessionASRProxysubclass) that detects once on the first speech then locks the session language. If the LID model can't be loaded (e.g. offline), it degrades to--canary-default-langinstead of failing startup.pyproject.toml: acanaryoptional extra (nemo-toolkit[asr], marker matching the siblingdiarization-sortformer) with[tool.uv]conflict entries vsvoxtral-hf/qwen3-vllm-metal(NeMo'stransformerspin conflicts with those);uv.lockregenerated accordingly (large but auto-generated — the churn is uv's conflict-marker encoding, only +2 packages).--backend canary, and the--canary-*flags documented alongside the other backends.Validating without a GPU (per your ask)
Canary runs on CPU. Install and run the included smoke script:
Real output from a CPU/MPS run (macOS, no CUDA), transcribing a TTS clip of "the quick brown fox jumps over the lazy dog":
Equivalent in-pipeline check via
TestHarness(gated behind NeMo, skips when absent —tests/test_canary_backend.py::test_canary_end_to_end_via_testharness):I also ran it live end-to-end through the
/asrWebSocket UI on this branch (mic → FFmpeg → VAD → Canary → LocalAgreement), transcribing correctly and near-real-time on MPS.Test plan
pytest tests/test_canary_backend.py→ 14 passed, 2 skipped (the 2 skips are the NeMo-gated real-model tests). Covers helpers, VoxLingua→Canary code mapping, theCanarySessionASRdetect-once-then-lock state machine (incl. low-confidence / LID-exception / no-LID paths), andonline_factoryrouting. Runs in CI without NeMo./asrrun validated on CPU/MPS (above).scripts/smoke_canary.pyon your side to confirm on your hardware.Notes / caveats
main.langid_ambernetreachable; otherwise it falls back to--canary-default-lang(fail-soft).target_langis intentionally out of scope here; a native AlignAtt streaming variant could be a later follow-up.Happy to adjust anything to fit the project's conventions.