fix: 글자 속도를 생성 단계 length_scale(음절 발화 속도)로 변경

- 사후 배속(atempo) 대신 model.tts_to_file(speed=) 로 각 음절(안/녕/하)의
  발음 속도 자체를 조절. 피치 보존, TSM 아티팩트 없음
- _synth_segment/_synth_natural 에 speed 인자 전달, synth_wav 의 사후
  타임스트레치 제거. 단어·문장 간격은 여전히 발화 속도와 독립
This commit is contained in:
claude
2026-08-26 00:20:57 +09:00
parent dde57aafda
commit fae52f67c5

View File

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