Files
watch_sceen_ai/tests/test_textnorm.py
EJClaw 356d1128fa fix(voice): stop backtick TTS crash and actually count error turns
Claude replies with markdown/backticks by default; MeloTTS's Korean text
normaliser has no entry for '`' and dies with KeyError: '`', so any reply
mentioning a command/code block crashed the whole voice turn (500 on
/api/voice-turn). Fix at the shared synth() choke point with
normalize_for_speech(), which flattens code fences/inline code/links/markdown
and guarantees no backtick reaches the worker — covering both the dashboard
voice turn and the Discord speak() bridge. Also add a PERSONA line asking the
model to avoid markdown (belt-and-suspenders; the code strip is the real fix).

errors_total never moved for turn-level failures: it was only bumped by
log("error") events, and the dashboard voice path calls turn.finish(error=...)
without logging. Emit one error-level log event from Turn.finish() when a turn
ends in error, so both the server counter and the browser SSE mirror stay
consistent, guarded to count at most once. Drop the now-redundant pipeline
log("error") to avoid double counting and remove the dead _publish stub.

Verified: raw backtick -> worker KeyError '`' reproduced; after fix real
MeloTTS synth of a backtick+fenced reply succeeds; /api/voice-turn returns 200
with a wav body on a backtick reply and errors_total stays 0, and an induced
synth failure returns 500 with errors_total incrementing to exactly 1. Full
suite 18 passed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-18 23:29:13 +09:00

40 lines
1.7 KiB
Python

"""TTS input normalisation. Claude speaks in markdown/backticks; MeloTTS's
Korean normaliser crashes on a bare backtick (``KeyError: '`'``), which used to
take down the whole voice turn. normalize_for_speech() must strip formatting and
above all guarantee no backtick reaches the synthesiser."""
from wsai.backends.melo import normalize_for_speech
def test_backticks_are_always_removed():
# The exact crash trigger: inline code, a fenced block, and a stray backtick.
reply = "`ls -la` 를 써봐. 예시:\n```python\nprint('hi')\n```\n그리고 ` 이건 홀로 남은 백틱"
out = normalize_for_speech(reply)
assert "`" not in out # the character that crashes MeloTTS is gone
assert "ls -la" in out # inner words are kept, just unwrapped
assert "print('hi')" in out # fenced code content survives as spoken text
def test_markdown_structure_flattened():
reply = "# 제목\n- 첫째 항목\n- 둘째 항목\n**굵게** 그리고 _기울임_\n> 인용문"
out = normalize_for_speech(reply)
assert "#" not in out
assert "**" not in out and "_" not in out
assert not out.lstrip().startswith(("-", ">"))
assert "첫째 항목" in out and "굵게" in out and "인용문" in out
def test_links_reduced_to_label():
out = normalize_for_speech("자세히는 [문서](https://example.com/docs) 참고해")
assert "문서" in out
assert "http" not in out and "]" not in out and "(" not in out
def test_plain_text_is_left_intact():
plain = "안녕, 지금 화면 잘 보고 있어. 뭐 도와줄까?"
assert normalize_for_speech(plain) == plain
def test_empty_is_safe():
assert normalize_for_speech("") == ""