Files
watch_sceen_ai/tests/test_emotion.py
EJClaw 87b77f9997 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>
2026-08-22 10:18:54 +09:00

91 lines
3.4 KiB
Python

"""Emotion-tag parsing for expressive TTS. A ``[감정]`` tag must steer the
pitch/speed of the text that follows without being spoken; a bracket that is NOT
a known emotion word must be kept as ordinary spoken content."""
from wsai.backends.emotion import (
EMOTION_PARAMS,
match_emotion,
parse_segments,
)
from wsai.dashboard import _speech_text
BASE = 1.3
def test_emotion_tag_is_not_spoken_and_sets_delivery():
segs = parse_segments("[기쁨] 오늘 날씨 좋다", BASE)
assert len(segs) == 1
assert "기쁨" not in segs[0].text # the tag word is dropped
assert segs[0].text == "오늘 날씨 좋다"
mult, semis = EMOTION_PARAMS["happy"]
assert segs[0].speed == BASE * mult
assert segs[0].pitch == semis
def test_midreply_emotion_change_splits_segments():
segs = parse_segments("[속상함] 정말 힘들었겠다. [힘차게] 하지만 넌 할 수 있어!", BASE)
assert len(segs) == 2
assert segs[0].text == "정말 힘들었겠다."
assert segs[1].text == "하지만 넌 할 수 있어!"
assert segs[0].pitch == EMOTION_PARAMS["sad"][1]
assert segs[1].pitch == EMOTION_PARAMS["hopeful"][1]
def test_non_emotion_bracket_is_spoken_without_brackets():
segs = parse_segments("[기쁨] 첫째는 [1번] 항목이야", BASE)
assert len(segs) == 1
# "1번" is not an emotion -> read it; brackets themselves are gone.
assert "1번" in segs[0].text
assert "[" not in segs[0].text and "]" not in segs[0].text
assert segs[0].pitch == EMOTION_PARAMS["happy"][1]
def test_text_before_first_tag_is_neutral():
segs = parse_segments("잠깐만. [신남] 찾았다!", BASE)
assert segs[0].text == "잠깐만."
assert segs[0].speed == BASE and segs[0].pitch == 0.0
assert segs[1].pitch == EMOTION_PARAMS["excited"][1]
def test_plain_text_is_one_neutral_segment():
segs = parse_segments("그냥 평범한 문장이야", BASE)
assert len(segs) == 1
assert segs[0].speed == BASE and segs[0].pitch == 0.0
def test_empty_input_yields_no_segments():
assert parse_segments("", BASE) == []
assert parse_segments(" ", BASE) == []
def test_only_emotion_tags_yield_no_segments():
# A reply that is nothing but tags has nothing to say.
assert parse_segments("[기쁨][신남]", BASE) == []
def test_match_emotion_is_synonym_and_space_tolerant():
assert match_emotion("속상함") == "sad"
assert match_emotion(" 힘 차게 ") == "hopeful" # squeezed + trimmed
assert match_emotion("행복하게") == "happy"
assert match_emotion("메모") is None # not an emotion
def test_persona_examples_are_all_recognised():
# Every emotion the brain persona advertises must resolve, or it would be
# 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]