Skip to content

feat: add NVIDIA Nemotron 3.5 ASR backend (0.3.0) - #7

Merged
vr000m merged 12 commits into
mainfrom
feat/nemotron-asr-backend
Jun 6, 2026
Merged

feat: add NVIDIA Nemotron 3.5 ASR backend (0.3.0)#7
vr000m merged 12 commits into
mainfrom
feat/nemotron-asr-backend

Conversation

@vr000m

@vr000m vr000m commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a fourth ASR backend, nemotron (NVIDIA Nemotron 3.5 ASR streaming 0.6b via mlx-audio), mirroring ParakeetBackend structurally and shipped as 0.3.0 (additive, non-breaking). Built phase-by-phase via /conduct against the reviewed dev plan.

Phase 0 — dependency pin + real API verification (blocking gate)

Re-verified externals and probed the real installed post-#774 mlx-audio (not assumptions):

  • PR #774 MERGED — pinned the [dependency-groups] nemotron group to merge SHA 14add66. PyPI mlx-audio latest is 0.4.3 (2026-04-28), which predates #774, so a git-SHA pin is required.
  • generate(audio, *, language=None, …) -> AlignedResult: kwarg is language (not target_lang), takes a file path (like parakeet), returns the same AlignedResult dataclass with .text.
  • prompt_dictionary has 121 keys incl. "auto" (verified) and "en-US"; default_language = "auto"; unknown values degrade gracefully. No strip_lang_tags flag exists.
  • DEFAULT_NEMOTRON_LANGUAGE = "auto" decided — accepted token + the model's own default. End-to-end load(...).generate(<temp WAV>, language="auto") ran on-device and returned .text == "" for a sine tone (the empty-decode path).

What landed

  • stt_server/backends/nemotron.pyNemotronBackend + _NemotronStream. Lazy mlx_audio import in start()/_get_model() only (lean-base preserved), asyncio+threading decode lock pair, model load inside the thread lock, in-flight close() drain (3 s), private 0o700 temp-dir decode WAVs unlinked after decode, empty-decode contract. One material difference from parakeet: language is forwarded to generate() with the DEFAULT_NEMOTRON_LANGUAGE fallback when the client sends None. Docstring states the three-way language contract (parakeet ignores / mlx_whisper forwards / nemotron forwards-with-default), streaming-deferred note, and labels the Metal-drain rationale assumed-by-analogy for mlx-audio.
  • CLI + the two hard gates (landed in lockstep): __main__.py _make_backend/_resolve_model arms + --backend choices; render_stt_plist.py _BACKEND_RE widened; install_stt_agent.sh DEFAULT_MODEL nemotron arm (avoids the silent Whisper-fallback misconfig).
  • Tests: tests/test_nemotron_backend.py (full parakeet parity + two-way language split + PII/temp-dir/shutdown invariants); CLI choice enumeration; lean-base subprocess no-import proof; renderer allowlist; installer default-model regression.
  • Packaging/docs: version 0.3.0; README backend bullet, socket row, uv sync --group nemotron install block + dev-group rationale; CHANGELOG [0.3.0].

Packaging decision (Option 1) & verification

Nemotron ships behind a git-pinned [dependency-groups] dev group, not a published extra: a direct-URL dep can't appear in a published wheel's Requires-Dist (PyPI rejects it), which would block 0.3.0 from PyPI. PEP 735 groups are never emitted into wheel/sdist metadata. Verified: the built wheel and sdist Requires-Dist carry zero mlx-audio/direct-URL entries; Version: 0.3.0. Promote to a versioned nemotron extra once mlx-audio releases with #774 (tracked follow-up).

Validation

  • Full suite: 286 passed, 2 skipped (pre-existing). ruff format --check + ruff check clean.
  • Branch diff is additive; existing echo/mlx/parakeet backends unaffected (proven by the lean-base no-import subprocess test).

Review & follow-up commits

After the initial implementation, a multi-lens deep review + Codex review ran; all actionable findings were fixed:

  • fix(review) (691bf29): regression tests (test_install_migration.py, test_render_stt_plist.py) now import DEFAULT_NEMOTRON_MODEL instead of re-hardcoding the literal (a backend rename now fails the guard rather than passing stale); numpy moved from the nemotron dependency group to the always-synced dev group (it is test infra — mlx-audio pulls it transitively at runtime, so the nemotron group is just the git-pinned mlx-audio); a leaked file handle in a source-grep test wrapped in with. Re-verified: 286 passed / 2 skipped, ruff clean, wheel Requires-Dist still PyPI-clean.
  • chore (5f61733): gitignore .deep-review/ / .conduct/ local skill state (per the Codex [P3] note — run-specific state shouldn't be source-controlled).

vr000m added 12 commits June 5, 2026 13:46
The README documented `uv sync --extra stt-server-mlx` / `stt-server-parakeet`,
but pyproject names the extras `mlx` / `parakeet` / `client` -- so those
documented commands failed (uv resolves extras by their pyproject key). This was
leftover monorepo-extraction drift (stt_server/__init__.py said "extras split
once extracted").

Fix docs to match the shipped pyproject (non-breaking) rather than rename the
extras (which would break consumers already running `--extra parakeet`).
Corrected README executable commands + prose and the stray stt-server-* refs in
__init__.py, client.py, __main__.py, parakeet.py, mlx_teardown_spike.sh,
test_stt_server.py. Launchd labels (pipecat.stt-server.parakeet) left untouched.
New `nemotron` ASR backend via mlx-audio (Blaizzy/mlx-audio#774), mirroring
ParakeetBackend. Records two locked decisions: (1) ship the mlx-audio dep as a
git-pinned [dependency-groups] dev group so 0.3.0 stays PyPI-publishable until
mlx-audio releases #774; (2) extra/group naming uses the corrected convention.
Language-default (auto vs en-US vs omit) left as an integration-time design
question with trade-offs documented.
All /review-plan findings addressed (folded in by 80b79ad). Verified coverage:
_BACKEND_RE allowlist + install_stt_agent DEFAULT_MODEL (mandatory Phase 2/3),
full parakeet test-mirror (whitespace/identity/lean-base no-import/split-language),
assumption hedges (defensive getattr, Metal parity assumed-by-analogy, full
mlx_audio surface as Phase-0 integration gate), wheel METADATA no-leak proof.

Writes the /conduct readiness marker; Status -> Reviewed.
Pin mlx-audio to the PR #774 merge SHA in a [dependency-groups] nemotron dev
group (PyPI 0.4.3 predates Nemotron STT; keeps published 0.3.0 PyPI-clean).
uv.lock updated. Probed the installed package: generate(path, language=...)
returns AlignedResult.text; 'auto' is a verified prompt key and the model
default -> DEFAULT_NEMOTRON_LANGUAGE='auto'. End-to-end load+decode verified.
NemotronBackend + _NemotronStream mirror ParakeetBackend structurally: lazy
mlx_audio import in start()/_get_model(), asyncio+threading decode lock pair,
in-flight close() drain, private 0o700 temp-dir decode WAVs, empty-decode
contract. Material difference: language is forwarded to generate(path,
language=lang) with DEFAULT_NEMOTRON_LANGUAGE='auto' fallback when client
sends None. 24 tests pass (full parakeet parity + two-way language split +
PII/temp-dir/shutdown invariants); mlx_audio fully stubbed.
Wire nemotron into every backend-enumeration site, mirroring parakeet:
- __main__.py: _make_backend nemotron arm (lazy NemotronBackend import),
  _resolve_model nemotron arm (lazy DEFAULT_NEMOTRON_MODEL), --backend
  choices gain nemotron.
- render_stt_plist.py: _BACKEND_RE widened to include nemotron (hard gate).
- install_stt_agent.sh: DEFAULT_MODEL nemotron arm -> Nemotron repo id, not
  the silent Whisper fallback (hard gate); header enumeration updated.

Tests land together: argparse choice include + unknown-reject parity,
_resolve_model nemotron default + override, lean-base subprocess no-import
proof (blocks mlx_audio, both seams succeed), render-plist allowlist-pass,
and the installer default-model regression. 108 passed, 2 skipped (pre-existing).
Bump version 0.2.0 -> 0.3.0. README: NemotronBackend bullet with PII/temp-WAV
note, --backend {echo,mlx,parakeet,nemotron} enumeration, socket-convention
row (pipecat.stt-server.nemotron / nemotron.sock), install/smoke block using
'uv sync --group nemotron' with the dev-group rationale, model-table row.
CHANGELOG: [0.3.0] Added + Notes stating Option 1 landed, why no published
nemotron extra yet, and the PyPI-clean verification.

Packaging gate verified: wheel + sdist Requires-Dist carry no mlx-audio /
direct-URL entry (PEP 735 dev group does not leak); Version: 0.3.0. Full
suite 286 passed, 2 skipped; ruff format + check clean.
- Important: import DEFAULT_NEMOTRON_MODEL in the installer + renderer
  regression tests instead of re-hardcoding the literal, so a backend
  model-id rename fails the regression rather than passing against a stale
  copy (closes the installer second-point-of-truth coupling via its guard).
- numpy: move from the nemotron group to the always-synced dev group (it is
  test infra; mlx-audio pulls it transitively at runtime).
- Logic: wrap the source-grep file read in a with-statement (no leaked handle).

Full suite 286 passed, 2 skipped; ruff clean; wheel Requires-Dist still
carries no mlx-audio/direct-URL (PyPI-clean preserved).
Normalize the deep-review ignore entry to .deep-review/ (matching the
.conduct/ convention). Keeps local, run-specific review state out of source
control — per the Codex review note, the latest-* file bakes in run ids and
base/head commits that go stale immediately.
@vr000m
vr000m merged commit a0c3afa into main Jun 6, 2026
4 checks passed
@vr000m
vr000m deleted the feat/nemotron-asr-backend branch June 6, 2026 17:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant