Compare commits
2 Commits
c0665893a7
...
fae52f67c5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fae52f67c5 | ||
|
|
dde57aafda |
@@ -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))
|
||||||
|
|||||||
@@ -39,10 +39,11 @@ class TTSRequest(BaseModel):
|
|||||||
engine: str | None = Field(None, description="엔진 ID (melo / coqui-kss 등)")
|
engine: str | None = Field(None, description="엔진 ID (melo / coqui-kss 등)")
|
||||||
language: str = Field("KR", description="언어 코드")
|
language: str = Field("KR", description="언어 코드")
|
||||||
speaker: str | None = Field(None, description="화자/모델 ID")
|
speaker: str | None = Field(None, description="화자/모델 ID")
|
||||||
speed: float = Field(1.4, ge=0.5, le=2.0, description="글자(음소) 발화 속도")
|
# 기본값은 각 범위의 가운데. 슬라이더 양 끝이 조절 가능한 최소/최대치.
|
||||||
|
speed: float = Field(1.25, ge=0.5, le=2.0, description="글자(음소) 발화 속도(0.5~2.0, 기본 1.25)")
|
||||||
pitch: float = Field(0.0, ge=-12.0, le=12.0, description="피치(반음)")
|
pitch: float = Field(0.0, ge=-12.0, le=12.0, description="피치(반음)")
|
||||||
word_gap: float = Field(0.0, ge=0.0, le=1.0, description="단어 사이 무음(초)")
|
word_gap: float = Field(0.25, ge=0.0, le=0.5, description="단어 사이 무음 초(0~0.5, 기본 0.25)")
|
||||||
sentence_gap: float = Field(0.3, ge=0.0, le=2.0, description="문장 사이 무음(초)")
|
sentence_gap: float = Field(0.75, ge=0.0, le=1.5, description="문장 사이 무음 초(0~1.5, 기본 0.75)")
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
|
|||||||
@@ -43,26 +43,26 @@
|
|||||||
<div class="slider-row">
|
<div class="slider-row">
|
||||||
<div class="slider-head">
|
<div class="slider-head">
|
||||||
<span>글자 속도</span>
|
<span>글자 속도</span>
|
||||||
<span class="slider-val" id="speedVal">1.40x</span>
|
<span class="slider-val" id="speedVal">1.25x</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="range" id="speed" min="0.5" max="2.0" step="0.05" value="1.4" />
|
<input type="range" id="speed" min="0.5" max="2.0" step="0.05" value="1.25" />
|
||||||
<div class="slider-scale"><span>느리게</span><span>빠르게</span></div>
|
<div class="slider-scale"><span>0.5x 느리게</span><span>2.0x 빠르게</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="slider-row">
|
<div class="slider-row">
|
||||||
<div class="slider-head">
|
<div class="slider-head">
|
||||||
<span>단어 간격</span>
|
<span>단어 간격</span>
|
||||||
<span class="slider-val" id="wordGapVal">0 ms</span>
|
<span class="slider-val" id="wordGapVal">250 ms</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="range" id="wordGap" min="0" max="1.0" step="0.02" value="0" />
|
<input type="range" id="wordGap" min="0" max="0.5" step="0.01" value="0.25" />
|
||||||
<div class="slider-scale"><span>붙여서</span><span>띄어서</span></div>
|
<div class="slider-scale"><span>0 ms 붙여서</span><span>500 ms 띄어서</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="slider-row">
|
<div class="slider-row">
|
||||||
<div class="slider-head">
|
<div class="slider-head">
|
||||||
<span>문장 간격</span>
|
<span>문장 간격</span>
|
||||||
<span class="slider-val" id="sentGapVal">0.30 s</span>
|
<span class="slider-val" id="sentGapVal">0.75 s</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="range" id="sentGap" min="0" max="1.5" step="0.05" value="0.3" />
|
<input type="range" id="sentGap" min="0" max="1.5" step="0.05" value="0.75" />
|
||||||
<div class="slider-scale"><span>짧게</span><span>길게</span></div>
|
<div class="slider-scale"><span>0 s 짧게</span><span>1.5 s 길게</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="slider-row">
|
<div class="slider-row">
|
||||||
<div class="slider-head">
|
<div class="slider-head">
|
||||||
|
|||||||
Reference in New Issue
Block a user