feat(voice): improve TTS logging for root-cause diagnosis
- Route now logs every synthesis request (char count, voice, speed) - Route logs char count + text preview when the 8000-char limit is hit - Route logs empty audio with preview (helps spot no-chunk-produced edge case) - Route logs success with byte count and duration - Kokoro synthesise() logs per-call: samples produced, elapsed, chars/s - Kokoro synthesise() logs warning when zero audio chunks returned with preview - Kokoro synthesise() catches and logs pipeline-internal errors with preview - Frontend: console.warn now includes char count + 80-char preview on failure and retry Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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--
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user