feat(tts): add [기본] neutral emotion + raise base speed to 1.5x
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>
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user