Skip to content

feat(asr/nemotron): decode-time custom vocabulary biasing (no CTC head required) - #866

Merged
Alex-Wengg merged 4 commits into
mainfrom
feat/841-nemotron-vocab-bias
Aug 19, 2026
Merged

feat(asr/nemotron): decode-time custom vocabulary biasing (no CTC head required)#866
Alex-Wengg merged 4 commits into
mainfrom
feat/841-nemotron-vocab-bias

Conversation

@Alex-Wengg

@Alex-Wengg Alex-Wengg commented Aug 19, 2026

Copy link
Copy Markdown
Member

Closes #841.

Implements decode-time shallow-fusion hotword biasing for StreamingNemotronMultilingualAsrManager, based on the engine contributed inline in #841 by @vinod-memfold (Alma) — thank you for the detailed writeup, measurements, and field-testing addenda. This upstreams that design with the changes requested in the issue review.

What's here

  • NemotronVocabularyBias (new, beside the manager): matches vocabulary terms as lowercased-NFC piece text in Unicode-scalar space, so whichever segmentation the decoder drifts toward stays boostable. Word-start anchoring for space-delimited scripts (the CTC custom-vocabulary rescorer over-fires on short keyword terms since 0.14.8 (PR #634) #702 over-fire guard), unanchored 2-cluster terms for CJK, every viable match offset tracked.
  • Scalability (review ask): fresh-start candidates (offset 0, decode-state independent) are precomputed once at init; per-step continuation matching walks tail suffixes through a trie over term piece-forms. Per-emission cost is bounded by tail length and live partial matches, not vocabulary size — no per-term scan.
  • Weight safety (review ask): CustomVocabularyTerm.weight predates this engine and is a CTC-rescoring scale (the simple text-list loader assigns 10.0). Overrides are clamped to maxBoost = 6.0 with a one-time warning; the default boost is 4.5, the measured recall peak and the same cbw the tuned CTC rescorer settled on. Most terms should omit weight.
  • Explicit fused-argmax (B2) behavior (review ask + follow-up comment): with a vocabulary active, decode prefers a logits-producing step decoder (decoder_joint_noencproj / decoder_joint / bare pair) over decoder_joint_argmax; when B2 is the only step decoder, the vocabulary is rejected with an error log. All-or-nothing — no silently half-applied biasing.
  • Every argmax site biased and traced (follow-up comment): legacy per-frame loop (B3/B1/bare-pair), speculative scan, and all three drain branches; FLUIDAUDIO_BIAS_LOG=1 reports each boosted flip with both logits.
  • Match state resets on reset()/finish(); terms survive like the selected language and may be set before models load.
  • CLI: nemotron-multilingual-transcribe --custom-vocab <file> (JSON config or one-term-per-line text).
  • Docs: usage, weight semantics, asset requirements, greedy-decode and chunk-boundary caveats in Documentation/ASR/NemotronMultilingual.md.

Real-speech validation (review ask)

LibriSpeech test-clean, multilingual 2240ms B1 assets, debug build:

  • 7127-75947-0033 (three rare French names) baseline: "How is it Levallier? said Mademoiselle de Tenerchalte that the Vicomte de Bragalone spoke of you as Louise".
  • With {Valliere, Tonnay Charente, Bragelonne}: Tonnay Charente and Bragelonne substantially recover (Tonnay Chaunt / Bragelon at 6.0), driven by blank-overtake flips visible in the trace. "Levallier" is the documented alias-needed case — the model already spells it as a word it knows.
  • Neutrality: a 40-term invented-distractor vocabulary leaves a neutral utterance (1089-134686-0000) byte-identical with zero flips, at both 4.5 and 6.0.
  • False-fire profile: on the rare-name clip, distractors sharing the true audio's opening letters can contest the acoustically uncertain region (TenerchalteTone Chaunt) at both 4.5 and 6.0. This narrows Custom vocabulary for Nemotron streaming: decode-time RNN-T biasing (no CTC head required) #841's "0 false fires" claim (measured on 8+6 synthetic clips): confidently decoded speech is safe; contested OOV-name regions are not. Documented in the manager docs with guidance to keep vocabularies small and relevant — and it is why screen-harvested/speculative term lists (per the issue's own screen-feedback trap note) should be filtered before reaching this API.

Tests

20-case XCTest suite against a toy piece table (no CoreML): word-start anchoring, CJK unanchored terms, Devanagari conjunct rejoin in scalar space, multi-offset overlap through repeated leading words, alias/weight-clamp semantics, special-token immunity, reset lifecycle, biased selection including blank overtake, and a 2000-term scaling smoke.

Notes for reviewers

  • The sliding-window CTC path is untouched; CustomVocabularyTerm stays where it is to avoid churning open PRs (feat(asr/unified): vocabulary boosting on the unified Parakeet path #862) — moving it to shared ASR vocabulary code is a follow-up.
  • Whether text-list weight: 10.0 should map to the 4.5 default instead of clamping to 6.0 is the one open design call; clamping keeps monotonic semantics but is the aggressive end of the measured range.

Benchmark: earnings22-kws paired baseline-vs-biased (full 772 chunks, 3.2 h)

New nemotron-vocab-benchmark CLI command runs every chunk twice with the same manager (empty vocabulary vs the chunk's dictionary terms), scored with the same presence-based TP/FP/FN scheme as ctc-earnings-benchmark. Default boost 4.5, multilingual 2240 ms B1 assets:

chunk tier condition WER vocab recall precision RTFx
2240 ms baseline 19.77% 39.3% (TP=494, FN=762) 99.8% (FP=1) 88.2x
2240 ms biased 18.68% 59.7% (TP=750, FN=506) 99.6% (FP=3) 88.0x
1120 ms baseline 19.74% 39.8% (TP=500, FN=756) 99.8% (FP=1) 74.5x
1120 ms biased 18.76% 58.8% (TP=739, FN=517) 99.6% (FP=3) 74.4x

The 1120 ms tier doubles the chunk-boundary density yet gains the same recall (+19.0 vs +20.4 points) at the same WER improvement — corroborating the boundary-phase sweep: chunking is not the limiting factor for biasing.

Recall +20.4 points with WER improving 1.1 points; 2 net false fires over 3.2 hours; matcher overhead unmeasurable (RTFx flat, validating the trie + precomputed fresh-start design). Per-file regressions observed are the greedy over-boost class — an already-correct term perturbed by its own boost (LatamLatan, Andresandress).

Cross-chunk terms: supported (issue's caveat corrected)

#841 carried over the CTC path's "decoded as two independent halves" chunk-boundary caveat. It does not apply here: the decoder state and the bias match tail both persist across chunk boundaries (only reset()/finish() clear them). Verified by a boundary-phase sweep — stepping leading silence through a full 2240 ms chunk period (280 ms steps) on LibriSpeech 7127-75947-0033 sweeps the boundary through the "Tonnay Charente" audio, and biased recovery holds at every phase (full "Tonnay Charente" at two phases) while the unbiased baseline never recovers it. Docs updated accordingly.

Review fixes (c44696f)

Three findings from a local review pass, all addressed:

  1. B2 no longer blocks the advertised logits fallback. The load path's tier priority skips decoder_joint_noencproj/decoder_joint when decoder_joint_argmax exists, so a bundle shipping B2 plus a logits decoder still had biasing rejected or falling back to the slow bare pair. rebuildVocabularyBias now lazily loads decoder_joint from the remembered model directory before rejecting (B1 specifically, because it takes the plain encoder step every decode path has; B3 would also need an encoder_proj-emitting encoder and could silently strand the bias on B2).
  2. Plain-text vocabulary files no longer silently get the aggressive 6.0 boost. Weights above maxBoost (the text-list loader's blanket CTC-scale 10.0) now fall back to the measured 4.5 default with a warning, instead of clamping to the hottest allowed value; explicit weights in (0, 6.0] are honored. This also settles the open design call flagged above.
  3. Malformed JSON vocabulary files surface their parse error instead of being reinterpreted as hotword text: CustomVocabularyContext.loadVocabularyFile (first-meaningful-byte format detection) is now public and the CLI uses it.

Fixes 2 and 3 verified live (text-list run now logs the fallback and decodes identically to the 4.5 JSON run; truncated JSON aborts with the decode error). Fix 1 is compile-verified — no B2 asset exists locally to e2e it.

🤖 Generated with Claude Code

…d required)

Implements issue #841: token-level shallow fusion inside the Nemotron
multilingual streaming greedy RNN-T decode, based on the engine contributed
by the Alma team, with the changes requested in review:

- Scalability: fresh-start candidates (offset 0, decode-state independent)
  are precomputed once at init; per-step continuation matching walks tail
  suffixes through a trie over term piece-forms, so per-emission cost is
  bounded by tail length and live partial matches, not vocabulary size.
- Weight safety: CustomVocabularyTerm.weight predates this engine and is a
  CTC-rescoring scale (the simple text-list loader assigns 10.0). Applied
  raw as a per-token logit bonus it over-biases, so overrides are clamped
  to maxBoost=6.0 with a one-time warning; default boost is 4.5 (the
  measured recall peak, matching the tuned CTC rescorer cbw).
- Fused-argmax (B2) handling is explicit, not silent: with a vocabulary
  active, decode prefers a logits-producing step decoder over
  decoder_joint_argmax; if B2 is the only step decoder, the vocabulary is
  rejected with an error log (all-or-nothing, no half-applied biasing).
- Every argmax site is biased and traced: legacy per-frame loop (B3/B1/
  bare-pair), speculative scan, and all drain branches report boosted
  flips via FLUIDAUDIO_BIAS_LOG=1.
- Match state resets on reset()/finish(); terms survive like the selected
  language and may be set before models load.

CLI: nemotron-multilingual-transcribe gains --custom-vocab (JSON config or
one-term-per-line text).

Validated on real speech (LibriSpeech test-clean, cached 2240ms B1 assets):
7127-75947-0033 baseline misspells all three rare names (Levallier /
Tenerchalte / Bragalone); with the vocabulary, Tonnay Charente and
Bragelonne substantially recover, with blank-overtake flips visible in the
trace. Neutrality: a 40-term invented-distractor vocabulary leaves a
neutral utterance byte-identical with zero flips; on the rare-name clip,
distractors sharing the true audio's opening letters can contest the
uncertain region — documented as the false-fire profile, with guidance to
keep vocabularies small and relevant.

Unit tests cover word-start anchoring (#702 over-fire mode), CJK
unanchored 2-cluster terms, Devanagari conjunct rejoin in scalar space,
multi-offset overlap, alias/weight clamp semantics, special-token
immunity, reset lifecycle, biased selection incl. blank overtake, and a
2000-term scaling smoke.
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

PocketTTS Smoke Test ✅

Check Result
Build
Model download
Model load
Synthesis pipeline
Output WAV ✅ (146.3 KB)

Runtime: 0m8s

Note: PocketTTS uses CoreML MLState (macOS 15) KV cache + Mimi streaming state. CI VM lacks physical GPU — audio quality and performance may differ from Apple Silicon.

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Supertonic3 Smoke Test ✅

Check Result
Build
Model download (incl. VectorEstimatorVariants/ int4 buckets)
Model load
Synthesis pipeline (--ve-variant int4)
Output WAV ✅ (364.7 KB)

Runtime: 0m28s

Note: CI VMs lack a physical Neural Engine; the ANE-bucketed VectorEstimator falls back to CPU here. This validates download + variant resolution + synthesis, not ANE residency/perf.

…m vocabulary benchmark

Runs each earnings22-kws chunk twice with the same manager (empty vocabulary
vs the chunk's dictionary terms) and scores both with the presence-based
TP/FP/FN scheme ctc-earnings-benchmark uses, so the Nemotron decode-time
biasing path and the CTC rescoring path are comparable on the same rig.

First 50 chunks, default boost 4.5, multilingual 2240ms B1 assets:
  baseline  WER 21.58%  vocab recall 29.8% (TP=28 FN=66)  FP=0  RTFx 83.4x
  biased    WER 20.07%  vocab recall 48.9% (TP=46 FN=48)  FP=0  RTFx 83.2x

Recall +19.1 points with WER improving 1.5 points and no measurable decode
overhead from the matcher. Two per-file regressions observed, both the
greedy over-boost artifact class: an already-correct term perturbed by its
own boost (Latam->Latan, Andres->andress).
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Sortformer High-Latency Benchmark Results

ES2004a Performance (30.4s latency config)

Metric Value Target Status
DER 30.3% <35%
Miss Rate 28.2% - -
False Alarm 0.9% - -
Speaker Error 1.2% - -
RTFx 20.8x >1.0x
Speakers 4/4 - -

Sortformer High-Latency • ES2004a • Runtime: 2m 24s • 2026-08-19T08:26:33.248Z

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Offline VBx Pipeline Results

Speaker Diarization Performance (VBx Batch Mode)

Optimal clustering with Hungarian algorithm for maximum accuracy

Metric Value Target Status Description
DER 10.4% <20% Diarization Error Rate (lower is better)
RTFx 8.71x >1.0x Real-Time Factor (higher is faster)

Offline VBx Pipeline Timing Breakdown

Time spent in each stage of batch diarization

Stage Time (s) % Description
Model Download 24.209 20.1 Fetching diarization models
Model Compile 10.375 8.6 CoreML compilation
Audio Load 0.105 0.1 Loading audio file
Segmentation 31.071 25.8 VAD + speech detection
Embedding 120.153 99.8 Speaker embedding extraction
Clustering (VBx) 0.113 0.1 Hungarian algorithm + VBx clustering
Total 120.431 100 Full VBx pipeline

Speaker Diarization Research Comparison

Offline VBx achieves competitive accuracy with batch processing

Method DER Mode Description
FluidAudio (Offline) 10.4% VBx Batch On-device CoreML with optimal clustering
FluidAudio (Streaming) 17.7% Chunk-based First-occurrence speaker mapping
Research baseline 18-30% Various Standard dataset performance

Pipeline Details:

  • Mode: Offline VBx with Hungarian algorithm for optimal speaker-to-cluster assignment
  • Segmentation: VAD-based voice activity detection
  • Embeddings: WeSpeaker-compatible speaker embeddings
  • Clustering: PowerSet with VBx refinement
  • Accuracy: Higher than streaming due to optimal post-hoc mapping

🎯 Offline VBx Test • AMI Corpus ES2004a • 1049.0s meeting audio • 151.3s processing • Test runtime: 2m 39s • 08/19/2026, 04:19 AM EST

… biasing

The 'decoded as two independent halves' caveat was inherited from the
sliding-window CTC path, where windows really are re-decoded
independently. In the Nemotron streaming manager both the decoder state
and the bias match tail persist across chunk boundaries (only
reset()/finish() clear them), so cross-boundary terms keep their bias.

Verified empirically: stepping leading silence through a full 2240ms
chunk period (0-1960ms, 280ms steps) on LibriSpeech 7127-75947-0033
sweeps the boundary through the 'Tonnay Charente' audio; biased recovery
holds at every phase (Tonnay in 6/8, full Tonnay Charente at two phases)
while the unbiased baseline never recovers it.

Also records the full 772-chunk earnings22-kws paired benchmark:
  baseline  WER 19.77%  recall 39.3% (TP=494 FN=762)  FP=1  RTFx 88.2x
  biased    WER 18.68%  recall 59.7% (TP=750 FN=506)  FP=3  RTFx 88.0x
@github-actions

Copy link
Copy Markdown

✅ Nemotron Multilingual Benchmark — FLEURS

FLEURS en_us, chunk 2240ms, 100 samples, B1 fused decode path. Same English audio against both shipped models.

Model Language WER RTFx
latin/ (pruned 2828) English 7.95% 3.2x
multilingual/ (full 13087) English 7.91% 5.5x
Logs (tail)
[latin / English]

Language     | Prompt   | WER%   | CER%   | RTFx   | Duration  | Processed | Skipped
--------------------------------------------------------------------------------
en_us        | en-US    | 8.0    | 3.5    | 3.2    | 953.9s    | 100       | -
--------------------------------------------------------------------------------
AVERAGE      | —        | 8.0    | 3.5    | 3.2   


[multilingual / English]

Language     | Prompt   | WER%   | CER%   | RTFx   | Duration  | Processed | Skipped
--------------------------------------------------------------------------------
en_us        | en-US    | 7.9    | 3.6    | 5.5    | 953.9s    | 100       | -
--------------------------------------------------------------------------------
AVERAGE      | —        | 7.9    | 3.6    | 5.5   

@github-actions

Copy link
Copy Markdown

✅ Nemotron Multilingual Benchmark — FLEURS

FLEURS en_us, chunk 2240ms, 100 samples, B1 fused decode path. Same English audio against both shipped models.

Model Language WER RTFx
latin/ (pruned 2828) English 7.91% 6.5x
multilingual/ (full 13087) English 7.91% 5.5x
Logs (tail)
[latin / English]

Language     | Prompt   | WER%   | CER%   | RTFx   | Duration  | Processed | Skipped
--------------------------------------------------------------------------------
en_us        | en-US    | 7.9    | 3.5    | 6.5    | 953.9s    | 100       | -
--------------------------------------------------------------------------------
AVERAGE      | —        | 7.9    | 3.5    | 6.5   


[multilingual / English]

Language     | Prompt   | WER%   | CER%   | RTFx   | Duration  | Processed | Skipped
--------------------------------------------------------------------------------
en_us        | en-US    | 7.9    | 3.6    | 5.5    | 953.9s    | 100       | -
--------------------------------------------------------------------------------
AVERAGE      | —        | 7.9    | 3.6    | 5.5   

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

ASR Benchmark Results ✅

Status: All benchmarks passed

Parakeet v3 (multilingual)

Dataset WER Avg WER Med RTFx Status
test-clean 0.57% 0.00% 3.64x
test-other 1.19% 0.00% 2.33x

Parakeet v2 (English-optimized)

Dataset WER Avg WER Med RTFx Status
test-clean 0.80% 0.00% 4.09x
test-other 1.00% 0.00% 2.96x

Streaming (v3)

Metric Value Description
WER 0.00% Word Error Rate in streaming mode
RTFx 0.43x Streaming real-time factor
Avg Chunk Time 2.132s Average time to process each chunk
Max Chunk Time 2.536s Maximum chunk processing time
First Token 2.681s Latency to first transcription token
Total Chunks 31 Number of chunks processed

Streaming (v2)

Metric Value Description
WER 0.00% Word Error Rate in streaming mode
RTFx 0.48x Streaming real-time factor
Avg Chunk Time 1.842s Average time to process each chunk
Max Chunk Time 2.120s Maximum chunk processing time
First Token 1.892s Latency to first transcription token
Total Chunks 31 Number of chunks processed

Streaming tests use 5 files with 0.5s chunks to simulate real-time audio streaming

25 files per dataset • Test runtime: 11m46s • 08/19/2026, 04:39 AM EST

RTFx = Real-Time Factor (higher is better) • Calculated as: Total audio duration ÷ Total processing time
Processing time includes: Model inference on Apple Neural Engine, audio preprocessing, state resets between files, token-to-text conversion, and file I/O
Example: RTFx of 2.0x means 10 seconds of audio processed in 5 seconds (2x faster than real-time)

Expected RTFx Performance on Physical M1 Hardware:

• M1 Mac: ~28x (clean), ~25x (other)
• CI shows ~0.5-3x due to virtualization limitations

Testing methodology follows HuggingFace Open ASR Leaderboard

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Parakeet EOU Benchmark Results ✅

Status: Benchmark passed
Chunk Size: 320ms
Files Tested: 100/100

Performance Metrics

Metric Value Description
WER (Avg) 7.03% Average Word Error Rate
WER (Med) 4.17% Median Word Error Rate
RTFx 10.17x Real-time factor (higher = faster)
Total Audio 470.6s Total audio duration processed
Total Time 48.5s Total processing time

Streaming Metrics

Metric Value Description
Avg Chunk Time 0.049s Average chunk processing time
Max Chunk Time 0.097s Maximum chunk processing time
EOU Detections 0 Total End-of-Utterance detections

Test runtime: 0m56s • 08/19/2026, 04:32 AM EST

RTFx = Real-Time Factor (higher is better) • Processing includes: Model inference, audio preprocessing, state management, and file I/O

@github-actions

Copy link
Copy Markdown

✅ Nemotron Multilingual Benchmark — FLEURS

FLEURS en_us, chunk 2240ms, 100 samples, B1 fused decode path. Same English audio against both shipped models.

Model Language WER RTFx
latin/ (pruned 2828) English 7.84% 4.9x
multilingual/ (full 13087) English 7.91% 5.4x
Logs (tail)
[latin / English]

Language     | Prompt   | WER%   | CER%   | RTFx   | Duration  | Processed | Skipped
--------------------------------------------------------------------------------
en_us        | en-US    | 7.8    | 3.5    | 4.9    | 953.9s    | 100       | -
--------------------------------------------------------------------------------
AVERAGE      | —        | 7.8    | 3.5    | 4.9   


[multilingual / English]

Language     | Prompt   | WER%   | CER%   | RTFx   | Duration  | Processed | Skipped
--------------------------------------------------------------------------------
en_us        | en-US    | 7.9    | 3.6    | 5.4    | 953.9s    | 100       | -
--------------------------------------------------------------------------------
AVERAGE      | —        | 7.9    | 3.6    | 5.4   

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

VAD Benchmark Results

Performance Comparison

Dataset Accuracy Precision Recall F1-Score RTFx Files
MUSAN 94.0% 89.3% 100.0% 94.3% 340.2x faster 50
VOiCES 94.0% 89.3% 100.0% 94.3% 331.4x faster 50

Dataset Details

  • MUSAN: Music, Speech, and Noise dataset - standard VAD evaluation
  • VOiCES: Voices Obscured in Complex Environmental Settings - tests robustness in real-world conditions

✅: Average F1-Score above 70%

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Speaker Diarization Benchmark Results

Speaker Diarization Performance

Evaluating "who spoke when" detection accuracy

Metric Value Target Status Description
DER 15.1% <30% Diarization Error Rate (lower is better)
JER 24.9% <25% Jaccard Error Rate
RTFx 17.29x >1.0x Real-Time Factor (higher is faster)

Diarization Pipeline Timing Breakdown

Time spent in each stage of speaker diarization

Stage Time (s) % Description
Model Download 14.085 23.2 Fetching diarization models
Model Compile 6.037 9.9 CoreML compilation
Audio Load 0.091 0.2 Loading audio file
Segmentation 18.195 30.0 Detecting speech regions
Embedding 30.325 50.0 Extracting speaker voices
Clustering 12.130 20.0 Grouping same speakers
Total 60.681 100 Full pipeline

Speaker Diarization Research Comparison

Research baselines typically achieve 18-30% DER on standard datasets

Method DER Notes
FluidAudio 15.1% On-device CoreML
Research baseline 18-30% Standard dataset performance

Note: RTFx shown above is from GitHub Actions runner. On Apple Silicon with ANE:

  • M2 MacBook Air (2022): Runs at 150 RTFx real-time
  • Performance scales with Apple Neural Engine capabilities

🎯 Speaker Diarization Test • AMI Corpus ES2004a • 1049.0s meeting audio • 60.7s diarization time • Test runtime: 2m 50s • 08/19/2026, 04:37 AM EST

1. B2 no longer blocks the logits fallback it advertises. The load path's
   tier priority skips decoder_joint_noencproj/decoder_joint when
   decoder_joint_argmax exists, so a bundle shipping B2 alongside a logits
   decoder still had biasing rejected (or silently falling to the slow
   bare pair). rebuildVocabularyBias now lazily loads decoder_joint from
   the remembered model directory before rejecting. B1 specifically: it
   takes the plain encoder step every decode path has, whereas B3 also
   needs an encoder that emits encoder_proj — loading a model the decode
   loop can't feed would silently fall back to unbiased B2, the exact
   failure this avoids. setCustomVocabulary/rebuildVocabularyBias become
   async for the lazy load.

2. Legacy weights above maxBoost fall back to the 4.5 default instead of
   clamping to the aggressive 6.0 ceiling. The simple text-list loader
   blankets every term with CTC-scale weight 10.0, so plain text files
   were silently pinned to the hottest allowed boost with no way to
   request the measured default. Explicit weights in (0, 6.0] are still
   honored. Verified live: a text-list vocabulary now logs the fallback
   warning and decodes identically to the 4.5 JSON run.

3. Malformed JSON vocabulary files now surface their parse error instead
   of being reinterpreted as one-hotword-per-line text.
   CustomVocabularyContext.loadVocabularyFile (first-meaningful-byte
   format detection) is now public and the CLI uses it; the try/fallback
   in nemotron-multilingual-transcribe is gone. Verified live: truncated
   JSON aborts the run with the decode error.
@github-actions

Copy link
Copy Markdown

✅ Nemotron Multilingual Benchmark — FLEURS

FLEURS en_us, chunk 2240ms, 100 samples, B1 fused decode path. Same English audio against both shipped models.

Model Language WER RTFx
latin/ (pruned 2828) English 7.84% 4.0x
multilingual/ (full 13087) English 8.02% 4.5x
Logs (tail)
[latin / English]

Language     | Prompt   | WER%   | CER%   | RTFx   | Duration  | Processed | Skipped
--------------------------------------------------------------------------------
en_us        | en-US    | 7.8    | 3.5    | 4.0    | 953.9s    | 100       | -
--------------------------------------------------------------------------------
AVERAGE      | —        | 7.8    | 3.5    | 4.0   


[multilingual / English]

Language     | Prompt   | WER%   | CER%   | RTFx   | Duration  | Processed | Skipped
--------------------------------------------------------------------------------
en_us        | en-US    | 8.0    | 3.6    | 4.5    | 953.9s    | 100       | -
--------------------------------------------------------------------------------
AVERAGE      | —        | 8.0    | 3.6    | 4.5   

@Alex-Wengg
Alex-Wengg merged commit 0dc46a3 into main Aug 19, 2026
12 checks passed
@Alex-Wengg
Alex-Wengg deleted the feat/841-nemotron-vocab-bias branch August 19, 2026 13:56
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.

Custom vocabulary for Nemotron streaming: decode-time RNN-T biasing (no CTC head required)

1 participant