fix(stream): transcribe each append-mode chunk independently

The append mode used to feed chunks as successive conversation turns with
the context carried over. The model then copied the previous turn's text
verbatim instead of transcribing the new audio, so a continuous video
kept showing the same sentence forever. Transcribe each chunk
independently instead, matching sglang's segment streaming mode for
Qwen3-ASR; the fixed mode also stays flat at RTF 0.24 versus 0.57-1.08
for the incremental mode.

修复追加式的一个真 bug:原先把各段接成「携带上文的对话轮」,模型会抄上
一轮的文本 —— 连续视频里字幕永远重复同一句。改为每段独立转写(对应
sglang 给 Qwen3-ASR 的 segment 流式模式),内容随之正确变化;成本仍保持
平坦,RTF 0.24,而增量模式是 0.57~1.08。
This commit is contained in:
blumia committed 2026-10-12 14:32:07 +08:00
1 parent 85b206f1cd
commit 6d64987d64
2 files changed
+45 -26

No files matched your search

+16 -12
View File
@@ -370,25 +370,19 @@ void Qwen3Asr::streamReset()
d->turnCount = 0;
}
// 追加式轮次:把每小段音频当作一个新的对话轮追加到 KV 末尾。
// 序列形如 [user: audio_1][assistant: text_1][user: audio_2][assistant: text_2]...
// 好处是纯追加:不重算前缀、不重编码旧音频,单步成本与音频总长无关。
// 追加式:把音频切成定长段,每段独立转写后按顺序拼接(sglang 的 segment 模式)。
// 好处是纯追加/独立:不重算前缀、不重编码旧音频,单步成本与总长无关。
// 注意:段长必须给足(约 8s),因为编码器需要它的 8s 注意力窗口。
bool Qwen3Asr::prepareTurn(std::string * error)
{
if (d->turnReady) return true;
const std::string marker = mtmd_default_marker();
// 第 1 轮是完整的 user/assistant 结构;后续轮接在上一轮文本之后开新一轮。
const std::string s1 = "<|im_start|>user\n" + marker +
"<|im_end|>\n<|im_start|>assistant\n";
const std::string sc = "<|im_end|>\n<|im_start|>user\n" + marker +
"<|im_end|>\n<|im_start|>assistant\n";
std::vector<llama_token> tail1, tailC;
std::vector<llama_token> tail1;
if (!d->splitPrompt(s1, d->firstHead, tail1, error)) return false;
if (!d->splitPrompt(sc, d->contHead, tailC, error)) return false;
// 两种轮尾应当一致
d->tailTokens = tail1;
llama_memory_clear(llama_get_memory(d->ctx), true);
@@ -407,10 +401,20 @@ AsrStreamStep Qwen3Asr::streamAppendTurn(const float * pcm, size_t nSamples, int
std::string err;
if (!prepareTurn(&err)) { res.error = err; return res; }
// 每一轮都当作独立的一段来转写(清空 KV、只用第 1 轮的 prompt)。
//
// 不能沿用上一轮的上下文:实测模型会"抄"上一轮的文本 —— 新一轮的音频
// 哪怕内容完全不同,它也会把上一轮的句子原样再吐一遍。
// 这与 sglang 给 Qwen3-ASR 定的 segment 流式模式一致:
// 把音频切成定长段分别独立转写,再按顺序拼接。
llama_memory_clear(llama_get_memory(d->ctx), true);
d->nPastTurn = 0;
d->turnCount = 0;
const auto t0 = std::chrono::steady_clock::now();
// 1) 轮首文本(纯追加,位置接在 KV 末尾)
const std::vector<llama_token> & head = (d->turnCount == 0) ? d->firstHead : d->contHead;
// 1) 轮首文本(全新的一段,从位置 0 开始)
const std::vector<llama_token> & head = d->firstHead;
llama_pos p = d->nPastTurn;
if (!d->decodeTokens(head.data(), (int32_t) head.size(), p, false)) {
res.error = "解码轮首失败";