Skip to content

fix(asr/nemotron-multilingual): rescue pause-delimited speech spans that decode to all-blank (#838) - #865

Merged
Alex-Wengg merged 2 commits into
mainfrom
fix/838-isolated-word-blank
Aug 19, 2026
Merged

fix(asr/nemotron-multilingual): rescue pause-delimited speech spans that decode to all-blank (#838)#865
Alex-Wengg merged 2 commits into
mainfrom
fix/838-isolated-word-blank

Conversation

@Alex-Wengg

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

Copy link
Copy Markdown
Member

Fixes #838.

Problem

On the Nemotron multilingual streaming manager, a short word spoken in isolation between pauses can silently decode to all-blank, and whether it does depends on the preceding audio, not the word (see #838 for the original repro and narrowing).

Reproduced with the issue's say-clip harness, swept across 19 lead voices (the reporter's exact voices didn't fail on this machine — say output differs across macOS versions, and the bug is state-dependent): the same 0.39 s "Gemma" clip was silently dropped in 33 of 171 trials, with one lead voice (Karen) dropping it at every gap from 0 to 2000 ms and a Samantha lead never dropping it — exactly the reported shape.

Root cause

  • Not chunk geometry. The failure is unchanged at the 1120 ms and 2240 ms tiers, so the [42,13]-lookahead vs 7-frame-chunk theory from the issue thread is exonerated.
  • The carried encoder+decoder state, jointly. Mid-stream isolation experiments: resetting only the decoder LSTM state or only the encoder cache makes decode worse (the two must stay coherent); resetting both — equivalent to a fresh session — recovers the word. This matches the reporter's follow-up finding that the same span always decodes correctly standalone. It is a model-level sensitivity of the cache-aware export, not a Swift pipeline bug, so the fix is a targeted repair rather than a decode-path change.

Fix: blank-span rescue

Same shape as the #861 final-window re-decode on the sliding-window path, applied to pause-delimited spans:

  • Track speech spans with an 80 ms RMS gate (span closes after 160 ms of silence; 240 ms pre-roll).
  • When a span closes and no lexical token timing falls inside it, re-decode the buffered span audio on fresh encoder caches + decoder state, appending recovered tokens to the transcript. The live stream's state is saved and restored around the rescue (loopback outputs are never output-backed, so the saved references stay valid); forced-prefix language seeding is re-applied.
  • Attribution details that mattered in practice:
    • Lexical tokens only — with long pauses the decode often spends the span's frames emitting the previous sentence's terminal punctuation ("…afternoon." + dropped word); punctuation must not mask a swallowed word.
    • 400 ms close-side slack — RNN-T emissions lag the audio; without it a late-emitted word gets duplicated by the rescue ("Jenna Gemma").
  • A span that was never speech re-decodes to nothing, so a false trigger is a no-op costing only the extra decode. The normal decode path is untouched whenever spans emit normally.

The trial decode is staged and validated before anything reaches the transcript: its tokens are captured and removed from the accumulators, partial callbacks are suppressed for its duration (one callback fires after a successful commit), and the result is committed only when it contains lexical content — inserted at the span's timestamp position (lang-tag-aware index mapping), so words already decoded from later audio in the same 1120/2240 ms chunk keep their order. Staged timings are clamped to the span's real extent; without the clamp, the rescue decode's own emission latency stamps trailing tokens past the span end and masks a second consecutive drop (observed on the stress fixture: the utterance right after a rescued word also decoded to all-blank and was silently lost — with the clamp, back-to-back drops rescue independently and in order).

Per the issue's API ask, two counters are public and cleared on reset(): detectedBlankSpanCount (a live span decoded to all-blank — the drop signal, even when the rescue also comes back blank) and blankRescueCount (committed lexical recoveries, always ≤ detected). Env knobs: FLUIDAUDIO_DISABLE_BLANK_RESCUE opts out; FLUIDAUDIO_RESCUE_RMS_THRESHOLD tunes the silence gate (default 0.0025, 0 disables).

Results

Repro matrix (19 voices × 9 gaps, 560 ms tier):

true silent drops
before 33 / 171
after 7 / 171 — all at gap ≤ 200 ms

The 7 remaining drops have no VAD-detectable pause to split the span (gap ≤ 200 ms) — the same inherent limitation as the reporter's Silero-based rescue workaround. Remaining non-drop misses are homophone mishears ("Gemma" → "Jenna"), present with and without the fix (that's #841 territory, not a drop).

Regression A/B (FLUIDAUDIO_DISABLE_BLANK_RESCUE=1 vs default), 560 ms tier:

dataset WER (off / on) CER (off / on) RTFx (off / on)
FLEURS en_us, 50 files 8.2 / 8.2 4.4 / 4.5 60.0 / 59.5
FLEURS cmn_hans_cn, 20 files 17.5 / 17.5 17.5 / 17.5 54.6 / 54.5

Also corrects the stale att_context_size=[56,0] comments — the published multilingual artifacts ship [42,13] with channel cache [1,24,42,1024] per their metadata.json.

Not covered (follow-ups)

  • Gap ≤ 200 ms drops (word butts against the previous utterance with no pause) — needs a different mechanism.
  • The English StreamingNemotronAsrManager likely has the same failure class and could take the same rescue; untested here.
  • Rescued tokens are timestamp-ordered in the timing stream (post-review); tokens decoded from the rescue's zero-pad flush are clamped to the span end, so their timings are span-accurate but not frame-exact.

🤖 Generated with Claude Code

…hat decode to all-blank (#838)

The cache-aware encoder + RNN-T decoder carry state across chunks. After
certain preceding audio, the greedy decode collapses to blank for an
entire short, pause-delimited word and silently drops it (issue #838).
Reproduced with the issue's say-clip harness across 19 lead voices: the
same 0.39s word was dropped in 33/171 trials, with one lead voice (Karen)
dropping it at every gap from 0 to 2000ms.

Isolation showed the failure lives in the carried encoder+decoder state
jointly: resetting either side alone makes decode worse, resetting both
(= fresh session) recovers the word, and the failure persists unchanged
at the 1120ms and 2240ms tiers, exonerating chunk geometry. The same span
always decodes correctly from fresh state, matching the reporter's
standalone-replay finding.

Fix: blank-span rescue, mirroring the #861 final-window re-decode on the
sliding-window path. The manager tracks speech spans with an 80ms RMS
gate (span closes after 160ms of silence, 240ms pre-roll). When a span
closes and no lexical token timing falls inside it, the span audio is
re-decoded on fresh encoder caches + decoder state (live stream state is
saved and restored; forced-prefix language seeding is re-applied) and
recovered tokens are appended to the transcript. Punctuation-only
emissions do not mask a span: with long pauses the decode often spends
the span's frames on the previous sentence's terminal punctuation.
A 400ms close-side attribution slack absorbs RNN-T emission latency so a
late-emitted word does not get duplicated by the rescue.

Results on the repro matrix (19 voices x 9 gaps, 560ms tier): true
silent drops 33 -> 7, and all 7 remaining are gap <= 200ms where no
VAD-detectable pause exists to split the span (same limitation as the
reporter's Silero-based workaround). Remaining non-drop misses are
homophone mishears (Gemma -> Jenna), present with and without the fix.

Regression A/B (FLUIDAUDIO_DISABLE_BLANK_RESCUE=1 vs default):
- FLEURS en_us, 50 files, 560ms: WER 8.2 both, CER 4.4 vs 4.5,
  RTFx 60.0 vs 59.5
- FLEURS cmn_hans_cn, 20 files, 560ms: WER/CER 17.5/17.5 identical,
  RTFx 54.6 vs 54.5

The rescue exposes blankRescueCount (nonzero = the live decode silently
dropped pause-delimited speech) as a caller-visible signal, per the
issue's API ask. Env knobs: FLUIDAUDIO_DISABLE_BLANK_RESCUE opts out,
FLUIDAUDIO_RESCUE_RMS_THRESHOLD tunes the silence gate (default 0.0025,
0 disables).

Also corrects the stale att_context_size=[56,0] comments: published
multilingual artifacts ship [42,13] with channel cache [1,24,42,1024]
per their metadata.json.
@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: 0m26s

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.

@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% 731.1x faster 50
VOiCES 94.0% 89.3% 100.0% 94.3% 745.5x 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

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 22.2x >1.0x
Speakers 4/4 - -

Sortformer High-Latency • ES2004a • Runtime: 2m 20s • 2026-08-19T08:11:27.833Z

@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 21.52x >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.784 30.3 Fetching diarization models
Model Compile 6.336 13.0 CoreML compilation
Audio Load 0.111 0.2 Loading audio file
Segmentation 14.620 30.0 Detecting speech regions
Embedding 24.366 50.0 Extracting speaker voices
Clustering 9.747 20.0 Grouping same speakers
Total 48.772 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 • 48.7s diarization time • Test runtime: 2m 47s • 08/19/2026, 04:15 AM EST

@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

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 8.02% 4.6x
multilingual/ (full 13087) English 8.13% 4.6x
Logs (tail)
[latin / English]

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


[multilingual / English]

Language     | Prompt   | WER%   | CER%   | RTFx   | Duration  | Processed | Skipped
--------------------------------------------------------------------------------
en_us        | en-US    | 8.1    | 3.7    | 4.6    | 953.9s    | 100       | -
--------------------------------------------------------------------------------
AVERAGE      | —        | 8.1    | 3.7    | 4.6   

@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 9.69x >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 23.485 21.7 Fetching diarization models
Model Compile 10.065 9.3 CoreML compilation
Audio Load 0.066 0.1 Loading audio file
Segmentation 26.703 24.6 VAD + speech detection
Embedding 108.087 99.8 Speaker embedding extraction
Clustering (VBx) 0.110 0.1 Hungarian algorithm + VBx clustering
Total 108.336 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 • 134.9s processing • Test runtime: 2m 26s • 08/19/2026, 04:07 AM EST

@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 7.16x Real-time factor (higher = faster)
Total Audio 470.6s Total audio duration processed
Total Time 65.5s Total processing time

Streaming Metrics

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

Test runtime: 1m14s • 08/19/2026, 04:04 AM EST

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

@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% 4.91x
test-other 1.56% 0.00% 2.95x

Parakeet v2 (English-optimized)

Dataset WER Avg WER Med RTFx Status
test-clean 0.80% 0.00% 3.26x
test-other 1.62% 0.00% 2.34x

Streaming (v3)

Metric Value Description
WER 0.00% Word Error Rate in streaming mode
RTFx 0.50x Streaming real-time factor
Avg Chunk Time 1.860s Average time to process each chunk
Max Chunk Time 2.214s Maximum chunk processing time
First Token 2.257s 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.58x Streaming real-time factor
Avg Chunk Time 1.575s Average time to process each chunk
Max Chunk Time 3.182s Maximum chunk processing time
First Token 1.867s 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: 10m13s • 08/19/2026, 04:14 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

…cued tokens (#838 review)

Addresses the three review findings on PR #865:

1. Ordering (P1): the rescue previously appended recovered tokens to the
   accumulators, so when a dropped span and later speech shared one
   1120/2240ms chunk the transcript read 'later-word rescued-word'. The
   trial decode is now staged (captured and removed from the accumulators)
   and committed via mergeRescuedTokens, which inserts at the span's
   timestamp position with lang-tag-aware id/timing index mapping.
   Verified with a lead + dropped-word + trailing-speech fixture at all
   three tiers: 'Gemma thanks everyone', never inverted.

2. Unvalidated commit (P2): the rescue is committed only when the staged
   output contains lexical content; lang-tag re-emissions are dropped
   before the check and punctuation-only results leave the transcript
   untouched. Partial callbacks are suppressed during the trial decode
   (they would surface unvalidated, out-of-order text) and a single
   callback fires after a successful commit. Attempted spans are now
   counted separately: detectedBlankSpanCount increments whenever a live
   span decodes to all-blank (the caller-visible drop signal even when
   the rescue also comes back blank), blankRescueCount only on committed
   lexical recovery.

3. Counter lifecycle (P2): both counters are cleared in
   resetRescueState(), which runs from resetStates() on reset(),
   loadModels(), and loadFromShared().

Validation also surfaced a chained-drop bug the review scenario exposed:
the rescue decode's own emission latency (plus the zero-pad flush) stamps
trailing recovered tokens past the span end, and those timings bled into
the NEXT span's attribution window - masking a second consecutive drop
(the tail utterance after a rescued word decoded to all-blank at 560ms
and was never rescued). Staged timings are now clamped to the span's
real extent, after which back-to-back drops rescue independently
(det=2/resc=2 on the stress fixture, both words recovered in order).

Re-validated: 19-voice x 9-gap sweep unchanged at 16 misses (7 true
drops, all gap <= 200ms; rest homophone mishears); FLEURS en_us 50-file
A/B remains WER 8.2 / CER 4.5 / RTFx 59.9 (baseline 8.2/4.4/60.0).

Tests: mergeRescuedTokens ordering (mid-insert, append-at-end, leading
lang-tag offset, empty live), counter clearing across reset().
@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 8.06% 3.6x
multilingual/ (full 13087) English 8.09% 3.8x
Logs (tail)
[latin / English]

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


[multilingual / English]

Language     | Prompt   | WER%   | CER%   | RTFx   | Duration  | Processed | Skipped
--------------------------------------------------------------------------------
en_us        | en-US    | 8.1    | 3.7    | 3.8    | 953.9s    | 100       | -
--------------------------------------------------------------------------------
AVERAGE      | —        | 8.1    | 3.7    | 3.8   

@Alex-Wengg
Alex-Wengg merged commit 2ef3a0b into main Aug 19, 2026
12 checks passed
@Alex-Wengg
Alex-Wengg deleted the fix/838-isolated-word-blank 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.

nemotron-multilingual 560ms streaming: isolated short word silently decodes to blank, controlled by the preceding audio

1 participant