diff --git a/tests/gen_emotion_samples.py b/tests/gen_emotion_samples.py index 1d6aa81..21c09a8 100644 --- a/tests/gen_emotion_samples.py +++ b/tests/gen_emotion_samples.py @@ -1,11 +1,12 @@ -"""One-off: synthesize a short Korean sample for every canonical emotion and -stitch them into a single wav so the delivery of each emotion can be heard. +"""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. Run with the orchestrator venv: +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.wav + .venv/bin/python -m tests.gen_emotion_samples /abs/out_dir """ from __future__ import annotations @@ -13,31 +14,33 @@ from __future__ import annotations import asyncio import sys import wave +from pathlib import Path from wsai.backends.melo import MeloTTS -# (announced Korean name, emotion tag word, sample sentence) -SAMPLES: list[tuple[str, str, str]] = [ - ("기쁨", "기쁨", "오늘은 정말 기분 좋은 하루예요!"), - ("신남", "신남", "우와, 이거 진짜 신난다! 빨리 하자!"), - ("희망", "희망", "우리 분명히 잘 해낼 수 있어요!"), - ("슬픔", "슬픔", "조금 속상한 일이 있었어요."), - ("화남", "화남", "정말 너무하잖아요, 화가 나요."), - ("두려움", "두려움", "어떡하지, 너무 무서워요."), - ("놀람", "놀람", "어머, 이게 정말이에요?"), - ("혐오", "혐오", "으, 이건 좀 별로예요."), - ("차분", "차분", "천천히 하나씩 정리해 볼게요."), - ("다정", "다정", "언제든지 편하게 말해 주세요."), - ("진지", "진지", "이건 정말 중요한 이야기예요."), - ("실망", "실망", "조금 아쉬운 결과네요."), - ("피곤", "피곤", "아, 오늘 너무 피곤하네요."), - ("사랑스럽게", "사랑스럽게", "당신은 정말 소중한 사람이에요."), - ("장난스럽게", "장난스럽게", "히히, 한번 맞혀 보세요!"), - ("궁금", "궁금", "그건 대체 왜 그런 걸까요?"), - ("속삭임", "속삭임", "조용히, 우리끼리만 아는 비밀이에요."), - ("외침", "외침", "다 같이 힘내자, 파이팅!"), - ("단호", "단호", "이번엔 반드시 해내겠어요."), - ("안도", "안도", "휴, 이제야 마음이 놓이네요."), +# (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", "안도", "안도", "휴, 이제야 마음이 놓이네요."), ] @@ -56,21 +59,25 @@ def stitch(paths: list[str], out: str, gap_s: float = 0.45) -> None: wo.writeframes(silence) -async def main(out: str) -> None: - tts = MeloTTS() # base speed comes from the new WSAI_TTS_SPEED default (1.15) +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 name, tag, sample in SAMPLES: + for i, (canon, name, tag, sample) in enumerate(SAMPLES, 1): text = f"{name}. [{tag}] {sample}" - p = await tts.synth(text) - paths.append(p) - print(f" {name:8s} -> {p}") + 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, out) - print(f"stitched {len(paths)} clips -> {out}") + stitch(paths, str(d / "emotion_samples_all.wav")) + print(f"wrote {len(paths)} per-emotion wavs + stitched all -> {d}") if __name__ == "__main__": - out = sys.argv[1] if len(sys.argv) > 1 else "/tmp/emotion_samples.wav" - asyncio.run(main(out)) + out_dir = sys.argv[1] if len(sys.argv) > 1 else "/tmp/emotion_samples" + asyncio.run(main(out_dir)) diff --git a/wsai/backends/emotion.py b/wsai/backends/emotion.py index cfe7cd4..8ed175b 100644 --- a/wsai/backends/emotion.py +++ b/wsai/backends/emotion.py @@ -30,6 +30,7 @@ from dataclasses import dataclass # artefact-free. The pitch column is retained (rather than removed) so the effect # can be re-enabled per-emotion later with a real, artefact-free pitch method. EMOTION_PARAMS: dict[str, tuple[float, float]] = { + "base": (1.00, 0.0), # 기본 / neutral — the plain base voice, no colour "happy": (1.08, 0.0), # 기쁨 / cheerful "excited": (1.15, 0.0), # 신남 / excited "hopeful": (1.10, 0.0), # 희망 / 힘차게 @@ -69,6 +70,7 @@ def _norm(word: str) -> str: return re.sub(r"\s+", "", word).lower() +_register("base", "기본", "기본목소리", "기본톤", "보통", "평범", "무감정", "default", "neutral", "normal", "plain") _register("happy", "기쁨", "기쁘게", "기뻐", "기뻐하며", "행복", "행복하게", "행복하게도", "즐겁게", "즐거움", "밝게", "반가움", "반갑게", "반가워", "cheerful", "happy", "joyful") _register("excited", "신남", "신나게", "신나서", "흥분", "들뜬", "들떠서", "설렘", "설레며", "excited", "thrilled") _register("hopeful", "희망", "희망차게", "힘차게", "힘내", "힘내서", "응원", "응원하며", "격려", "hopeful", "encouraging") diff --git a/wsai/backends/melo.py b/wsai/backends/melo.py index a4817e5..81cce5d 100644 --- a/wsai/backends/melo.py +++ b/wsai/backends/melo.py @@ -12,7 +12,7 @@ Env: WSAI_MELO_DEVICE cpu | cuda | auto (default auto: GPU if torch sees one, else CPU; the worker falls back to CPU if CUDA fails) WSAI_TTS_OUT_DIR where wavs are written (default ~/.cache/wsai/tts) - WSAI_TTS_SPEED synthesis speed multiplier (default 1.15) + WSAI_TTS_SPEED synthesis speed multiplier (default 1.5) """ from __future__ import annotations @@ -93,7 +93,7 @@ class MeloTTS: self.out_dir = Path(out_dir or os.environ.get("WSAI_TTS_OUT_DIR") or (Path.home() / ".cache/wsai/tts")) self.speed = float(speed if speed is not None - else os.environ.get("WSAI_TTS_SPEED", "1.15")) + else os.environ.get("WSAI_TTS_SPEED", "1.5")) self.sink = sink or _log_sink self._proc: asyncio.subprocess.Process | None = None self._lock = asyncio.Lock()