perf(tts): NE PAS streamer LLM→CosyVoice (contention CPU) — documenté — v0.1.9

« Reste optionnel » : tenté le streaming LLM→TTS pour CosyVoice (synthèse de la
1ʳᵉ phrase pendant que le LLM décode, comme le chemin Qwen TTS-B).

Mesuré device : CONTRE-PRODUCTIF. LLM decode (6 threads CPU) et synthèse
CosyVoice (8 threads CPU) saturent tous les cœurs ; les chevaucher écroule le
LLM (17 → 0,55 tok/s, tour 1,1s → 41s, 1er son à 43s). Reverté.

Le streaming LLM→TTS n'a de sens que si les 2 étages utilisent des compute
distincts (Qwen3 NPU + TTS CPU). Pour CosyVoice (tout CPU) : laisser le LLM
finir vite PUIS synthesizeAndPlay, qui streame déjà phrase-par-phrase EN INTERNE
(synthèse N+1 pendant lecture N, sans contention). Net diff = un commentaire
garde-fou dans KazeiaService pour ne pas re-tenter ; code de streaming retiré.

Re-validé batch : LLM 17,1 tok/s (1,9s), TTS 1er son ~8s (RTF CosyVoice inhérent).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kazeia Team 2026-06-15 12:29:25 +02:00
parent b822974b99
commit db0e56b26c
2 changed files with 9 additions and 2 deletions

View File

@ -44,8 +44,8 @@ android {
applicationId = "com.kazeia" applicationId = "com.kazeia"
minSdk = 28 minSdk = 28
targetSdk = 36 targetSdk = 36
versionCode = 9 versionCode = 10
versionName = "0.1.8" versionName = "0.1.9"
// WebDAV de distribution — injecté depuis local.properties. // WebDAV de distribution — injecté depuis local.properties.
buildConfigField("String", "WEBDAV_BASE", "\"${localProp("webdav.base")}\"") buildConfigField("String", "WEBDAV_BASE", "\"${localProp("webdav.base")}\"")

View File

@ -1837,6 +1837,11 @@ Pas d'introduction, pas d'explication. Juste les 4 lignes en francais. /no_think
// the enqueueSentence plumbing fall through to the legacy post-LLM // the enqueueSentence plumbing fall through to the legacy post-LLM
// pipeline.speakText batch call. // pipeline.speakText batch call.
val qwen = tts as? com.kazeia.tts.Qwen3TtsEngine val qwen = tts as? com.kazeia.tts.Qwen3TtsEngine
// NB CosyVoice : PAS de streaming LLM→TTS. LLM decode et synthèse CosyVoice
// sont tous deux CPU-bound (saturent les cœurs) ; les chevaucher étrangle le
// LLM (mesuré : 17→0,55 tok/s, tour 1,1s→41s). On laisse le LLM finir vite
// (session ~1,1s) PUIS synthesizeAndPlay (chemin legacy) qui streame déjà
// phrase-par-phrase EN INTERNE (synthèse N+1 pendant lecture N, sans contention).
// Bulle vide + signal Thinking sur le visualizer : la couleur de l'orbe // Bulle vide + signal Thinking sur le visualizer : la couleur de l'orbe
// pulse pendant que le LLM réfléchit, plus de "..." textuels parasites. // pulse pendant que le LLM réfléchit, plus de "..." textuels parasites.
// L'écran reveal mot-par-mot s'enchaîne quand le premier audio joue. // L'écran reveal mot-par-mot s'enchaîne quand le premier audio joue.
@ -1899,6 +1904,8 @@ Pas d'introduction, pas d'explication. Juste les 4 lignes en francais. /no_think
val sentenceCounter = java.util.concurrent.atomic.AtomicInteger(0) val sentenceCounter = java.util.concurrent.atomic.AtomicInteger(0)
// TTS coupé (llmOnly) → pas de session de synthèse ouverte ; la bulle // TTS coupé (llmOnly) → pas de session de synthèse ouverte ; la bulle
// se remplit token par token dans onToken (voir plus bas). // se remplit token par token dans onToken (voir plus bas).
// Streaming LLM→TTS réservé à Qwen3 (NPU/Hexagon : pas de contention CPU avec
// le LLM). CosyVoice (CPU) passe par le chemin batch (voir note plus haut).
val ttsStreamer: com.kazeia.tts.SentenceStreamer? = val ttsStreamer: com.kazeia.tts.SentenceStreamer? =
if (qwen != null && !llmOnly) { if (qwen != null && !llmOnly) {
qwen.onSegmentPlaying = onSegPlay qwen.onSegmentPlaying = onSegPlay