diff --git a/app/engines/melo_engine.py b/app/engines/melo_engine.py index 0fbac9b..ccbd663 100644 --- a/app/engines/melo_engine.py +++ b/app/engines/melo_engine.py @@ -199,14 +199,15 @@ class MeloEngine(BaseEngine): return list(spk2id.values())[0] def _synth_segment( - self, text: str, lang: str, speaker: str | None + self, text: str, lang: str, speaker: str | None, speed: float = 1.0 ) -> tuple[np.ndarray, int]: - # 항상 자연 속도(1.0)로 합성 — 속도는 사후 타임스트레치로 처리해 발음 유지 + # 글자(음절) 발화 속도는 모델 length_scale(=1/speed)로 생성 단계에서 조절. + # 각 음절의 발음 자체가 빨라/느려지며(피치 보존), 사후 배속이 아니다. model = self._get_model(lang) speaker_id = self._resolve_speaker(model, lang, speaker) with self._global_lock: audio = model.tts_to_file( - text, speaker_id, output_path=None, speed=1.0, quiet=True + text, speaker_id, output_path=None, speed=speed, quiet=True ) sr = model.hps.data.sampling_rate return np.asarray(audio, dtype=np.float32), sr @@ -273,11 +274,12 @@ class MeloEngine(BaseEngine): return np.asarray(out, dtype=np.float32) def _synth_natural( - self, text: str, language: str, speaker: str | None + self, text: str, language: str, speaker: str | None, speed: float = 1.0 ) -> tuple[np.ndarray, int]: - """자연 속도(1.0)로 텍스트 한 조각을 합성. AUTO 모드는 언어별로 나눠 합성. + """텍스트 한 조각을 합성. AUTO 모드는 언어별로 나눠 합성. - 속도/문장·단어 간격 조절은 이 결과 위에서 처리한다(여기서는 하지 않음). + speed 는 모델 length_scale 로 각 음절의 발화 속도를 조절한다(생성 단계). + 문장·단어 간격은 이 결과 위에서 삽입한다(여기서는 하지 않음). """ if language == "AUTO": # 영어 구간엔 사용자가 고른 억양, 한국어 구간엔 KR 음성 @@ -291,7 +293,7 @@ class MeloEngine(BaseEngine): gap = None for seg_lang, seg_text in parts: seg_spk = spk_by_lang.get(seg_lang, en_speaker) - audio, seg_sr = self._synth_segment(seg_text, seg_lang, seg_spk) + audio, seg_sr = self._synth_segment(seg_text, seg_lang, seg_spk, speed) if sr is None: sr = seg_sr gap = np.zeros(int(sr * 0.06), dtype=np.float32) @@ -304,7 +306,7 @@ class MeloEngine(BaseEngine): audios.append(audio) audio = np.concatenate(audios) if audios else np.zeros(1, dtype=np.float32) return audio, (sr or self._get_model(DEFAULT_LANG).hps.data.sampling_rate) - return self._synth_segment(text, language, speaker) + return self._synth_segment(text, language, speaker, speed) # ---- 합성 ----------------------------------------------------- def synth_wav( @@ -318,10 +320,11 @@ class MeloEngine(BaseEngine): sentence_gap: float | None = None, ) -> bytes: """세 가지를 독립적으로 조절: - - speed: 글자(음소) 발화 속도 — 자연 합성 후 타임스트레치(피치 보존) + - speed: 글자(음절) 발화 속도 — 모델 length_scale 로 각 음절의 발음 + 속도를 생성 단계에서 조절(피치 보존, 사후 배속 아님) - word_gap: 단어 사이 추가 무음(초). >0 이면 어절 단위로 나눠 합성 - sentence_gap: 문장 사이 무음(초) - 간격은 타임스트레치의 영향을 받지 않아 글자속도와 독립적으로 유지된다. + 간격은 발화 속도와 무관하게 지정한 초만큼 유지되어 서로 독립적이다. """ text = (text or "").strip() if not text: @@ -338,11 +341,6 @@ class MeloEngine(BaseEngine): if language in ("AUTO", "KR"): text = normalize_korean(text) - def stretch(a: np.ndarray, sr: int) -> np.ndarray: - if abs(speed - 1.0) > 1e-3: - return self._time_stretch(a, sr, speed) - return a - sentences = split_sentences(text) or [text] sr: int | None = None pieces: list[np.ndarray] = [] @@ -353,10 +351,9 @@ class MeloEngine(BaseEngine): words = sent.split() wpieces: list[np.ndarray] = [] for wi, word in enumerate(words): - a, s = self._synth_natural(word, language, speaker) + a, s = self._synth_natural(word, language, speaker, speed) if sr is None: sr = s - a = stretch(a, sr) if wi > 0: wpieces.append(np.zeros(int(sr * word_gap), dtype=np.float32)) wpieces.append(a) @@ -366,10 +363,10 @@ class MeloEngine(BaseEngine): else np.zeros(1, dtype=np.float32) ) else: - a, s = self._synth_natural(sent, language, speaker) + a, s = self._synth_natural(sent, language, speaker, speed) if sr is None: sr = s - sent_audio = stretch(a, sr) + sent_audio = a if pieces and sentence_gap > 1e-4: pieces.append(np.zeros(int(sr * sentence_gap), dtype=np.float32))