Add NVIDIA Nemotron-3.5-ASR streaming multilingual backend - #10
Conversation
Adds a new eddy::nemotron backend for NVIDIA's nemotron-3.5-asr-streaming-0.6b: a cache-aware streaming FastConformer-RNNT with prompt-conditioned multilingual decoding (40+ languages). This is distinct from the Parakeet path (stateless overlapping-chunk encoder + TDT duration decoding) and needs its own module: cache-aware streaming encoder loop, integer prompt_id language conditioning, plain RNNT greedy decode, and a dedicated tokenizer (Nemotron emits standalone U+2581 word-boundary tokens, so text is rebuilt by concatenating raw pieces then mapping U+2581 -> space). - include/eddy/models/nemotron/nemotron.hpp, src/models/nemotron/ nemotron_openvino.cpp: OpenVINO backend (preprocessor/encoder/ decoder/joint IR + vocab + metadata). - examples/cpp/nemotron_cli.cpp: CLI (--device, --lang, --model-dir). - model_configs.hpp: nemotron-streaming entry pointing at FluidInference/Nemotron-3.5-ASR-Streaming-Multilingual-0.6b-ov. Adds ModelConfig::repo_subdir so files download from the repo's fp16/ subfolder (default precision) while staying flat in cache. - ensure_models.cpp: honor repo_subdir in the download URL. Accuracy (FLEURS full test splits, FP32 IR, CPU; matches FP16): en_us WER 11.78, es_419 WER 6.99, fr_fr WER 12.92, cmn_hans_cn CER 21.05, ja_jp CER 15.12 (weighted 13.70) -- at or below the FluidAudio CoreML reference on every language. RTFx ~3.66x single-CPU. Closes #8
Code Review — Nemotron Streaming BackendThis PR adds a well-structured cache-aware streaming RNNT backend for NVIDIA's Nemotron-3.5 model. The mel-cache management and greedy RNNT loop are correctly implemented (state persists across chunks, LSTM is not advanced on blank, cache indices are in-bounds). A few issues worth addressing before merge: 🔴 High —
|
…dening - nemotron_openvino.cpp: assert cache_*_out byte sizes match the pre-allocated input caches before memcpy (mismatched IR export now trips here instead of silently over-/under-reading); resolve_prompt_id lazily calls the call_once-guarded ensure_compiled() so it is safe to call before transcribe()/warmup(); drop unused <cmath>, add <cctype> (std::tolower) + <cassert>; comment the blank_idx == vocab_size filter. - ensure_models.cpp: reject shell-unsafe characters in the curl URL and output path before std::system (repo_subdir widened the interpolation surface; today all fields are constants but ModelConfig is caller-supplied). - hf_fetch_models.cpp: list available models dynamically from MODEL_MAP instead of the stale "parakeet-v2" literal. Not changed: the audio_length finding. The CLI passes the full padded chunk_samples to match the WER-validated Python reference (transcribe_ov.py pads the final chunk then passes chunk.shape[1]); switching to the unpadded length would diverge from the validated path.
…dening - nemotron_openvino.cpp: assert cache_*_out byte sizes match the pre-allocated input caches before memcpy (mismatched IR export now trips here instead of silently over-/under-reading); resolve_prompt_id lazily calls the call_once-guarded ensure_compiled() so it is safe to call before transcribe()/warmup(); drop unused <cmath>, add <cctype> (std::tolower) + <cassert>; comment the blank_idx == vocab_size filter. - ensure_models.cpp: reject shell-unsafe characters in the curl URL and output path before std::system (repo_subdir widened the interpolation surface; today all fields are constants but ModelConfig is caller-supplied). - hf_fetch_models.cpp: list available models dynamically from MODEL_MAP instead of the stale "parakeet-v2" literal. Not changed: the audio_length finding. The CLI passes the full padded chunk_samples to match the WER-validated Python reference (transcribe_ov.py pads the final chunk then passes chunk.shape[1]); switching to the unpadded length would diverge from the validated path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the review — addressed in 2420230. Applied
Not applied (with reasoning)
Build is clean and a sanity transcription still matches FP16/FP32 word-for-word. |
Code Review — Add NVIDIA Nemotron-3.5-ASR streaming multilingual backendThis PR adds a well-structured, self-contained streaming ASR backend for Nemotron: cache-aware chunked encoder with prompt-conditioned language selection, plain RNNT greedy decode, and a dedicated tokenizer that handles U+2581 word-boundary tokens correctly. The accuracy numbers look solid and the architectural separation from Parakeet is well-justified. A few issues to address before merging, ordered by severity: 1.
|
INT8 weight-only encoder (per-channel symmetric; Conformer relative-pos projections kept FP16 to satisfy the OV CPU plugin) + FP16 decoder/joint/ preprocessor, served from the "int8" subfolder of the same HF repo. WER matches FP16/FP32 (en_us 10.99 vs 11.78); ~half the RAM of FP16 (2.1GB vs 3.9GB peak) and ~half the disk (749MB vs 1.3GB). No CPU speed gain (weights decompress to float on x86) — the win is memory footprint, chiefly for Intel NPU / memory-constrained deployments.
Code Review — Add NVIDIA Nemotron-3.5-ASR streaming multilingual backendThis PR adds a well-structured cache-aware streaming RNNT backend for Nemotron, correctly separating it from the stateless Parakeet path. The mel-cache continuity logic, greedy RNNT decode, and U+2581 detokenization are all sound. The Bugs
I.preproc_req.set_tensor("audio_length", make_i32(static_cast<int>(chunk_samples)));
I.preproc_req.set_tensor("audio_length", make_i32(static_cast<int>(end - off)));
assert(cc.get_byte_size() == cache_channel.get_byte_size());
assert(ctt.get_byte_size() == cache_time.get_byte_size());
std::memcpy(cache_channel.data<float>(), cc.data<float>(), cache_channel.get_byte_size());Standard if (cc.get_byte_size() != cache_channel.get_byte_size())
throw std::runtime_error("Cache channel shape mismatch: model IR does not match metadata.json");
} else if (a == "--device" && i + 1 < argc) {
device = argv[++i];
} else {
audio_file = a; // catches --device, --lang, --model-dir when they are the last argument
}If a named option appears without a value (e.g., } else if (a == "--device" || a == "--lang" || a == "--model-dir") {
std::cerr << "Error: " << a << " requires an argument\n";
return 1;
}Design concern
int OpenVINONemotron::resolve_prompt_id(const std::string& language) const {
const_cast<OpenVINONemotron*>(this)->ensure_compiled();
Efficiency (hot path)
// Before the while loop:
ov::Tensor token_len_tensor = make_i32(1);
ov::Tensor token_tensor(I.token_et, ov::Shape{1, 1});
// Inside the sym loop, replace make_i32(1) with token_len_tensor
// and write directly: token_tensor.data<int32_t>()[0] = last_token;Similarly, Minor usability gap
Reviewed with Claude Code |
INT8 weight-only variant validated ✅Added a Approach: weight-only INT8 of the encoder only (per-channel symmetric, data-free — mirrors the CoreML Accuracy — FLEURS full test splits, forced language, greedy (directly comparable to the FP32 table):
Essentially lossless (largest swing +0.15 WER, within noise; Chinese improves slightly). Footprint: encoder |
Wire the Nemotron streaming backend through the eddy_c C API (previously
CLI-only), mirroring the Parakeet C API:
- EddyNemotronModel handle + EddyNemotronConfig (device/model_dir/language)
- EddyNemotronResult (text, detected_language, token_ids, prompt_id_used,
latency_ms) with eddy_nemotron_free_result
- eddy_nemotron_create / _destroy / _infer_file / _infer_buffer
model_dir defaults to the "nemotron-streaming" Eddy cache; language is fixed
at creation (recreate to change). Verified end-to-end against the FP16 IR:
transcript matches the CLI/Python reference word-for-word.
Rename eddy::parakeet -> eddy::model_utils for the model-download helper
(ensure_models.{hpp,cpp}): it is model-agnostic (operates on any
eddy::ModelConfig) and already serves Nemotron downloads, so the
model-specific namespace was misleading. Updated all call sites
(parakeet_cli, benchmark_fleurs, hf_fetch_models, eddy_c). openvino_utils
stays under eddy::parakeet — it is coupled to the Parakeet ModelFile type
and unused by Nemotron. Addresses the follow-up noted in the PR #10 review.
Wire the Nemotron streaming backend through the eddy_c C API (previously
CLI-only), mirroring the Parakeet C API:
- EddyNemotronModel handle + EddyNemotronConfig (device/model_dir/language)
- EddyNemotronResult (text, detected_language, token_ids, prompt_id_used,
latency_ms) with eddy_nemotron_free_result
- eddy_nemotron_create / _destroy / _infer_file / _infer_buffer
model_dir defaults to the "nemotron-streaming" Eddy cache; language is fixed
at creation (recreate to change). Verified end-to-end against the FP16 IR:
transcript matches the CLI/Python reference word-for-word.
Rename eddy::parakeet -> eddy::model_utils for the model-download helper
(ensure_models.{hpp,cpp}): it is model-agnostic (operates on any
eddy::ModelConfig) and already serves Nemotron downloads, so the
model-specific namespace was misleading. Updated all call sites
(parakeet_cli, benchmark_fleurs, hf_fetch_models, eddy_c). openvino_utils
stays under eddy::parakeet — it is coupled to the Parakeet ModelFile type
and unused by Nemotron. Addresses the follow-up noted in the PR #10 review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-ups addressed (commit abe3a6d)1. Nemotron C-API wiring — Nemotron is now exposed through
2. Scoping note: All targets build clean ( |
Code Review — Add NVIDIA Nemotron-3.5-ASR streaming multilingual backendThis PR adds a well-structured cache-aware streaming RNNT backend for Nemotron. The architecture decision to give it its own module (rather than overloading the Parakeet TDT path) is sound, and the namespace rename from Correctness1. } catch (const std::exception& e) {
if (err) *err = capture_exception(e);
return EDDY_ERROR_FILE_NOT_FOUND; // ← wrong for format/permission errors
}
2. I.preproc_req.set_tensor("audio_length", make_i32(static_cast<int>(chunk_samples)));On the final chunk where 3. out->text = copy_string(res.text); // alloc 1
out->detected_language = copy_string(res.detected_language); // alloc 2
// ...
out->token_ids = new int[out->num_tokens]; // throws → leak allocs 1 & 2
API Consistency4. C API null device defaults to // eddy_c.cpp
const std::string device = config.device ? config.device : "CPU";
// nemotron.hpp
std::string device = "AUTO";A C caller who zero-initializes Performance5. for (size_t t = 0; t < t_enc; ++t) {
for (size_t sym = 0; sym < I.config.max_symbols_per_frame; ++sym) {
ov::Tensor token(I.token_et, ov::Shape{1, 1}); // ← shape never changesThis is the hottest allocation in the entire pipeline — up to Similarly, Design6. int OpenVINONemotron::resolve_prompt_id(const std::string& language) const {
const_cast<OpenVINONemotron*>(this)->ensure_compiled();The method is publicly declared Minor
🤖 Generated with Claude Code |
…C-API hardening Encoder/decoder (nemotron_openvino.cpp): - Replace assert() cache-size checks with runtime `throw` (Release builds define NDEBUG, so the asserts were compiled out); add the same guard for the decoder h_out/c_out LSTM state copies. - Hoist the per-chunk (audio, mel_in) and inner-loop (token, token_length, mel_length, audio_length) tensors above the loops — they have fixed shape/value for the whole call; the inner token/token_length allocs were the hottest path. Drop now-unused <cassert>. - Make ensure_compiled() const so resolve_prompt_id() (const) can lazily compile without the const_cast UB; document why it's safe (only mutates *impl_ through the unique_ptr). - Keep audio_length = full padded chunk_samples (matches the WER-validated transcribe_ov.py), now with a comment explaining why end-off would diverge. C API (eddy_c.cpp / eddy_c.h): - eddy_nemotron_infer_file: only a genuinely missing file returns FILE_NOT_FOUND; read_wav format/decode errors now return INFERENCE_FAILED. - eddy_nemotron_infer_buffer: zero-init *out and free on catch so a throw mid-fill can't leak the already-allocated strings. - Hoist the doubly-constructed model_dir string; annotate token_ids ownership. CLI (nemotron_cli.cpp): - Add --model <name> to pick the cache variant (e.g. nemotron-streaming-int8) without --model-dir wrangling. - Error (not silently swallow as the audio path) when a known flag is given without a value, and reject unknown -options. Align nemotron::Config default device to "CPU" to match the C API and CLI. Builds clean; FP16/INT8 transcripts match the reference word-for-word.
abe3a6d to
df37fa3
Compare
|
Addressed the latest review rounds (force-pushed; final fix commit Bugs
Design
Performance
Usability
Minor — hoisted the doubly-constructed Not changed (intentional): All targets build clean. FP16 and INT8 transcripts still match the reference word-for-word; Parakeet re-verified end-to-end after the namespace rename. |
Code Review — Add NVIDIA Nemotron-3.5-ASR streaming multilingual backendThis PR adds a well-structured new backend for the Nemotron cache-aware streaming RNNT model: clean separation into its own Bug —
|
…cache hardening - eddy_nemotron_infer_file: zero-init *out before FILE_NOT_FOUND return so a caller-then-free_result does not delete[] uninitialized pointers. - eddy_nemotron_create: fail fast via check_models_available(NEMOTRON_FILES), matching the eddy_parakeet_create contract instead of deferring to first infer. - nemotron metadata: require cache_channel_shape/cache_time_shape explicitly with a path-aware error rather than letting nlohmann's bare key-not-found propagate. - mel_cache: resize instead of assign (every element is overwritten; drop the redundant per-chunk zero-fill). - chunk_samples(): integer frames*rate/100, avoiding the 0.01 FP rounding hazard. - ensure_models: split is_shell_safe into tight is_url_safe / is_path_safe; URL charset drops '~', path charset adds native '\' and space (fixes Windows paths). - CLAUDE.md: document both Parakeet/batch and Nemotron/streaming as supported. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review — Add NVIDIA Nemotron-3.5-ASR streaming multilingual backendThis is a well-structured PR: the Nemotron backend is cleanly separated from Parakeet (justified by the fundamental difference in streaming vs. stateless chunking), the C API mirrors the Parakeet pattern, and the 1. BUG —
|
Preprocessor: replace the dynamic-shape nemotron_preprocessor.xml with a native C++ log-mel featurizer (MelFeaturizer) reproducing NeMo's AudioToMelSpectrogramPreprocessor exactly (preemph 0.97 -> center-pad -> Hann-windowed 512-pt STFT via a self-contained radix-2 FFT -> power -> slaney mel -> log(x+2^-24)). Reverse-engineered from the IR (validated to ~1e-5 on window/filterbank constants and <=0.14% WER end-to-end over 1050 FLEURS files). A startup guard verifies the featurizer's frame geometry matches the model's chunk_mel_frames. nemotron_preprocessor.xml/.bin dropped from NEMOTRON_FILES. NPU: the OpenVINO NPU plugin miscompiles BitwiseNot on a boolean (it does an integer complement, so ~0=-1 and ~1=-2 are both 'true'), making the FastConformer attention mask all-true -> uniform softmax -> encoder output collapses to ~0 -> empty transcripts. Rewrite BitwiseNot(bool) -> LogicalNot in the encoder IR before compiling (no-op on CPU/GPU). Nemotron now runs correctly on NPU at WER/CER parity with CPU and ~2x the throughput (RTFx ~22x vs ~11x). Adds benchmark_nemotron_fleurs (FLEURS WER/CER/RTFx harness, UTF-8/CJK-aware CER) used to validate the above across en/es/fr/zh/ja on CPU and NPU. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review — PR #10: Nemotron-3.5-ASR streaming multilingual backendThis PR adds a cache-aware streaming RNNT backend ( Three issues worth addressing before merge: 1.
|
… + Unicode punct strip) Match FluidAudio's NemotronMultilingualFleursBenchmark / WERCalculator: route CJK languages (ja/ko/zh/cmn/yue/th/lo) through character-level CER (whitespace WER is meaningless without word segmentation; report WER=CER), and strip Unicode Mark/Symbol/Punctuation (approx category M/S/P over the blocks that occur in FLEURS) to a space in normalization instead of keeping non-ASCII punctuation. Brings CJK CER onto the published reference: ja 19.0 -> 15.46 (ref 15.15), zh 21.78 -> 20.18 (ref 20.94). en/es/fr unaffected (ASCII) or marginally improved (European punctuation now stripped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…EURS) 5-language FLEURS results (en/es/fr WER, zh/ja CER) for nemotron-streaming-int8 on Intel NPU at ~22-24x RTFx, scored with the FluidAudio methodology. Matches the OpenVINO FP32 reference and beats the FluidAudio CoreML reference on es/fr/zh/ja. Documents the NPU BitwiseNot->LogicalNot enablement fix and the native C++ mel preprocessor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review — Add NVIDIA Nemotron-3.5-ASR Streaming Multilingual BackendThis PR adds a well-structured, cache-aware streaming ASR backend for Nemotron. The NPU BitwiseNot→LogicalNot rewrite, the native C++ featurizer with geometry validation at init time, and the meticulous cache copy-back checks (encoder caches, LSTM state) all show careful attention to correctness. The accuracy numbers are compelling. Three confirmed bugs and two plausible issues need attention before merge. 🔴 Confirmed bugs1. When the file exists but is malformed (stereo, unsupported format, wrong sample rate in the WAV header),
// src/eddy_c.cpp — fix: add before the try block
try {
*out = EddyNemotronResult{}; // ← add this
auto pcm = eddy::audio::read_wav(wav_path);
return eddy_nemotron_infer_buffer(handle, pcm.data(), pcm.size(), 16000, out, err);
} catch (const std::exception& e) {
eddy_nemotron_free_result(out); // ← and add this (matches infer_buffer pattern)
if (err) *err = capture_exception(e);
return EDDY_ERROR_INFERENCE_FAILED;
}2. The zero-init ( // Current order:
if (sample_rate != 16000) { return EDDY_ERROR_INVALID_ARGUMENT; } // *out untouched
*out = EddyNemotronResult{}; // too late
// Fix: zero-init immediately after the null-pointer guard, before any other early return:
if (!handle || !pcm || !out) { ... return EDDY_ERROR_INVALID_ARGUMENT; }
*out = EddyNemotronResult{}; // ← move here
if (sample_rate != 16000) { ... return EDDY_ERROR_INVALID_ARGUMENT; }3. The header contract (
Neither The same gap exists in the pre-existing Parakeet and Whisper functions, so this is systemic, but the new Nemotron functions perpetuate it rather than fixing it. The minimal fix is to add 🟡 Plausible issues4.
The encoder cache shapes are explicitly byte-size-validated at runtime (lines 422–427); a similar one-liner guard for the joint output could be added in // After compiling the joint model:
auto joint_out_shape = impl_->joint.output(0).get_shape();
size_t joint_vsz = /* product of joint_out_shape */;
if (joint_vsz <= static_cast<size_t>(impl_->blank_idx)) {
throw std::runtime_error("Nemotron joint IR output size " + std::to_string(joint_vsz) +
" does not cover blank_idx " + std::to_string(impl_->blank_idx));
}5. const float* lg = logits.data<float>();
const size_t vsz = logits.get_size();
int best = 0;
float best_score = lg[0]; // ← OOB if vsz == 0A degenerate joint IR with a zero-size output axis would cause an out-of-bounds read here. This is low-probability (OpenVINO would likely refuse to compile such an IR) but easy to guard: if (vsz == 0) throw std::runtime_error("Nemotron joint IR returned empty logits tensor");This would also catch the off-by-one scenario from finding #4 more aggressively if the IR somehow produces fewer logits than expected. Minor notes
🤖 Generated with Claude Code |
Code Review — Add NVIDIA Nemotron-3.5-ASR Streaming Multilingual BackendThis PR adds a cache-aware streaming FastConformer-RNNT backend with a clean pImpl design, a native C++ log-mel featurizer (avoiding the dynamic-shape OV preprocessor that NPU can't compile), and a C API that mirrors the existing Parakeet one. The NPU-specific 1.
|
…ants
Auto-detect prompt conditioning from the encoder's input ports: the
multilingual encoder has a prompt_id input, the English nemotron-speech-
streaming encoder does not. When absent, skip prompt_id resolution and don't
feed the tensor. One eddy::nemotron backend now serves both models; everything
else (C++ mel featurizer, NPU BitwiseNot fix, cache-aware decode loop) is shared.
Adds nemotron-speech-streaming{,-int8} model configs (English, FluidInference/
nemotron-speech-streaming-en-0.6b-ov). Multilingual path regression-checked
(en_us NPU unchanged at WER 12.2 on the smoke set).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review — Add NVIDIA Nemotron-3.5-ASR streaming multilingual backendThis PR adds a well-structured 🔴 Bug — File:
Fix: add 🟡 Correctness risk — File: The loop replaces every Fix: guard the replacement with 🟡 Latent bug — File: n_fft_(512),
...
n_freq_(512 / 2 + 1), // should be n_fft_ / 2 + 1The header documents 🟡 C API design — File:
Consider adding a 🟢 Minor — JSON output: File:
🟢 Minor — benchmark silently falls back to an empty model path on Linux/macOS File: const char* lad = std::getenv("LOCALAPPDATA");
if (lad) model_dir = ...;On Linux and macOS Summary: The core RNNT streaming loop, featurizer math, NPU workaround, and C API structure are solid. The |
Point nemotron-speech-streaming{,-int8} at the existing
FluidInference/Nemotron-3.5-ASR-Streaming-Multilingual-0.6b-ov repo under
en/fp16 and en/int8 instead of a separate HF space.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review — PR #10: Nemotron Streaming BackendThis PR adds the 🔴 Bug —
|
nemotron_cli --model help lists nemotron-streaming[-int8] (multilingual) and nemotron-speech-streaming[-int8] (English); banner/header no longer hardcode '3.5 Multilingual'. Backend file header documents that one path serves both variants (prompt auto-detected). Comment-only/help-text changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review — PR #10: Add NVIDIA Nemotron-3.5-ASR streaming multilingual backendGreat addition overall. The cache-aware streaming RNNT pipeline is well-structured, the NPU Bug 1 —
|
… guard - eddy_c: zero-init *out on ALL early/error returns (sample_rate mismatch, malformed-WAV INFERENCE_FAILED), not just FILE_NOT_FOUND, so callers that free after any error don't delete[] uninitialized pointers. - ensure_models: delete partial files on download failure (a truncated file would otherwise pass file_nonempty and be skipped, loading a corrupt model); reject '..' path components (charset allows '.'/'/'). - nemotron encoder: guard the BitwiseNot->LogicalNot rewrite on boolean input type (only valid for bool); verify cache_len_out is i32 for symmetry with the cache_*_out byte-size checks. - featurizer: ptrdiff_t frame indices (long is 32-bit on Win64); clarify the log-guard constant (6e-8, the IR's stored value) and the length-mask vs OV. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review — Add NVIDIA Nemotron-3.5-ASR streaming multilingual backendThis is a substantial, well-structured addition: a separate Six issues worth addressing before merge: 1.
|
Closes #8.
Adds a new
eddy::nemotronbackend for NVIDIA'snemotron-3.5-asr-streaming-0.6b— a cache-aware streaming FastConformer-RNNT with prompt-conditioned multilingual decoding (40+ languages).Why a separate module
The existing Parakeet path is a stateless overlapping-chunk encoder with TDT duration-bin decoding. Nemotron is fundamentally different — cache-aware streaming, plain RNNT, and an integer
prompt_idfor per-chunk language conditioning — so it gets its own backend rather than overloading Parakeet:cache_channel/cache_time/cache_lenacross chunks,att_context=[56,0]).prompt_idlanguage conditioning via the encoder'sprompt_kernel(seeprompt_dictionaryinmetadata.json;auto=101).Tokenizer::decodedrops spaces).Changes
include/eddy/models/nemotron/nemotron.hpp,src/models/nemotron/nemotron_openvino.cpp— OpenVINO backend (preprocessor / encoder / decoder / joint IR + vocab + metadata).examples/cpp/nemotron_cli.cpp— CLI (--device,--lang,--model-dir).include/eddy/eddy_c.h,src/eddy_c.cpp— C API for Nemotron (EddyNemotronModel,eddy_nemotron_create/_destroy/_infer_file/_infer_buffer/_free_result), mirroring the Parakeet C API for language bindings.model_configs.hpp—nemotron-streaming(→fp16/) andnemotron-streaming-int8(→int8/) entries onFluidInference/Nemotron-3.5-ASR-Streaming-Multilingual-0.6b-ov. AddsModelConfig::repo_subdirso files download from a repo subfolder while staying flat in the local cache.ensure_models.cpp— honorrepo_subdirin the download URL (Parakeet unaffected; field defaults to empty). Namespace renamededdy::parakeet→eddy::model_utils(it's a model-agnostic download helper now used by Nemotron too); call sites updated.Accuracy — FLEURS, full test splits, CPU, greedy, forced language
FP32 IR vs the FluidAudio CoreML reference (FP16 transcripts identical to FP32):
At or below the reference on every language — no conversion regression. Audio-weighted RTFx ≈ 3.66× on a single CPU (Intel Xeon E5-2699 v4, 4 vCPU); higher on Intel NPU / iGPU.
INT8 weight-only variant (
nemotron-streaming-int8)Weight-only INT8 compression of the encoder (per-channel symmetric, data-free — mirrors the CoreML
linear_quantize_weightsbuild). The 24 Conformer relative-position projections (self_attn.linear_pos) are kept FP16 (int8-compressing them trips an OpenVINO CPU compile bug and they hold little weight mass); decoder/joint/preprocessor stay FP16. Measured on the same FLEURS splits — directly comparable to the FP32 table:Essentially lossless. Footprint: encoder
.bin732 MB (vs 1.25 GB FP16 / 2.5 GB FP32), peak RSS 2.1 GB (vs 3.9 GB FP16). It's a memory/disk win, not a CPU speed win — on x86 int8 weights decompress to float per-op (RTFx 2.96× vs 3.66×); native int8 speedup applies on Intel NPU. Useint8/for memory-constrained / NPU deployments,fp16/for fastest CPU inference.Verification
eddy,eddy_c, both CLIs,benchmark_fleurs,hf_fetch_models).libeddy_c.soagainst the FP16 IR — transcript matches the CLI/Python reference word-for-word (detected_lang=<en-US>,prompt_id=101).hf_fetch_models(renameddownload_models) and transcribed viaparakeet_cli(renamedcheck_models_available) — correct output, RTFx 4.5×.Notes
fp32/,fp16/,int8/subfolders); default download is FP16.openvino_utilsintentionally stays undereddy::parakeet— it's coupled to the ParakeetModelFiletype and unused by Nemotron.🤖 Generated with Claude Code