fix: retry an ONNX decode that comes back empty - #257
Merged
jatinkrmalik merged 1 commit intoSep 5, 2026
Conversation
Padding the edges was not enough. A real 4s recording that this app dropped shows why: the audio decodes to nothing, and the same samples scaled by 1.001 decode to the full sentence while 0.999 still decode to nothing. Shifting the speech by 100ms flips the result either way. The decoders stop the moment they emit end-of-transcript and for some inputs emit it as their very first token; which inputs is not predictable from anything the app can measure, so no amount of tuning the padding fixes it. Retry instead. When a segment decodes to nothing, decode it again with the silence distributed differently — all of it in front, all behind, then split unevenly. Every layout adds exactly as much as the first attempt, so a retry can never push a segment past the one-pass limit the segmenter respects, and retries only ever follow an attempt that already produced nothing. Measured against 20 inputs derived from that recording, each confirmed to decode to nothing on this model: the ladder recovers all 20, including one that no single reframing in the search recovered on its own. The 11 clips that already worked are unchanged.
✅ Deploy Preview for voca-mac canceled.
|
|
| Filename | Overview |
|---|---|
| Sources/VocaMac/Services/SherpaAudioPreparation.swift | Adds configurable, fixed-total silence layouts while preserving minimum waveform length and model-length budgeting. |
| Sources/VocaMac/Services/SherpaService.swift | Adds bounded retries for empty decodes using independently prepared waveforms and existing per-call decoder streams. |
| Tests/VocaMacTests/SherpaAudioPreparationTests.swift | Verifies that recovery layouts preserve prepared length, speech samples, and distinct framing. |
| Tests/VocaMacTests/SherpaServiceTests.swift | Verifies recovery sequencing, no retry after success, and termination after exhausting layouts. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Prepare segment with balanced silence] --> B[Decode]
B --> C{Transcript empty?}
C -- No --> D[Return first result]
C -- Yes --> E[Prepare next recovery layout]
E --> F[Decode reframed segment]
F --> G{Transcript recovered?}
G -- Yes --> H[Return recovered result]
G -- No, layouts remain --> E
G -- No layouts remain --> I[Return original empty result]
A --> J{Digital silence?}
J -- Yes --> K[Skip segment]
Reviews (1): Last reviewed commit: "fix: retry an ONNX decode that comes bac..." | Re-trigger Greptile
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #256. That fix was real but incomplete — recordings were still being dropped, and this is the actual mechanism.
What the padding fix missed
#256 assumed speech starting in sample zero was the trigger. It was a trigger, and padding killed it: 174 generated clips and 12 with synthetic room-noise floors all pass. But real recordings kept coming back empty.
A dump of one — a 4.0s clip this app dropped — shows why:
"""Hey, let's see if this sucks for eight seconds of audio."""""A 0.1% amplitude change is the difference between a perfect transcript and nothing at all. The decoders stop the moment they emit
<|endoftext|>and for some inputs emit it as their very first token:Which inputs land on that knife edge is not predictable from anything the app can measure — level, duration, DC offset, noise floor and language were all tested and none separate the failures. So no amount of tuning the padding fixes this. It is chaotic sensitivity in an upstream greedy decoder.
The fix
When a segment decodes to nothing, decode it again with the same silence distributed differently: all in front, all behind, then split unevenly.
Two properties keep this honest:
Verification
From the dumped recording I derived 20 inputs, each independently confirmed to decode to nothing on this model (gain-scaled and time-shifted variants — every one a genuine reproduction of the bug).
The ladder recovers all 20, including one (
+2000ms lead-in) that no single reframing in the search recovered on its own. The 11 clips that already worked are unchanged.swift test: 481 tests, 0 failures. New tests cover that a retry never changes the segment's length, that a successful decode is never retried, and that the ladder terminates.Upstream
The real defect is in sherpa-onnx:
offline-recognizer-canary-impl.htakesargmaxof the first decoder logits unguarded, so when that argmax is<|endoftext|>the token loop never runs andtokens.pop_back()empties the result. Its reference script (scripts/nemo/canary/test_180m_flash.py) has the same structure, so the C++ is faithful to the reference — neither guards the first generated position, which is never a legitimate place for end-of-transcript when the audio contains speech.Worth reporting upstream; this fix is a workaround at the only layer we control.
Note
The dumped-audio diagnostic that made this debuggable (writes the samples behind an empty decode to a WAV, opt-in via a hidden default) is deliberately not in this PR — one logical change per PR. Happy to send it separately if useful; it will pay for itself the next time this class of bug appears.