fix: 단어 간격을 무음 스케일링으로 전환(자연 0 기준 아래로도 축소)

- 어절 개별 합성(억양 저하, 0 미만 불가) 대신, 문장을 통째로 자연 합성한 뒤
  내부 어절 무음 구간을 _scale_word_gaps 로 늘이거나 줄인다
- word_gap<0 이 자연 상태(0)보다 더 촘촘해짐(요청 충족), 억양 보존
- min_pause(80ms) 미만 짧은 무음/경계 무음은 건드리지 않아 발음 안전
This commit is contained in:
claude
2026-08-26 20:42:24 +09:00
parent c0fbc4d881
commit 5ed2d5fa8a

View File

@@ -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)
else:
# 문장을 통째로 자연 합성(억양 유지) 후 어절 사이 무음만 조절
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:
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: