diff --git a/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaPipeline.kt b/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaPipeline.kt index 30d112e..104c924 100644 --- a/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaPipeline.kt +++ b/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaPipeline.kt @@ -129,13 +129,35 @@ class KazeiaPipeline { return ProcessorResult(responseText = text, metadata = mapOf("mode" to "echo")) } - private suspend fun speak(text: String) { + private suspend fun speak(text: String) = speakText(text) + + /** + * Public entry point for speaking a full (possibly multi-sentence) text. + * When TTS is Qwen3, text is sentence-split and fed through a streaming + * session so first audio arrives after the first sentence rather than + * after the full response is synthesised. Other TTS backends fall back + * to the legacy one-shot synthesizeAndPlay call. + * + * Made public so KazeiaService can route its voice-command replies and + * the echo-mode playback through the same path — otherwise each TTS + * site reimplemented the "streaming-or-fallback" dispatch. + */ + suspend fun speakText(text: String) { val ttsEngine = tts ?: return _pipelineState.value = PipelineState.Speaking try { - ttsEngine.synthesizeAndPlay(text, context.language, - onComplete = { _pipelineState.value = PipelineState.Idle } - ) + val qwen = ttsEngine as? com.kazeia.tts.Qwen3TtsEngine + if (qwen != null) { + qwen.startStreamingSession() + val streamer = com.kazeia.tts.SentenceStreamer { s -> qwen.enqueueSentence(s) } + streamer.append(text) + streamer.flush() + qwen.endStreamingSession() + } else { + ttsEngine.synthesizeAndPlay(text, context.language, + onComplete = { _pipelineState.value = PipelineState.Idle } + ) + } } catch (e: Exception) { log("TTS error: ${e.message}") } diff --git a/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaService.kt b/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaService.kt index 5084dba..e973a7d 100644 --- a/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaService.kt +++ b/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaService.kt @@ -1008,13 +1008,13 @@ class KazeiaService : Service() { val lastKazeia = _messages.value.lastOrNull { it.role == ChatMessage.Role.KAZEIA } if (lastKazeia != null) { serviceScope.launch { - _pipelineState.value = PipelineState.Speaking - tts.synthesizeAndPlay(lastKazeia.text, "fr", - onComplete = { - _pipelineState.value = if (_isListening.value) - PipelineState.Listening else PipelineState.Idle - } - ) + // Route the "repeat last answer" command through the + // streaming TTS session so the user hears the first + // sentence immediately instead of waiting for a full + // re-synthesis of a long previous response. + pipeline.speakText(lastKazeia.text) + _pipelineState.value = if (_isListening.value) + PipelineState.Listening else PipelineState.Idle } } } @@ -1132,20 +1132,15 @@ class KazeiaService : Service() { _pipelineState.value = PipelineState.Thinking conversationManager.onNewTurn() - // If LLM not loaded, use echo mode with TTS + // If LLM not loaded, use echo mode with TTS. Route through the + // streaming session — in debug builds this is the fast path most + // exercised, and it benefits from per-sentence playback like any + // real response would. if (!llm.isLoaded()) { log("Echo mode: '$patientMessage' → TTS (${tts::class.simpleName})") val echoResponse = patientMessage addMessage(ChatMessage(role = ChatMessage.Role.KAZEIA, text = echoResponse)) - _pipelineState.value = PipelineState.Speaking - tts.synthesizeAndPlay( - text = echoResponse, - language = "fr", - onComplete = { - _pipelineState.value = if (_isListening.value) - PipelineState.Listening else PipelineState.Idle - } - ) + pipeline.speakText(echoResponse) _pipelineState.value = if (_isListening.value) PipelineState.Listening else PipelineState.Idle return } @@ -1185,16 +1180,7 @@ class KazeiaService : Service() { if (responseText.isNotEmpty()) { addMessage(ChatMessage(role = ChatMessage.Role.KAZEIA, text = responseText)) - - _pipelineState.value = PipelineState.Speaking - tts.synthesizeAndPlay( - text = responseText, - language = "fr", - onComplete = { - _pipelineState.value = if (_isListening.value) - PipelineState.Listening else PipelineState.Idle - } - ) + pipeline.speakText(responseText) } _pipelineState.value = if (_isListening.value)