From 80a83d9944b9c78ef578814b70369e6c0d113c94 Mon Sep 17 00:00:00 2001 From: EJClaw Date: Sat, 22 Aug 2026 23:10:52 +0900 Subject: [PATCH] =?UTF-8?q?fix(tts):=20remove=20monster-voice=20artefact?= =?UTF-8?q?=20=E2=80=94=20disable=20librosa=20pitch=20shift,=20base=20spee?= =?UTF-8?q?d=201.3=E2=86=921.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The emotional TTS path applied a librosa post-hoc pitch_shift (±1–3 semitones) on top of a 1.3x-fast Melo base, producing a robotic "monster" delivery. Zero out the pitch column for every emotion so pitch_shift is never invoked (the worker's _pitch_shift already no-ops on 0.0), and drop the default synthesis speed to 1.0. Emotion is now conveyed by speed alone — natural, artefact-free. The pitch column is retained so a proper pitch method can be re-enabled later. Verified: 29 tests pass; real 2-emotion synthesis on CUDA yields a clean wav with pitch=0.0 on all segments. Co-Authored-By: Claude Opus 4.7 --- wsai/backends/emotion.py | 46 +++++++++++++++++++++++----------------- wsai/backends/melo.py | 4 ++-- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/wsai/backends/emotion.py b/wsai/backends/emotion.py index 98a080a..cfe7cd4 100644 --- a/wsai/backends/emotion.py +++ b/wsai/backends/emotion.py @@ -23,27 +23,33 @@ from dataclasses import dataclass # Canonical emotion -> (speed multiplier relative to base, pitch shift in semitones). # Kept deliberately modest so delivery stays natural, not cartoonish. +# +# Pitch is held at 0.0 for every emotion: librosa's post-hoc pitch_shift on Melo +# output produced a robotic, "monster"-sounding artefact (worse when stacked on a +# fast base speed). Emotion is therefore conveyed by speed only — natural and +# 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]] = { - "happy": (1.08, 2.0), # 기쁨 / cheerful - "excited": (1.15, 3.0), # 신남 / excited - "hopeful": (1.10, 1.5), # 희망 / 힘차게 - "sad": (0.90, -2.5), # 슬픔 / sad - "angry": (1.12, 1.0), # 화남 / angry - "fearful": (1.12, 2.0), # 두려움 / terrified - "surprised": (1.05, 3.0), # 놀람 / surprise - "disgust": (0.96, -1.0), # 혐오 / disgust - "calm": (0.95, -1.0), # 차분 / calm - "friendly": (1.00, 1.0), # 다정 / friendly - "serious": (0.97, -1.0), # 진지 / serious - "disappointed":(0.92, -2.0), # 실망 / disappointed - "tired": (0.90, -2.0), # 피곤 / 지침 - "affectionate":(0.98, 1.0), # 사랑스럽게 / affectionate - "playful": (1.08, 2.0), # 장난스럽게 / playful - "whisper": (0.92, -1.5), # 속삭임 / whispering - "shout": (1.05, 2.5), # 외침 / shouting - "determined": (1.05, 0.5), # 단호 / determined - "relieved": (0.95, 0.5), # 안도 / relieved - "curious": (1.03, 1.5), # 궁금 / curious + "happy": (1.08, 0.0), # 기쁨 / cheerful + "excited": (1.15, 0.0), # 신남 / excited + "hopeful": (1.10, 0.0), # 희망 / 힘차게 + "sad": (0.90, 0.0), # 슬픔 / sad + "angry": (1.12, 0.0), # 화남 / angry + "fearful": (1.12, 0.0), # 두려움 / terrified + "surprised": (1.05, 0.0), # 놀람 / surprise + "disgust": (0.96, 0.0), # 혐오 / disgust + "calm": (0.95, 0.0), # 차분 / calm + "friendly": (1.00, 0.0), # 다정 / friendly + "serious": (0.97, 0.0), # 진지 / serious + "disappointed":(0.92, 0.0), # 실망 / disappointed + "tired": (0.90, 0.0), # 피곤 / 지침 + "affectionate":(0.98, 0.0), # 사랑스럽게 / affectionate + "playful": (1.08, 0.0), # 장난스럽게 / playful + "whisper": (0.92, 0.0), # 속삭임 / whispering + "shout": (1.05, 0.0), # 외침 / shouting + "determined": (1.05, 0.0), # 단호 / determined + "relieved": (0.95, 0.0), # 안도 / relieved + "curious": (1.03, 0.0), # 궁금 / curious } # Every spelling Claude might realistically emit, mapped to a canonical emotion. diff --git a/wsai/backends/melo.py b/wsai/backends/melo.py index d1ef5f6..f77a54e 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.3) + WSAI_TTS_SPEED synthesis speed multiplier (default 1.0) """ 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.3")) + else os.environ.get("WSAI_TTS_SPEED", "1.0")) self.sink = sink or _log_sink self._proc: asyncio.subprocess.Process | None = None self._lock = asyncio.Lock()