fix(voice): stop dashboard from mangling the leading [감정] tag

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 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-08-22 10:18:54 +09:00
parent 4db73bf69f
commit 87b77f9997
2 changed files with 20 additions and 13 deletions

View File

@@ -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]

View File

@@ -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:
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
emotion, rest = m.group(1).strip(), m.group(2).strip()
return f"{emotion}, {rest}" if rest else emotion
def _make_handler(dash: "Dashboard"):