From 87b77f9997d435dfb0341b8bfdcd12c26db32467 Mon Sep 17 00:00:00 2001 From: EJClaw Date: Sat, 22 Aug 2026 10:18:54 +0900 Subject: [PATCH] =?UTF-8?q?fix(voice):=20stop=20dashboard=20from=20manglin?= =?UTF-8?q?g=20the=20leading=20[=EA=B0=90=EC=A0=95]=20tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard voice-turn path ran _speech_text() before MeloTTS.synth, which rewrote a leading "[힘차게] 안녕!" into "힘차게, 안녕!" — reading the first emotion aloud and destroying the tag before the TTS emotion parser could use it. Make _speech_text() a pass-through so every emotion tag (including the first) reaches synth intact and shapes pitch/speed instead of being spoken. Adds a regression test covering the leading-tag case. Co-Authored-By: Claude Opus 4.7 --- tests/test_emotion.py | 14 ++++++++++++++ wsai/dashboard.py | 19 ++++++------------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/tests/test_emotion.py b/tests/test_emotion.py index 1130244..083c5c4 100644 --- a/tests/test_emotion.py +++ b/tests/test_emotion.py @@ -7,6 +7,7 @@ from wsai.backends.emotion import ( match_emotion, parse_segments, ) +from wsai.dashboard import _speech_text BASE = 1.3 @@ -74,3 +75,16 @@ def test_persona_examples_are_all_recognised(): # read aloud instead of shaping the voice. for word in ["힘차게", "궁금", "반가움", "차분하게", "웃으며", "속상함"]: assert match_emotion(word) is not None, word + + +def test_voice_turn_keeps_leading_emotion_tag_for_tts_parser(): + text = "[속상함] 정말 힘들었겠다. [힘차게] 하지만 넌 할 수 있어!" + + spoken = _speech_text(text) + segs = parse_segments(spoken, BASE) + + assert spoken == text + assert segs[0].text == "정말 힘들었겠다." + assert segs[0].pitch == EMOTION_PARAMS["sad"][1] + assert segs[1].text == "하지만 넌 할 수 있어!" + assert segs[1].pitch == EMOTION_PARAMS["hopeful"][1] diff --git a/wsai/dashboard.py b/wsai/dashboard.py index e2397a4..5476999 100644 --- a/wsai/dashboard.py +++ b/wsai/dashboard.py @@ -18,7 +18,6 @@ from __future__ import annotations import json import logging import queue -import re import threading from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer @@ -26,20 +25,14 @@ from .monitor import Monitor log = logging.getLogger("wsai.dashboard") -_EMOTION_RE = re.compile(r"^\s*\[([^\]]+)\]\s*(.*)", re.S) - - def _speech_text(reply: str) -> str: - """Turn a reply like ``[힘차게] 안녕!`` into what the TTS should actually say. + """Return reply text exactly as authored for the TTS backend. - The bracketed emotion is spoken too (per user request), so the leading tag - becomes a natural spoken word: ``[힘차게] 안녕!`` -> ``힘차게, 안녕!``. Replies - without a tag are spoken as-is.""" - m = _EMOTION_RE.match(reply or "") - if not m: - return reply - emotion, rest = m.group(1).strip(), m.group(2).strip() - return f"{emotion}, {rest}" if rest else emotion + The TTS backend itself understands bracketed emotion tags: known emotion + tags steer delivery and are not spoken; non-emotion brackets are spoken. + Do not rewrite a leading tag here, or the first emotion would be read aloud + and lost before ``MeloTTS.synth`` can parse it.""" + return reply def _make_handler(dash: "Dashboard"):