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:
@@ -7,6 +7,7 @@ from wsai.backends.emotion import (
|
|||||||
match_emotion,
|
match_emotion,
|
||||||
parse_segments,
|
parse_segments,
|
||||||
)
|
)
|
||||||
|
from wsai.dashboard import _speech_text
|
||||||
|
|
||||||
BASE = 1.3
|
BASE = 1.3
|
||||||
|
|
||||||
@@ -74,3 +75,16 @@ def test_persona_examples_are_all_recognised():
|
|||||||
# read aloud instead of shaping the voice.
|
# read aloud instead of shaping the voice.
|
||||||
for word in ["힘차게", "궁금", "반가움", "차분하게", "웃으며", "속상함"]:
|
for word in ["힘차게", "궁금", "반가움", "차분하게", "웃으며", "속상함"]:
|
||||||
assert match_emotion(word) is not None, word
|
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]
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import queue
|
import queue
|
||||||
import re
|
|
||||||
import threading
|
import threading
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
|
||||||
@@ -26,20 +25,14 @@ from .monitor import Monitor
|
|||||||
|
|
||||||
log = logging.getLogger("wsai.dashboard")
|
log = logging.getLogger("wsai.dashboard")
|
||||||
|
|
||||||
_EMOTION_RE = re.compile(r"^\s*\[([^\]]+)\]\s*(.*)", re.S)
|
|
||||||
|
|
||||||
|
|
||||||
def _speech_text(reply: str) -> str:
|
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
|
The TTS backend itself understands bracketed emotion tags: known emotion
|
||||||
becomes a natural spoken word: ``[힘차게] 안녕!`` -> ``힘차게, 안녕!``. Replies
|
tags steer delivery and are not spoken; non-emotion brackets are spoken.
|
||||||
without a tag are spoken as-is."""
|
Do not rewrite a leading tag here, or the first emotion would be read aloud
|
||||||
m = _EMOTION_RE.match(reply or "")
|
and lost before ``MeloTTS.synth`` can parse it."""
|
||||||
if not m:
|
|
||||||
return reply
|
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"):
|
def _make_handler(dash: "Dashboard"):
|
||||||
|
|||||||
Reference in New Issue
Block a user