support file-based ASR for nemotron streaming model - #885
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds file-based WAV transcription for Nemotron streaming ASR models in the C++ SDK.
Changes:
- Routes Nemotron file requests through the streaming processor.
- Parses 16 kHz PCM16/float32 WAV files with channel downmixing.
- Adds language mapping, temperature handling, and OpenAI JSON output.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
audio_session.h |
Declares Nemotron transcription and WAV parsing helpers. |
audio_session.cc |
Implements Nemotron inference, decoding, language mapping, and WAV parsing. |
| int completion_tokens = 0; | ||
|
|
||
| auto decode_all_tokens = [&]() { | ||
| while (!generator->IsDone() && !original_request.canceled) { |
| size_t count = std::min(kNemotronSamplesPerChunk, samples.size() - offset); | ||
| run_one_pass(processor->Process(samples.data() + offset, count)); | ||
| } | ||
| run_one_pass(processor->Flush()); |
| } | ||
| } | ||
|
|
||
| auto samples = LoadPcmWavAsFloatSamples(req.filename); |
There was a problem hiding this comment.
This is a valid issue. We need unit tests to validate these changes such as:
- Happy path (
nemotron-3.5-asr-streaming-0.6bwith a valid 16kHz WAV) - Any regression fixes in this change
- Parser validation cases like malformed headers and unsupported sample rates
- PCM16 and float32 input handling
|
Why? Parakeet can execute faster and more accurate file-based transcription. I wouldn't want customers to rely on streaming models for non-streaming scenarios, and we're encouraging them with this. |
This is not a way to encourage them to do github/copilot-cli#4024 (comment) they are using streaming model, however they do file-based transcription and get error raised. IMO, we better to cover this edge case for them, instead of switch models? What do you think? @sylvanc |
Hmm, I haven't realized they're using file-based transcription. Ok, but there are better models for this going forward. |
|
@skottmckay can you please review this PR ? Thanks. |
|
|
||
| // If the model is a Nemotron speech model, process the transcription using the Nemotron-specific method. | ||
| // e.g. RNNT models require Nemotron-specific transcription handling. | ||
| if (IsNemotronSpeechModel()) { |
There was a problem hiding this comment.
I presume that the v2 SDK already supported Nemotron when it was merged into main. Why do we now need an early return to support it?
| FOUNDRY_LOCAL_TEXT_ITEM_TYPE_OPENAI_JSON)); | ||
| } | ||
|
|
||
| std::vector<float> AudioSession::LoadPcmWavAsFloatSamples(const std::string& audio_file_path) { |
There was a problem hiding this comment.
From Copilot:
Good addition overall — the Nemotron transcription path is clean, the WAV parser is thorough, and keeping a single generator across chunks is the right fix for truncated output. A few things worth addressing before merging:
🔴 Blocking issues
-
WAV chunk bounds check is subtly wrong (
LoadPcmWavAsFloatSamples, chunk-walking loop)
The bounds guardchunk_start + chunk_size > file_size - (chunk_size % 2 == 1 ? 1 : 0)mixes the padding-byte adjustment into the main bounds check, which can undercount available space and let a crafted file slip through. The padding byte lives after the chunk data, so it shouldn't reduce the window used to validate the chunk itself. Suggested fix:// Before: chunk_start + chunk_size > file_size - (chunk_size % 2 == 1 ? 1 : 0) // After: if (chunk_start < 0 || chunk_start + static_cast<std::streamoff>(chunk_size) > file_size) { FL_THROW(FOUNDRY_LOCAL_ERROR_INVALID_USAGE, "Corrupted WAV chunk."); }
The odd-byte padding seek that follows handles alignment correctly as-is.
-
Format validation happens after full data load (
LoadPcmWavAsFloatSamples,datachunk branch)
data.resize(chunk_size)allocates the full audio buffer before sample rate and format have been validated. For a large or malformed file, this could allocate hundreds of MB only to throw immediately afterward. Consider moving thesample_rate,audio_format, andbits_per_samplechecks to right after thefmtchunk is parsed, before thedatachunk is even encountered.
🟡 Suggestions
-
Gap at language ID 5 in
NemotronLanguageIdMap— IDs jump from4(zh-cn) to6(hi). If this is intentional (reserved/unsupported language), a brief comment would prevent future contributors from assuming it's a typo. -
prompt_tokenshardcoded to0inProcessNemotronFileTranscription— the existing path computes this from the processor. If it's not meaningful for Nemotron, a// TODOexplaining why would help maintain parity expectations with the OpenAI usage spec. -
decode_all_tokenslambda and thread-safety — the lambda capturesgeneratorby reference and is safe as used today (same-thread, same scope). A short comment noting it must not be moved off the creation thread would protect against future refactors.
|
@kunal-vaishnavi can you please review again? We are hoping to add this to next release. Thank you so much . |
I reviewed the PR again. Can you add unit tests to validate your changes and ensure there's no breaking change? |
|
@kunal-vaishnavi can you please review it again? Thanks |
| EXPECT_THROW(session.ProcessRequest(request, response), nlohmann::json::parse_error); | ||
| } | ||
|
|
||
| TEST_F(AudioSessionTest, NemotronOpenAIJsonWithInvalidRiffThrows) { |
There was a problem hiding this comment.
From Copilot:
Could we add a regression test that uses a multi-chunk WAV and verifies we get the full transcript back? The main bug this PR is fixing is truncation during file-based Nemotron transcription, so having an end-to-end case that proves the full output survives chunked decoding would give us much stronger confidence here.
| @@ -431,6 +515,145 @@ TEST_F(AudioSessionTest, OpenAIJsonWithInvalidJsonThrows) { | |||
| EXPECT_THROW(session.ProcessRequest(request, response), nlohmann::json::parse_error); | |||
| } | |||
|
|
|||
There was a problem hiding this comment.
From Copilot:
Could we add an explicit unit test for the float32 WAV path in LoadPcmWavAsFloatSamples? The parser has separate decoding logic for PCM16 and float32, so having coverage for float32 would help make sure that branch keeps working as expected.
| DecodeNemotronTokens(generator, tokenizer_stream, text, streaming_callback, response_id, original_request, | ||
| completion_tokens); | ||
| } | ||
|
|
There was a problem hiding this comment.
From Copilot:
Could we consider moving decode_all_tokens into a named private helper instead of keeping it as a local lambda? This logic is already central to the new Nemotron path, and giving it a dedicated method would make the control flow a bit easier to follow, test, and extend if we need to change the decode behavior later.
|
|
||
| const std::unordered_map<std::string, std::string>& NemotronLanguageIdMap() { | ||
| // Language id 5 is intentionally missing because upstream Nemotron lang_id assignments skip it. | ||
| static const std::unordered_map<std::string, std::string> kMap = { |
There was a problem hiding this comment.
What is the language id for 5? I see it goes from 4 to 6 directly.
There was a problem hiding this comment.
@kunal-vaishnavi That is removed by Nvidia, you can check here.
https://microsoft.visualstudio.com/windows.ai.toolkit/_git/neutron-server/pullrequest/15742356
Add file-based Nemotron ASR transcription support in C++ SDK v2
Summary
This PR completes file-based audio transcription support for Nemotron streaming ASR models in sdk_v2/cpp by finishing the C++ AudioSession implementation and aligning behavior with the existing OpenAI-JSON transcription flow.
For more details, please refer to github/copilot-cli#4024 (comment)
What changed
• Implemented/finished Nemotron file transcription path in:
• Added WAV file parsing for file-based transcription input:
• RIFF/WAVE validation
• fmt / data chunk handling with bounds checks
• 16kHz enforcement
• PCM16 and float32 decoding
• Added Nemotron-specific language runtime option mapping ( lang_id ) and temperature handling.
• Fixed decode flow to keep a single generator across incremental audio processing (100ms chunks + flush), which resolves truncated transcription output for file-based runs.
Behavior
• OpenAI-JSON file transcription for Nemotron models now returns full transcript text from WAV input in SDK v2.
• Existing streaming and non-Nemotron paths remain unchanged.
End-to-end scenario covered
• Model alias: nemotron-3.5-asr-streaming-0.6b
• Input: sample-speech-1m-16k.wav
• Path exercised: JS SDK v2 AudioClient.transcribe(...) → C++ SDK v2 audio transcription pipeline