Repository navigation
Description
Target repo : https://github.com/0xShug0/audio.cpp
Base commit : cc21c9e
Related : PR #1137 (nemotron_transcribe family), docs/models/nemotron_transcribe.md,
docs/models/nemotron_transcribe_validation.md
Model : NVIDIA Nemotron 3.5 Transcribe 30B-A4B, IQ4_NL expert profile
Hardware : NVIDIA GB10 (Blackwell, compute capability 12.1), CUDA 13.0, aarch64
Built with : upstream origin/main, sm_121a-real, audiocpp_cli / audiocpp_server
Summary
The nemotron_transcribe family reproduces its documented English/Japanese
validation outputs, but four Mandarin Chinese cases behave incorrectly:
the 15 s diarization path returns pure English under auto, the 120 s plain
path returns mixed English/Chinese under auto and pure English under
zh-CN, and the diarization path consistently drops punctuation even when
the language is correct. The upstream
validation doc covers English and Japanese only, so Chinese is currently
outside the verified set — this report documents the observed behaviour and
asks whether it is a model limitation, a port bug, or an expected consequence
of the default instruction templates.
Most of these issues share a single root cause and have a working
workaround. The language request option defaults to auto, and the
decoder is then left without a language anchor. On Mandarin input the
result is inconsistent: some recordings come out as an English translation,
some mix languages depending on clip length, and all diarization outputs lose
punctuation. Passing an explicit source language makes the output stable and
correct in every case tested. See "Root cause" below — it may be worth
reading that section first, since it reframes the rest of the report.
The issues are reproducible on a single CUDA session and are reproduced
below via audiocpp_cli and independently via the HTTP server.
Root cause: the diarization prompt has no language anchor
In src/community_models/nemotron_transcribe/session.cpp, the source
language defaults to auto:
const auto language = runtime::find_option(request.options, {"language"}).value_or(
request.text_input && !request.text_input->language.empty() ? request.text_input->language : "auto");and the language hint is only appended when the language is explicit:
instruction = diarization ? kSpeakerPrompt : kTranscriptionPrompt;
if (!language.empty() && language != "auto") instruction += " The speech is in " + language_name(language) + ".";The important detail is how the two built-in prompts differ. The plain
transcription prompt ends with an instruction to keep the source language:
constexpr const char * kTranscriptionPrompt =
"Produce a verbatim transcript of the audio. Preserve named entities, abbreviations, numbers, dates, "
"measurements, acronyms, and technical terms as clearly as possible. Keep the same language as spoken and do not translate.";The speaker prompt has no equivalent clause:
constexpr const char * kSpeakerPrompt =
"Transcribe this audio. Include all spoken words and verbal fillers. Write numbers as spoken. "
"Use no punctuation and no capitalization. Start every speaker turn with a speaker tag such as "
"<spk:0>, <spk:1>, <spk:2>, etc.; assign one stable tag per speaker and keep speaker identities "
"consistent. Output only the speaker-tagged transcript.";So kTranscriptionPrompt is self-anchoring — it tells the model to keep the
language as spoken — while kSpeakerPrompt says only "Transcribe this
audio", leaving the target language unspecified. With the default auto
there is no hint appended, and the speaker prompt therefore reaches the
decoder with no language anchor at all.
The consequence is not uniform, which is what makes this awkward to
diagnose. The plain template is generally unaffected by the missing anchor:
on the 15 s and 20 s fixtures it stays Chinese under auto. But on the
120 s fixture the plain output becomes mixed English/Chinese under auto
and pure English under zh-CN — the first part of the audio is translated
while later segments remain in Mandarin (for auto), or the entire output
is translated to English (for zh-CN). This suggests that longer context can
overwhelm the prompt's language anchor, or that the model's internal context
window causes the instruction to be partially forgotten. The diarization path
degrades on the 15 s fixture (pure English), and even stays Chinese on the
20 s fixture under auto. The degraded cases are stable across repeated runs
— they are a property of the input, the prompt, and the clip length, not a
flaky decode.
| fixture | auto |
language=zh-CN |
|---|---|---|
| 15 s, diarization | <spk:0> huh now i i put my voice you know you go you go |
<spk:0> 好现在我录我的声音给你你拿去当你拿去克隆… |
| 20 s, diarization | <spk:0> 了解瓶子相当于了解瓶子是瓶子对不对了解瓶子那它是瓶子吗**凯文我这个**没这种**是**是吗对不对我了解瓶子**哦**我了解桌子这样对那这张是不是桌子就不知道了 |
<spk:0> 了解瓶子相当于了解瓶子是瓶子对不对了解瓶子那它是瓶子吗**田阿伯说的**没这种**事**是吗对不对我了解瓶子**啊**我了解桌子这样对那这张是不是桌子就不知道了 |
The behaviour matrix
audiocpp_cli over both Mandarin fixtures, plain and diarization paths,
with and without an explicit source language, plus a caller-supplied
speaker prompt. Word counts are from the CTC --words-out alignment;
punct counts Chinese punctuation marks in the text.
| case | words | punct | output language |
|---|---|---|---|
15 s, plain, auto |
55 | 3 | Chinese |
15 s, plain, zh-CN |
55 | 7 | Chinese |
15 s, diarization, auto |
13 | 0 | English |
15 s, diarization, zh-CN |
55 | 0 | Chinese |
15 s, diarization, zh-CN + custom speaker prompt |
55 | 7 | Chinese |
20 s, plain, auto |
67 | 13 | Chinese |
20 s, plain, zh-CN |
67 | 12 | Chinese |
20 s, diarization, auto |
68 | 0 | Chinese |
20 s, diarization, zh-CN |
68 | 0 | Chinese |
20 s, diarization, zh-CN + custom speaker prompt |
68 | 8 | Chinese |
120 s, plain, auto |
? | 21 | English + Chinese (mixed: 316 Han, 604 Latin) |
120 s, plain, zh-CN |
? | 0 | Pure English (failure: 0 Han, 1518 Latin) |
Three things fall out of this table.
The plain path is not universally protected by auto. On the 15 s and
20 s fixtures it stays Chinese, but on the 120 s fixture it returns a
mixed English/Chinese transcript under auto — the first part of the audio is
translated while later segments remain in Mandarin. The single case that
returns pure English is 15 s diarization under auto. The 20 s
diarization case is Mandarin even under auto, so the same prompt and
the same option produce different languages on different clips.
Critical finding: On the 120 s fixture, language=zh-CN also fails — it
returns pure English (0 Han characters). This means explicit language
setting does NOT guarantee correct language on long clips; clip length can
overwhelm the model's language anchor in different ways (auto produces
mixed output, zh-CN produces pure English).
Punctuation never appears on the diarization path under the built-in prompt,
because that prompt asks for none. It appears on the plain path, and it
appears on the diarization path once the caller supplies their own speaker
prompt.
Punctuation counts are not stable across runs. The same command and the same
audio produced 9 and 13 punctuation marks on the 20 s plain fixture in two
consecutive passes. The transcript wording changes with them — 「天,我吃个没这种是是吗」 versus 「天博吃个没这种事是吗」 — so punctuation density is not a reliable signal on its own.
Reproduce with prompt-matrix.sh in the attached package; the recorded
output is prompt-matrix.log.
The diarization prompt suppresses punctuation by instruction
One behaviour does not follow the root cause above, and it is worth
separating out because it is easy to read the fix as more complete than it is.
With language=zh-CN the default template produces properly punctuated
Mandarin, but the diarization path produces the same sentence with no
punctuation at all:
default, language=zh-CN:
好,现在我录我的声音给你,你拿去当你拿去克隆,然后克隆好之后,你随便用一段文字去转语音,转出来的语音传给我,然后让我听听看。
(7 punctuation marks)
diarization, language=zh-CN:
<spk:0> 好现在我录我的声音给你你拿去当你拿去克隆然后克隆好之后你随便用一段文字去转语音转出来的语音传给我然后让我听听看
(0 punctuation marks)
The same holds on the 20 s fixture (12 punctuation marks under default
with language=zh-CN, 0 under diarization). So the explicit language fixes
which language is decoded, but the speaker prompt does not recover
punctuation. This is
consistent with the two paths using separate built-in prompts rather
than one prompt with a speaker prefix, and it means the language option
alone does not make the diarization output as usable as the default output.
(The cases above are mandarin15_plain_zhCN vs mandarin15_diarize_zhCN
and mandarin20_plain_zhCN vs mandarin20_diarize_zhCN in the attached
bench.log.)
The 20 s row is worth a note. The two outputs differ in three short
segments — the model heard something unclear there and rendered it
「凯文我这个」 under auto and 「田阿伯说的」 under zh-CN, plus a
one-character difference (是/事) and a particle change (哦/啊). The plain
transcription of the same clip without diarization gives yet other readings
of that segment — 「天,我吃个没这种是是吗」 under auto and
「天博吃个没这种事是吗」 under zh-CN. These are four readings of the same
unclear audio, so this row reflects ordinary ASR ambiguity in one
low-confidence segment rather than a language effect. The
15 s row is different in kind: there the whole sentence comes back in
English.
So the failure is not a clean "works / does not work" split — it is a
continuum, and the explicit-language setting is what makes the result
predictable.
The clearest illustration is the 120 s fixture, where the transcript
switches language partway through the same recording. The Mandarin content
("bottle", "table") comes out as English for the first two thirds and then
as Chinese for the rest, under auto:
Do you understand bottles? It is equivalent to understanding bottles, bottles, right? ...
So用这个逻辑去讲的话,了解空心之前是不能了解四处地的。对,所以有一些人是走向这个。...
The same passage is being rendered twice — once through an English decoder
and once through a Chinese one — which is what a missing language anchor
looks like from the outside. This also explains why the shorter fixtures
appear to work under auto: the decoder only has to drift if the audio is
long enough for it to settle on the wrong language.
Passing an explicit source language removes the degradation in both cases.
Same audio, same binary, only --request-option language=… differs:
--request-option language=auto
好现在我录我的声音给你,你拿去当你拿去克隆,然后克隆好之后你随便用一段文字去转语音转出来的语音传给我然后让我听听看。
--request-option language=zh-CN
好,现在我录我的声音给你,你拿去当你拿去克隆,然后克隆好之后,你随便用一段文字去转语音,转出来的语音传给我,然后让我听听看。
Here the explicit-language run additionally restores punctuation and correct
word segmentation. On the clip that already transcribed correctly under
auto, the two runs are equivalent apart from minor word-boundary
differences.
The same substitution repairs the diarization case, which otherwise returns
an English translation:
--request-option diarization=true
<spk:0> huh now i i put my voice you know you go you go
--request-option diarization=true --request-option language=zh-CN
<spk:0> 好现在我录我的声音给你你拿去当你拿去克隆然后克隆好之后你随便用一段文字去转语音转出来的语音传给我然后让我听听看
Two notes on the workaround:
- The
languageoption is only consulted in thedefaulttemplate branch
(theelseat line 175). Thetranslation,audio_qaandlanguage_id
branches build their instruction and never consult it, so those three
templates need the language stated ininstructinstead. - The value is free-form, not a BCP-47 code —
zh-CN,zh,Chineseand
Mandarinall produced the same corrected output.
Separately, and unrelated to Chinese: requesting word timestamps together
with any non-default template is rejected outright.
$ audiocpp_cli ... --request-option template_name=translation \
--request-option return_timestamps=true
audiocpp_cli failed: CTC timestamps and speaker labels require a source-language transcript
(The reject_ts_translation and reject_ts_translation_zhCN cases in the
attached bench.log.)
The guard is at line 160 and the condition is word_times && mode != "default"
— it is about the template, not about the language. The message names the
source language, so it reads as if naming the language would satisfy the
check, but no value of language does; template_name=translation with
language=zh-CN and return_timestamps=true is rejected the same way. I
mention it because the error message is misleading, not because it blocks
any of the issues above.
Two supporting observations:
model_specs/nemotron_transcribe.jsondeclares"languages": ["auto"],
so the runtime logsruntime.model.language_count 1and the port carries no
per-language information. The sibling specmodel_specs/nemotron_asr.json
lists 40 languages includingzh-CN, andmodel_specs/nemotron_3_diar.json
lists["multilingual"]. It looks like the language list was not carried
over when the family was split out, soaudiocpp_model_language_count()
cannot answer "does this model support Chinese?".- The model itself is described as covering 40 languages, so the limitation
is in the port's metadata and prompt construction rather than in the
weights.
Setup
audiocpp_cli --task asr --family nemotron_transcribe \
--model /models/nemotron-transcribe-30b-a4b-experts-IQ4_NL.gguf \
--backend cuda --audio <wav> --text-out /tmp/out.txt --metrics \
--request-option template_name=<template> [other options]Three Mandarin fixtures are used, all 16 kHz mono:
mandarin-20s.wav— 20 s Mandarin, speaker discussing bottles and tablesmandarin-15s.wav— 15 s Mandarin, speaker recording their voice for voice cloninglong-120s.wav— 120 s Mandarin, same conversation as the 20 s fixture but
extended with additional segments
Plain transcription of the 15 s and 20 s fixtures is correct and fluent under
language=zh-CN, so the encoder, decoder and tokenizer paths are working:
好现在我录我的声音给你,你拿去当你拿去克隆,然后克隆好之后你随便用一段文字去转语音
转出来的语音传给我然后让我听听看。
1. template_name=language_id reports the wrong language
Mandarin input is reported as English.
$ audiocpp_cli ... --audio mandarin-15s.wav --request-option template_name=language_id
text_output=English
The same audio transcribes as Mandarin under the default template, so the
language detection is not reflecting the audio content.
Note that the language request option does not fix this — the
language_id branch builds its instruction without consulting language,
so --request-option language=zh-CN still yields English. Stating the
language inside instruct does work:
--request-option template_name=language_id \
--request-option "instruct=Identify the language spoken in this audio. The speech is in Chinese. Output only the language name."
text_output=Chinese
One note on the fix: Chinese is itself the English name of the language.
The output language is whatever instruct asks for — asking for the name in
Chinese returns 中文 (the langid_zhName case in the attached bench.log),
so the template does not normalise to English. The language_id branch is
therefore usable, provided the caller states the source language in instruct
rather than passing the language option.
2. template_name=audio_qa returns a content-free answer
With an English question about a Mandarin recording, the model answers with
the single token speech instead of answering.
$ audiocpp_cli ... --audio mandarin-15s.wav \
--request-option template_name=audio_qa \
--request-option "instruct=What is discussed in this recording?"
text_output=speech
custom_schema_output={"schema":"nemotron_transcribe.result.v1","data":{
"generated_tokens":2,"termination":"end_of_turn","stop_token_id":11,
"template_name":"audio_qa", ...}}
Note generated_tokens=2 — the model emitted a very short degenerate answer
and stopped on stop_token_id=11, rather than producing a content answer.
The degradation is stable across repeated runs, and the trigger is the
language of instruct rather than the audio: a Mandarin instruct on the
same recording returns a full Chinese answer, and an English instruct that
names the answer language returns a full English answer.
--request-option template_name=audio_qa \
--request-option "instruct=這段錄音在討論什麼?"
text_output=如何使用克隆功能並將結果轉回音頻
--request-option template_name=audio_qa \
--request-option "instruct=What is discussed in this recording? The speech is in Chinese. Answer in English."
text_output=Okay, now I'll record my voice for you. You take it as... you take it to clone. Then, after the cloning is done, just use a piece of text to convert it to audio, send the resulting audio to me, and let me listen.
(These two are the audioqa_zhInstruct and audioqa_enAnswer cases in the
attached bench.log.)
So instruct determines the answer language, and the degenerate case is the
one where instruct asks in English for Mandarin audio with no language
anchor on the decoder side. Stating the source language and the answer
language in instruct restores a content-bearing answer:
--request-option template_name=audio_qa \
--request-option "instruct=What is discussed in this recording? The speech is in Chinese. Answer in Chinese."
text_output=好,现在我录我的声音给你,你拿去克隆,克隆好了之后,你随便用一段文字转语音,转出来的语音传给我,让我听听看。
The answer is a close paraphrase of the recording rather than a topical
summary, but it is no longer the degenerate single-token reply. The
audio_qa branch does not consult the language option either, so the
language has to be stated in instruct.
3. template_name=translation mixes target and source language mid-output
Translating Mandarin audio to English starts correctly in English, then
switches back to Chinese for the remainder of the output:
$ audiocpp_cli ... --audio mandarin-20s.wav \
--request-option template_name=translation --request-option target_language=English
Do you understand bottles? It is equivalent to understanding bottles, bottles,
right? Understand bottles. So is it a bottle? Can we eat this kind of thing?
Is that so? I understand bottles. I understand tables like this. Yes, then
this is not a table. I don't know. If I don't know, then we cannot say it is
understanding. So we understand bottles and understanding bottles, bottles,
that cannot be separated. In other words, when understanding the meaning of
the place, we must understand the meaning of the place, the meaning of the
place, the meaning of the place, equal to understanding the meaning of the
place. Like this, right? If you understand the meaning of the place, then we
must understand it is the meaning of the place. That is absolutely necessary.
So用这个逻辑去讲的话,了解空心之前是不能了解四处地的。对,所以有一些人是走向这个。
对,就变成装置,经典上面两种都有,就是变成要透过四处地,通达圣意地 ...
The English portion is a reasonable translation; the switch to Chinese begins
mid-paragraph and the remaining Chinese text is not translated at all. The
source audio at that point is still Mandarin speech, so this looks like the
decoder reverting to the source language rather than a transcription error.
Naming the source language in instruct stops the language switch — the
output stays English throughout. It does surface a different defect: Chinese
nouns that the model leaves as pinyin rather than translating.
--request-option template_name=translation \
--request-option "instruct=Translate the audio into English. The speech is in Chinese. Translate all of it into English and do not switch back to the source language."
To understand "píngzi" is to understand that it is a "píngzi," right? To understand "píngzi," then is it a "píngzi"? This is not the case with "kēbo." Is it? Right? I understand "píngzi"; I understand the table. Yes, but whether this is a table, I don't know.
The translation branch does not consult the language option, so the
source language has to be stated in instruct.
4. diarization=true produces a translation instead of a transcription
This one is not a language-detection error: the transcript body is a rough
English rendering of the Mandarin speech rather than garbled text.
$ audiocpp_cli ... --audio mandarin-15s.wav --request-option diarization=true
text_output=<spk:0> huh now i i put my voice you know you go you go
The Mandarin source is 「好现在我录我的声音给你…」 ("OK, now I record my voice
for you…"), and the diarization output is an English paraphrase of exactly
that sentence. Under the default template on the same audio the output is
Mandarin — both with language=zh-CN and with no language at all,
because the plain transcription prompt asks the model to keep the language
as spoken. The two prompts differ here, and that is the whole difference
between the working and the failing case.
This one is fixed by naming the source language, which appends an explicit
anchor to whichever prompt is used:
$ audiocpp_cli ... --audio mandarin-15s.wav \
--request-option diarization=true --request-option language=zh-CN
text_output=<spk:0> 好现在我录我的声音给你你拿去当你拿去克隆然后克隆好之后你随便用一段文字去转语音转出来的语音传给我然后让我听听看
The speaker tag and CTC alignment are otherwise correct. Via audiocpp_cli the
custom_schema_output shows 13 aligned words, every one with
speaker_confidence between 0.9966 and 0.9993, speaker_column: 0,
transcript_speaker_tag: "spk:0", failed_streams: [], and second-based
start/end offsets. Only the transcript language changes. That the CTC head
aligns the English words correctly while the transcript body is a translation
of the Mandarin source suggests the decoder is emitting a translated string
rather than the alignment failing.
Note that the CLI and the HTTP server expose different word schemas. The CLI
returns speaker_confidence and second-based start/end; the server's
/v1/audio/transcriptions/details returns confidence (always 0 in these
runs) and sample-based start_sample/end_sample, and does not emit
speaker_confidence or failed_streams at all. The server also returns a
speaker_turns array, which the CLI output does not include.
5. audiocpp_cli and the HTTP server disagree on the same request
audiocpp_cli --request-option diarization=true and
POST /v1/audio/transcriptions/details with {"options":{"diarization":true}}
on the same audio produce different transcripts, and the difference is
stable, not sampling noise.
Reproduce the CLI side:
audiocpp_cli --task asr --family nemotron_transcribe \
--model /models/nemotron-transcribe-30b-a4b-experts-IQ4_NL.gguf \
--backend cuda --audio mandarin-15s.wav \
--request-option diarization=true --text-out /tmp/cli.txtReproduce the server side:
audiocpp_server --config server.json --log &
curl -s http://127.0.0.1:18080/v1/audio/transcriptions/details \
-H 'Content-Type: application/json' \
-d '{"model":"nemotron","audio":"mandarin-15s.wav","options":{"diarization":true}}'CLI (3 consecutive runs, byte-identical):
<spk:0> huh now i i put my voice you know you go you go
Server (3 consecutive requests, byte-identical):
<spk:0> huh now i i put my voice you you need to you need to clone then clone your voice after you say then you can vary it to whatever you want
These are not a prefix and a longer version of the same string — they diverge
after "huh now i i put my voice", and the server continues where the CLI
stops. The CLI's words array has 13 entries, the server's has 31. The two
front-ends agree on the first eight words (huh now i i put my voice you)
and then diverge: the CLI stops at know you go you go, while the server
continues you need to you need to clone then clone your voice after you say then you can vary it to whatever you want. So the two front-ends are not
decoding the same thing: they are producing two different English renderings
of the same Chinese audio, and each is stable across repeated runs. That
points at a difference in the decoding configuration between the CLI and the
HTTP paths (sampling seed, stopping criteria, or how the audio is framed),
not at nondeterminism.
With language=zh-CN the same comparison gives 55 entries on both sides and
the two arrays are identical element by element, so the divergence is
confined to the unanchored language=auto path.
Both responses are included verbatim in the attached server-full.json
(full /v1/audio/transcriptions/details bodies, words arrays not
truncated), and words-compare.sh reproduces the comparison end to end.
Note that the server's words array does align the full English output
(31 entries, sample-based), while the CLI's words array covers only the
shorter English output. This is consistent with the two paths differing in
how much of the generated sequence is retained, not in the CTC alignment.
However, this discrepancy disappears once the source language is stated. With
"options":{"diarization":true,"language":"zh-CN"} the server returns
<spk:0> 好现在我录我的声音给你你拿去当你拿去克隆然后克隆好之后你随便用一段文字去转语音转出来的语音传给我然后让我听听看
which is byte-identical to the CLI output for the same options. (Verified by
running the request above against the server and comparing the returned
text field to the CLI's mandarin15_diarize_zhCN output — they match
character for character, <spk:0> prefix included.) So the divergence is
confined to the language=auto path, and is most likely a side effect of the
missing language anchor described in the root-cause section rather than an
independent front-end difference. It is still worth reporting because it
shows that the two front-ends reach different decoding configurations for an
identical request, which will produce confusing results for anyone comparing
them.
Note that agreement here means the two front-ends agree, not that the result
is fully correct: as described above, the diarization path produces no
punctuation even with the language stated, so both front-ends return the same
unpunctuated Mandarin.
Questions
-
Should
languagedefault toautofor this family? The plain
transcription prompt is self-anchoring ("Keep the same language as
spoken"), so on 15 s and 20 s fixtures it stays Chinese underauto. But
on the 120 s fixture it becomes mixed English/Chinese underautoand
pure English underzh-CN— the first part of the audio is translated
while later segments remain in Mandarin (forauto), or the entire output
is translated to English (forzh-CN). This suggests that longer context
can overwhelm the prompt's language anchor, or that the model's internal
context window causes the instruction to be partially forgotten. The
degradation path differs:autoproduces mixed output whilezh-CN
produces pure English, meaning explicit language setting does NOT guarantee
correct language on long clips. Meanwhile the speaker prompt has no such
clause and a Mandarin recording can come back in English on the diarization
path. Meanwhile the sibling specmodel_specs/nemotron_asr.jsonalready
enumerates 40 source languages includingzh-CN. Would it be reasonable
for the port to append the language hint to the speaker prompt
unconditionally, or to require an explicitlanguagebefore building the
instruction? Should the plain path's protection be strengthened for long
clips? -
The
translation,audio_qaandlanguage_idbranches never consult the
languageoption, so the source language can only be conveyed through
instruct. Should those branches honourlanguagetoo, for consistency
with thedefaultbranch? -
model_specs/nemotron_transcribe.jsondeclares"languages": ["auto"]
where the sibling specs declare concrete languages. Should the concrete
list be carried over, so thataudiocpp_model_language_count()and the
UI can report the model's actual language coverage? -
In
translation, naming the source language removes the mid-output
language switch but leaves Chinese nouns as pinyin ("píngzi", "kēbo").
Is that an expected property of the upstream model, and is there a
decoding setting that avoids it? -
Should the upstream validation doc be extended with a Mandarin fixture,
givendocs/models/nemotron_transcribe.mdalready mentions Chinese
alignment units? -
For issue 5, should
audiocpp_cliand the server produce identical
transcripts for identical requests? They agree oncelanguageis
explicit, so the divergence appears confined to theautopath. -
The guard at line 160 rejects
return_timestamps=truefor any
non-defaulttemplate, with the message "CTC timestamps and speaker
labels require a source-language transcript". The condition is actually
about the template, not the language — no value oflanguagesatisfies
it. The check isword_times = flag(request.options, "return_timestamps") || diarization; if (word_times && mode != "default") throw runtime::InvalidRequestError("CTC timestamps and speaker labels require a source-language transcript");
and it runs before
languageis read at all (line 164), so passing
language=zh-CNcannot influence it. Verified:template_name=translation
withlanguage=zh-CNandreturn_timestamps=truestill fails with the
same message. Is the message intended to say "timestamps are only
available for the default template", or was a language-dependent check
intended here? -
The speaker prompt suppresses punctuation by instruction — it contains
"Use no punctuation and no capitalization" — so the unpunctuated
diarization output is the prompt working as designed, not a defect. The
gap is in the documentation: thediarizationoption in
model_specs/nemotron_transcribe.jsonis described as "Request
speaker-tagged transcription, CTC words and speaker activity turns",
with no mention that the output will contain no punctuation or
capitalization. A caller asking for diarization gets speaker tags and
timestamps but silently loses sentence boundaries. Should the option
description state this, and should there be a way to request
punctuation and speaker labels together?
Workarounds
Until the above is resolved, the following produce correct output for
Mandarin input:
| Template | Working invocation |
|---|---|
| default | --request-option language=zh-CN (recommended — see note below) |
| diarization | --request-option diarization=true --request-option language=zh-CN |
language_id |
--request-option template_name=language_id --request-option "instruct=Identify the language spoken in this audio. The speech is in Chinese. Output only the language name." |
audio_qa |
--request-option template_name=audio_qa --request-option "instruct=What is discussed in this recording? The speech is in Chinese. Answer in Chinese." |
translation |
--request-option template_name=translation --request-option "instruct=Translate the audio into English. The speech is in Chinese. Translate all of it into English and do not switch back to the source language." |
The language value is free-form — zh-CN, zh, Chinese and Mandarin
all behaved identically.
The default row is recommended: while the plain transcription prompt
already keeps the source language on short clips, the 120 s fixture shows
that longer context can overwhelm that anchor and produce mixed output under
auto or pure English under zh-CN. Explicit language setting does NOT
guarantee correct language on long clips — both auto and zh-CN fail,
but in different ways. This is the only reliable way to minimize (not eliminate)
length-dependent degradation.
One caveat on the diarization row: it fixes the language, but the result
carries no punctuation, because the speaker prompt asks for none (see the
section above). If punctuation matters, either run the default template
and treat the speaker turn as unavailable, or pass the speaker prompt
yourself with "Use punctuation and capitalization" in place of "Use no
punctuation and no capitalization" — that returns the <spk:0> tag, 55
aligned words and 7 punctuation marks together, as shown under "Additional
observations".
Additional observations (not part of the numbered issues)
|- An explicit instruct replaces the built-in prompt entirely, including
its language hint — model_specs/nemotron_transcribe.json documents this
for the language option ("An explicit instruct replaces this prompt,
including its language hint"). Because it also replaces the speaker
prompt's "Use no punctuation and no capitalization" instruction, a
caller-supplied instruct does get punctuation back. Passing the built-in
speaker prompt with "Use punctuation and capitalization" in place of "Use no
punctuation and no capitalization" returns the <spk:0> tag, the same
55 words as the unpunctuated run, and 7 punctuation marks on the 15 s
fixture (8 on the 20 s fixture) — the only configuration found that
returns speaker tags and punctuation together. It works only because
diarization documents that "An explicit instruct must request speaker
tags itself". Whether that split between the built-in prompt and the
caller-supplied one is intended is not obvious.
- The 120 s plain fixture shows two distinct degradations:
- Under
auto: mixed English/Chinese output (316 Han, 604 Latin) — partial translation degradation where the first part of the audio is translated while later segments remain in Mandarin. - Under
zh-CN: pure English output (0 Han, 1518 Latin) — complete language failure even with explicit language setting.
Both cases confirm that clip length can overwhelm the model's language anchor, but the degradation path differs:autoproduces mixed output whilezh-CNproduces pure English. This is a critical finding — explicit language setting does NOT guarantee correct language on long clips.
Verified by runninglong120_zhCN.txttest (0 Han, 1518 Latin).
- Under
model_specs/nemotron_transcribe.jsondeclares"languages": ["auto"],
which does not communicate which source languages are actually supported.- On the GB10 build the model runs between 4.9x and 21.3x realtime
(RTF 0.047–0.204) across the benchmark matrix, depending on template and
clip length. The short-answer templates (language_id,audio_qa) are the
fastest because they emit few tokens — 21.3x and 21.1x on the 15 s
fixture. The transcription templates land between 8.9x and 13.3x on the
15 s fixture and between 4.9x and 10.3x on the 20 s fixture, and the
120 s fixture runs at 5.7x. Realtime factor is not directly comparable
across clip lengths, since a fixed per-call cost is amortised over more
audio in the longer clips.
Individual measurements vary between runs by tens of percent — the 15 s
fixture appears once as 9.7x and once as 2.6x in the attachedbench.log,
and a separate repeat pass measured it at 9.5–10.1x every time. I would
not read the exact figures as precise. - Peak memory reported by
--metricson the 120 s fixture is
peak_footprint_mb=68395withpeak_footprint_source=linux_vsize, and
peak_rss_mb=1211. The footprint figure is the virtual size of the
process, which includes the mapped model weights; the resident figure is
far smaller. The same run reportsmetrics.wall_ms=19724.1against
metrics.audio_duration_ms=120009, i.e. RTF 0.164 and 6.08x realtime,
consistent with the 120 s entry inbench.log. - Setting
languageexplicitly costs essentially nothing. On the 15 s
fixture, repeated runs ofautoandzh-CNland within a few percent of
each other (9.56x / 10.09x versus 9.46x / 9.52x), so the workaround
carries no meaningful throughput penalty.
Activity
0xShug0 commentedon Nov 3, 2026
@huanruilee Thanks for the detailed report!
Reproduced: both 120-second Mandarin clips returned English translations instead of Chinese transcripts with
language=auto.Not reproduced: long clips failing with explicit Chinese, short-clip diarization switching to English, incorrect language identification, content-free QA, or CLI/server transcript differences.
I tested Q4_K, whereas you used IQ4_NL. These results don't establish a port bug; we still need comparison with the official implementation using identical audio and prompts.
Could you attach your audio fixtures and exact server configuration so I can reproduce the remaining failures?
@JarodMica Could you also take a look when you get a chance? Thanks!
0xShug0 commentedon Nov 3, 2026
@huanruilee I checked the official implementation with NVFP4 weights and the F16 encoder, and reproduced the same issue: 120-second Mandarin clips returned English translations instead of Chinese transcripts with
language=auto. So the symptom also occurs upstream and it is not exclusive to audio.cpp.I still couldn't reproduce the other reported issues in either audio.cpp or the official implementation. Please provide the audio fixtures and exact settings so I can determine whether these are model issues, porting bugs, or bugs inherited from upstream.
JarodMica commentedon Nov 3, 2026
@0xShug0 I did some follow-up testing with Codex.
We used a continuous hour from this Mandarin philosophy lecture, from 02:00 to 62:00, with greedy decoding and a 20,000-token output cap.
The default transcription request produced repetitive English in all five configurations:
The original full-BF16 model did it too, without GGML or GGUF conversion involved. That run repeated “So we have to do that.” 2,832 times. We also tried NVIDIA’s recommended checkpoint prompt, which ran into English repetition as well.
There is a difference in how the runtimes handle it. NVIDIA C++ has a repetition guard that audio.cpp currently lacks, so it cuts these loops off much sooner. Those shorter outputs still weren’t complete transcripts, and they didn’t stop because the model generated EOS. We also checked the unchanged NVIDIA CLI, and it produced the same text as the comparison harness.
There are still some differences I haven’t pinned down. One short BF16 case with a Chinese hint looped in audio.cpp while the references ended normally. Running it in a fresh process gave the exact same output, so it doesn’t appear to be leftover state from an earlier request. There was also a one-frame frontend difference, but matching the encoded frame count didn’t resolve the hour-long failure.
So I’m leaning on saying the main issue isn’t specific to audio.cpp or IQ4 since it happens in the Nvidia upstream comparison.
PR #1190 addresses a regional language-hint behavior; which originates from this investigation, but it doesn’t fix this case.
nemotron_transcribe: normalize regional language hints (#1190)
0xShug0 commentedon Nov 3, 2026
@JarodMica Thanks for the thorough investigation! Testing the original BF16 checkpoint (which I won’t have enough storage for until I upgrade on Black Friday 😄 ) is especially helpful. I'll take a look at the repetition guard.
0xShug0 commentedon Nov 4, 2026
#1198 adds an opt-in repetition guard.