User reported the emotion samples sounded the same and the speed change wasn't
noticeable. Two changes:
- Add a "base" (기본/neutral) emotion at speed 1.0x so the plain base voice can
be selected explicitly via a [기본] tag and auditioned against the others.
- Bump the WSAI_TTS_SPEED default 1.15 -> 1.5 for a clearly faster base voice.
Emotion multipliers scale off base, so every emotion speeds up together.
Also extends gen_emotion_samples.py to emit one wav per emotion (incl. 기본)
plus a stitched all-in-one, so each emotion can be delivered as a separate clip.
Verified: 29 tests pass; match_emotion('기본') == 'base'; per-emotion synthesis
at base 1.5 produces distinct clip durations (base 4.9s vs happy 3.3s).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
84 lines
4.2 KiB
Python
84 lines
4.2 KiB
Python
"""One-off: synthesize a short Korean sample for every canonical emotion.
|
|
|
|
Each clip announces the emotion name at neutral (base) speed, then speaks a
|
|
sample sentence steered by that emotion's ``[태그]`` — exactly the path the live
|
|
voice server uses. Writes one wav per emotion into an output directory (plus a
|
|
stitched all-in-one) so each emotion can be auditioned separately. Run with the
|
|
orchestrator venv:
|
|
|
|
.venv/bin/python -m tests.gen_emotion_samples /abs/out_dir
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import sys
|
|
import wave
|
|
from pathlib import Path
|
|
|
|
from wsai.backends.melo import MeloTTS
|
|
|
|
# (canonical english for filename, announced Korean name, emotion tag, sample)
|
|
SAMPLES: list[tuple[str, str, str, str]] = [
|
|
("base", "기본", "기본", "이건 기본 목소리예요, 감정 없이 이렇게 말해요."),
|
|
("happy", "기쁨", "기쁨", "오늘은 정말 기분 좋은 하루예요!"),
|
|
("excited", "신남", "신남", "우와, 이거 진짜 신난다! 빨리 하자!"),
|
|
("hopeful", "희망", "희망", "우리 분명히 잘 해낼 수 있어요!"),
|
|
("sad", "슬픔", "슬픔", "조금 속상한 일이 있었어요."),
|
|
("angry", "화남", "화남", "정말 너무하잖아요, 화가 나요."),
|
|
("fearful", "두려움", "두려움", "어떡하지, 너무 무서워요."),
|
|
("surprised", "놀람", "놀람", "어머, 이게 정말이에요?"),
|
|
("disgust", "혐오", "혐오", "으, 이건 좀 별로예요."),
|
|
("calm", "차분", "차분", "천천히 하나씩 정리해 볼게요."),
|
|
("friendly", "다정", "다정", "언제든지 편하게 말해 주세요."),
|
|
("serious", "진지", "진지", "이건 정말 중요한 이야기예요."),
|
|
("disappointed", "실망", "실망", "조금 아쉬운 결과네요."),
|
|
("tired", "피곤", "피곤", "아, 오늘 너무 피곤하네요."),
|
|
("affectionate", "사랑스럽게", "사랑스럽게", "당신은 정말 소중한 사람이에요."),
|
|
("playful", "장난스럽게", "장난스럽게", "히히, 한번 맞혀 보세요!"),
|
|
("curious", "궁금", "궁금", "그건 대체 왜 그런 걸까요?"),
|
|
("whisper", "속삭임", "속삭임", "조용히, 우리끼리만 아는 비밀이에요."),
|
|
("shout", "외침", "외침", "다 같이 힘내자, 파이팅!"),
|
|
("determined", "단호", "단호", "이번엔 반드시 해내겠어요."),
|
|
("relieved", "안도", "안도", "휴, 이제야 마음이 놓이네요."),
|
|
]
|
|
|
|
|
|
def stitch(paths: list[str], out: str, gap_s: float = 0.45) -> None:
|
|
with wave.open(paths[0], "rb") as w0:
|
|
nch, sw, fr = w0.getnchannels(), w0.getsampwidth(), w0.getframerate()
|
|
silence = b"\x00" * (int(fr * gap_s) * sw * nch)
|
|
with wave.open(out, "wb") as wo:
|
|
wo.setnchannels(nch)
|
|
wo.setsampwidth(sw)
|
|
wo.setframerate(fr)
|
|
for i, p in enumerate(paths):
|
|
with wave.open(p, "rb") as w:
|
|
wo.writeframes(w.readframes(w.getnframes()))
|
|
if i < len(paths) - 1:
|
|
wo.writeframes(silence)
|
|
|
|
|
|
async def main(out_dir: str) -> None:
|
|
d = Path(out_dir)
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
tts = MeloTTS() # base speed comes from WSAI_TTS_SPEED default
|
|
await tts.warmup()
|
|
print(f"melo ready ({tts.load_ms} ms), base speed {tts.speed}")
|
|
paths: list[str] = []
|
|
for i, (canon, name, tag, sample) in enumerate(SAMPLES, 1):
|
|
text = f"{name}. [{tag}] {sample}"
|
|
src = await tts.synth(text)
|
|
dst = d / f"emo_{i:02d}_{canon}.wav"
|
|
Path(src).replace(dst)
|
|
paths.append(str(dst))
|
|
print(f" {name:8s} -> {dst}")
|
|
await tts.aclose()
|
|
stitch(paths, str(d / "emotion_samples_all.wav"))
|
|
print(f"wrote {len(paths)} per-emotion wavs + stitched all -> {d}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
out_dir = sys.argv[1] if len(sys.argv) > 1 else "/tmp/emotion_samples"
|
|
asyncio.run(main(out_dir))
|