diff --git a/app/engines/melo_engine.py b/app/engines/melo_engine.py index 3e661d0..44c7070 100644 --- a/app/engines/melo_engine.py +++ b/app/engines/melo_engine.py @@ -367,6 +367,47 @@ class MeloEngine(BaseEngine): unit, _ = self._trim_start_silence(unit, sr, budget) pieces.append(unit) + def _scale_word_gaps( + self, + a: np.ndarray, + sr: int, + delta: float, + thresh: float = 0.02, + min_pause: float = 0.08, + ) -> np.ndarray: + """자연 합성된 문장 오디오에서 '어절 사이 무음'을 delta 초만큼 조절. + + - 억양 보존: 문장을 통째로 합성한 결과의 내부 무음 구간만 늘리거나 줄인다. + - delta>0 이면 무음을 늘려 단어를 벌리고, delta<0 이면 무음을 줄여 붙인다. + - 앞/뒤 경계 무음과 min_pause 미만의 짧은 무음(자음 폐쇄 등)은 건드리지 않아 + 발음이 깨지지 않는다. + """ + n = a.size + if abs(delta) < 1e-4 or n == 0: + return a + silent = np.abs(a) < thresh + changes = np.flatnonzero(np.diff(silent.astype(np.int8)) != 0) + 1 + bounds = [0, *changes.tolist(), n] + min_n = int(sr * min_pause) + floor_n = int(sr * 0.015) + add_n = int(delta * sr) + out: list[np.ndarray] = [] + last = len(bounds) - 2 + for k in range(len(bounds) - 1): + s, e = bounds[k], bounds[k + 1] + seg = a[s:e] + is_internal = 0 < k < last # 앞/뒤 경계 구간은 제외 + if silent[s] and is_internal and (e - s) >= min_n: + new_n = max(floor_n, (e - s) + add_n) + if new_n >= (e - s): + seg = np.concatenate( + [seg, np.zeros(new_n - (e - s), dtype=np.float32)] + ) + else: + seg = seg[:new_n] + out.append(seg) + return np.concatenate(out) if out else a + # ---- 합성 ----------------------------------------------------- def synth_wav( self, @@ -381,8 +422,8 @@ class MeloEngine(BaseEngine): """세 가지를 독립적으로 조절: - speed: 글자(음절) 발화 속도 — 모델 length_scale 로 각 음절의 발음 속도를 생성 단계에서 조절(피치 보존, 사후 배속 아님) - - word_gap: 단어 사이 간격(초). >0 무음 삽입(어절 단위 합성), - <0 경계 무음을 잘라 더 붙임, 0 자연스러운 문장 합성 + - word_gap: 단어 사이 간격(초). 문장을 통째로 합성한 뒤 어절 사이 무음을 + >0 이면 늘리고 <0 이면 줄인다(억양 보존, 0 미만으로도 붙일 수 있음) - sentence_gap: 문장 사이 간격(초). >0 무음 삽입, <0 경계 무음 트리밍 간격은 발화 속도와 무관하게 유지되어 서로 독립적이다. """ @@ -401,36 +442,23 @@ class MeloEngine(BaseEngine): if language in ("AUTO", "KR"): text = normalize_korean(text) - # word_gap 이 0 이 아니면(양/음 모두) 어절 단위로 나눠 간격을 조절 - use_word_mode = abs(word_gap) > 1e-4 sentences = split_sentences(text) or [text] sr: int | None = None pieces: list[np.ndarray] = [] first = True for si, sent in enumerate(sentences): - sent_gap = 0.0 if first else sentence_gap - if use_word_mode: - words = sent.split() or [sent] - for wi, word in enumerate(words): - a, s = self._synth_natural(word, language, speaker, speed) - if sr is None: - sr = s - gap = sent_gap if wi == 0 else word_gap - if first: - pieces.append(a) - first = False - else: - self._append_unit(pieces, a, gap, sr) + # 문장을 통째로 자연 합성(억양 유지) 후 어절 사이 무음만 조절 + a, s = self._synth_natural(sent, language, speaker, speed) + if sr is None: + sr = s + if abs(word_gap) > 1e-4: + a = self._scale_word_gaps(a, sr, word_gap) + if first: + pieces.append(a) + first = False else: - a, s = self._synth_natural(sent, language, speaker, speed) - if sr is None: - sr = s - if first: - pieces.append(a) - first = False - else: - self._append_unit(pieces, a, sent_gap, sr) + self._append_unit(pieces, a, sentence_gap, sr) audio = np.concatenate(pieces) if pieces else np.zeros(1, dtype=np.float32) if sr is None: