diff --git a/frontend/src/composables/useStreamingTts.ts b/frontend/src/composables/useStreamingTts.ts index dda7084..80c9ed2 100644 --- a/frontend/src/composables/useStreamingTts.ts +++ b/frontend/src/composables/useStreamingTts.ts @@ -86,11 +86,21 @@ export function useStreamingTts(options: UseStreamingTtsOptions): UseStreamingTt try { blob = await synthesiseSpeech(stripped) } catch (e) { - console.warn('[StreamingTTS] Synthesis failed, retrying sentence', { sentence: stripped, error: e }) + const errMsg = e instanceof Error ? e.message : String(e) + console.warn('[StreamingTTS] Synthesis failed, retrying', { + chars: stripped.length, + preview: stripped.slice(0, 80), + error: errMsg, + }) try { blob = await synthesiseSpeech(stripped) } catch (e2) { - console.warn('[StreamingTTS] Retry also failed, skipping sentence', { sentence: stripped, error: e2 }) + const errMsg2 = e2 instanceof Error ? e2.message : String(e2) + console.warn('[StreamingTTS] Retry failed, sentence dropped', { + chars: stripped.length, + preview: stripped.slice(0, 80), + error: errMsg2, + }) } } finally { pendingCount.value-- diff --git a/src/fabledassistant/routes/voice.py b/src/fabledassistant/routes/voice.py index e4bb98b..b0ed890 100644 --- a/src/fabledassistant/routes/voice.py +++ b/src/fabledassistant/routes/voice.py @@ -117,7 +117,12 @@ async def synthesise_speech(): if not text: return jsonify({"error": "text is required"}), 400 - if len(text) > 8000: + char_count = len(text) + if char_count > 8000: + logger.warning( + "TTS request rejected: text too long (%d chars, limit 8000). Preview: %r", + char_count, text[:120], + ) return jsonify({"error": "text too long (max 8000 characters)"}), 400 voice = str(data.get("voice", "af_heart")) @@ -154,11 +159,29 @@ async def synthesise_speech(): except Exception: pass + blend_desc = f"blend({len(voice_blend)} voices)" if voice_blend else voice + logger.info("TTS synthesis start: %d chars, voice=%s, speed=%.2f", char_count, blend_desc, speed) + t0 = time.monotonic() try: wav_bytes = await synthesise(text, voice=voice, speed=speed, voice_blend=voice_blend) except Exception: - logger.exception("TTS synthesis failed") + logger.exception( + "TTS synthesis failed: %d chars, voice=%s. Preview: %r", + char_count, blend_desc, text[:120], + ) return jsonify({"error": "Synthesis failed"}), 500 + duration_ms = round((time.monotonic() - t0) * 1000) + if not wav_bytes: + logger.warning( + "TTS synthesis returned empty audio: %d chars, voice=%s, %dms. Preview: %r", + char_count, blend_desc, duration_ms, text[:120], + ) + else: + logger.info( + "TTS synthesis complete: %d chars → %d bytes in %dms (voice=%s)", + char_count, len(wav_bytes), duration_ms, blend_desc, + ) + from quart import Response return Response(wav_bytes, mimetype="audio/wav") diff --git a/src/fabledassistant/services/tts.py b/src/fabledassistant/services/tts.py index fa68d74..34f5263 100644 --- a/src/fabledassistant/services/tts.py +++ b/src/fabledassistant/services/tts.py @@ -239,17 +239,32 @@ async def synthesise( voice_param = _build_voice_param() t0 = time.monotonic() audio_chunks: list = [] - for _, _, audio in _pipeline(text, voice=voice_param, speed=speed): # type: ignore[misc] - if audio is not None: - audio_chunks.append(audio) + try: + for _, _, audio in _pipeline(text, voice=voice_param, speed=speed): # type: ignore[misc] + if audio is not None: + audio_chunks.append(audio) + except Exception: + logger.exception( + "Kokoro pipeline error during synthesis: %d chars, preview=%r", + len(text), text[:80], + ) + raise if not audio_chunks: + logger.warning( + "Kokoro produced no audio chunks: %d chars, preview=%r", + len(text), text[:80], + ) return b"" combined = np.concatenate(audio_chunks) buf = io.BytesIO() sf.write(buf, combined, samplerate=24000, format="WAV", subtype="PCM_16") - logger.debug("TTS synthesis took %.2fs for %d chars", time.monotonic() - t0, len(text)) + elapsed = time.monotonic() - t0 + logger.info( + "Kokoro synthesis: %d chars → %d samples (%.2fs, %.0f chars/s)", + len(text), len(combined), elapsed, len(text) / elapsed if elapsed > 0 else 0, + ) return buf.getvalue() loop = asyncio.get_running_loop()