TTS: keep BigVGAN on CPU after GPU regression; LLM filter strips more tags
#2 BigVGAN GPU experiment: ORT-QNN GPU EP loaded the v2_decoder_conv ONNX model successfully (session creation 463 ms, no fallback warnings) but per-phrase inference jumped to ~3.5 s vs ~2 s on CPU 8-thread. The GPU/CPU memory transfer cost dominates for this conv-heavy decoder, and the optimization went the wrong way. Comment block updated to record both the HTP and GPU paths as tried-and-rejected so future passes don't re-walk the same ground. LLM streaming filter: extend the lookahead-based <think>…</think> suppressor to also strip singleton special tokens (<|im_start|>, <|im_end|>, <|endoftext|>). Previously the closing <|im_end|> at end of the assistant's turn leaked into the SentenceStreamer and ended up as a spurious sentence at the end of the TTS output. Same lookahead-buffer trick handles split tokens. Validated end-to-end: 'Bonjour, comment vas-tu ?' → "Bonjour ! Je vais bien, merci. Comment vas-tu ?" → seg 0 "Bonjour !", seg 1 "Je vais bien, merci." (no <|im_end|>), BigVGAN back to 1.8 s/phrase. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f4b15a72a7
commit
a41619ed67
|
|
@ -115,29 +115,43 @@ class ExecuTorchLlmEngine(
|
|||
var inThink = false
|
||||
val tokenScan = StringBuilder() // small lookahead to spot tag boundaries
|
||||
|
||||
// Singleton special tokens that should never reach the TTS streamer
|
||||
// (they leak when the model wraps its reply or signals end-of-turn).
|
||||
val stripTokens = listOf("<|im_start|>", "<|im_end|>", "<|endoftext|>")
|
||||
val maxTagLen = listOf("<think>", "</think>", "<|im_start|>", "<|im_end|>", "<|endoftext|>")
|
||||
.maxOf { it.length }
|
||||
|
||||
val cb = object : LlmCallback {
|
||||
override fun onResult(result: String) {
|
||||
if (firstTokenMs < 0) firstTokenMs = System.currentTimeMillis() - startTime
|
||||
responseBuilder.append(result)
|
||||
|
||||
// Forward to caller only outside <think> blocks. We accumulate
|
||||
// a tiny lookahead buffer so tag tokens that arrive split
|
||||
// ("<thi", "nk>") still match.
|
||||
// Forward to caller only outside <think> blocks, and strip
|
||||
// singleton special tokens. We accumulate a tiny lookahead buffer
|
||||
// so tag tokens that arrive split ("<thi", "nk>") still match.
|
||||
tokenScan.append(result)
|
||||
while (true) {
|
||||
if (!inThink) {
|
||||
val open = tokenScan.indexOf("<think>")
|
||||
if (open < 0) {
|
||||
// No tag pending — flush everything up to a safe point
|
||||
// (length minus 7 for the longest tag we look for).
|
||||
val safe = tokenScan.length - "<think>".length
|
||||
// No <think> open pending — strip any singleton tokens
|
||||
// that fully landed in the buffer, then flush prose
|
||||
// up to a safe point preserving lookahead.
|
||||
for (tok in stripTokens) {
|
||||
var idx = tokenScan.indexOf(tok)
|
||||
while (idx >= 0) {
|
||||
tokenScan.delete(idx, idx + tok.length)
|
||||
idx = tokenScan.indexOf(tok)
|
||||
}
|
||||
}
|
||||
val safe = tokenScan.length - maxTagLen
|
||||
if (safe > 0) {
|
||||
onToken?.invoke(tokenScan.substring(0, safe))
|
||||
tokenScan.delete(0, safe)
|
||||
}
|
||||
break
|
||||
}
|
||||
// Flush the prose before the tag, then enter think mode.
|
||||
// Flush the prose before the <think> tag, then enter think mode.
|
||||
if (open > 0) onToken?.invoke(tokenScan.substring(0, open))
|
||||
tokenScan.delete(0, open + "<think>".length)
|
||||
inThink = true
|
||||
|
|
|
|||
|
|
@ -243,7 +243,12 @@ class Qwen3TtsEngine(
|
|||
return session
|
||||
}
|
||||
|
||||
// Speech decoder V2 on CPU (HTP tested: BigVGAN convolutions too slow to compile)
|
||||
// Speech decoder V2 on CPU. Two paths tried, both worse than CPU:
|
||||
// - HTP: BigVGAN convolutions too slow to compile (timeout)
|
||||
// - GPU Adreno via QNN GPU EP: model loads but per-phrase
|
||||
// inference is ~3.5 s vs ~2 s on CPU (GPU/CPU memory transfer
|
||||
// overhead dominates for this conv-heavy model)
|
||||
// CPU 8-thread stays the practical optimum.
|
||||
val v2Path = "$path/v2_pre_conv"
|
||||
if (File("$v2Path/model.onnx").exists()) {
|
||||
nlog("Loading V2 speech decoder (CPU ONNX)...")
|
||||
|
|
|
|||
Loading…
Reference in New Issue