Spaces:
Running on Zero
Running on Zero
Use raw reference audio for voice cloning
Browse files- README.md +1 -1
- app.py +10 -31
- tests/test_latency_first_mode.py +11 -15
README.md
CHANGED
|
@@ -21,7 +21,7 @@ models:
|
|
| 21 |
## 模式
|
| 22 |
|
| 23 |
- **內建語者**:預設為內建語者 A;Speaker B 使用模型庫 `checkpoints/speaker_b_embedding.pt`,固定 seed 56789 與 projector scale 1.125。
|
| 24 |
-
- **參考音色**:
|
| 25 |
- **長文**:以最多 80 個字元快速分段,各分段單次生成後加入短靜音串接。
|
| 26 |
|
| 27 |
TTS model snapshot 固定為
|
|
|
|
| 21 |
## 模式
|
| 22 |
|
| 23 |
- **內建語者**:預設為內建語者 A;Speaker B 使用模型庫 `checkpoints/speaker_b_embedding.pt`,固定 seed 56789 與 projector scale 1.125。
|
| 24 |
+
- **參考音色**:錄音直接送入模型原生的 raw reference-audio conditioning,不經 ECAPA centroid;固定 seed 1337,每個文字分段仍只生成一次。
|
| 25 |
- **長文**:以最多 80 個字元快速分段,各分段單次生成後加入短靜音串接。
|
| 26 |
|
| 27 |
TTS model snapshot 固定為
|
app.py
CHANGED
|
@@ -106,7 +106,6 @@ from production import (
|
|
| 106 |
email_domain_mail_generation_variant,
|
| 107 |
endpoint_generation_plan,
|
| 108 |
estimate_step_seconds,
|
| 109 |
-
extract_windowed_speaker_embedding,
|
| 110 |
fade_variable_internal_edges,
|
| 111 |
finish_audio,
|
| 112 |
join_audio_chunks_variable,
|
|
@@ -249,14 +248,6 @@ QUALITY_FAST_EXACT_MIN_SPEAKER_SIMILARITY = 0.23
|
|
| 249 |
QUALITY_FAST_EXACT_MAX_BOUNDARY_SPEAKER_DROP = 0.08
|
| 250 |
QUALITY_MAX_SEQUENCE_PATHS = 3
|
| 251 |
SHORT_AUDIO_SPEAKER_GATE_SECONDS = 1.50
|
| 252 |
-
REFERENCE_MIN_DURATION_SECONDS = 3.0
|
| 253 |
-
# A short overlapping-window centroid is measurably closer to a recorded
|
| 254 |
-
# reference than one whole-clip embedding. Keep the cap small so extraction
|
| 255 |
-
# remains bounded and generation itself is still exactly single-pass.
|
| 256 |
-
REFERENCE_WINDOW_SECONDS = 3.0
|
| 257 |
-
REFERENCE_HOP_SECONDS = 1.5
|
| 258 |
-
REFERENCE_MAX_WINDOWS = 4
|
| 259 |
-
REFERENCE_FULL_CLIP_MAX_SECONDS = 0.0
|
| 260 |
SPEAKER_GENERATION_SEED = 56_789
|
| 261 |
SPEAKER_B_LABEL = "內建語者 B"
|
| 262 |
SPEAKER_B_PROJECTOR_SCALE = 1.125
|
|
@@ -264,8 +255,6 @@ SPEAKER_B_EMBEDDING_SHA256 = (
|
|
| 264 |
"e9556e14723c140985a104c1659d1ff8a5078d2fa28ce2fb756f04906641a8a7"
|
| 265 |
)
|
| 266 |
REFERENCE_GENERATION_SEED = 1_337
|
| 267 |
-
REFERENCE_CONDITIONING_SCALE = 1.25
|
| 268 |
-
REFERENCE_PROJECTOR_SCALE = 1.25
|
| 269 |
GLYPH_ASSET_MANIFEST_RELATIVE_PATH = "glyph_assets_v1/manifest.json"
|
| 270 |
# The asset builder replaces this unavailable sentinel with the exact frozen
|
| 271 |
# manifest SHA-256 before a candidate can expose the V5 profile.
|
|
@@ -2355,11 +2344,12 @@ def _fast_text_chunks(text: str) -> tuple[str, ...]:
|
|
| 2355 |
|
| 2356 |
def _generate_fast_chunk(
|
| 2357 |
text: str,
|
| 2358 |
-
centroid: torch.Tensor,
|
| 2359 |
*,
|
| 2360 |
cfg: float,
|
| 2361 |
steps: int,
|
| 2362 |
request_seed: int,
|
|
|
|
| 2363 |
) -> np.ndarray:
|
| 2364 |
"""Run one model call and return it without candidate qualification."""
|
| 2365 |
|
|
@@ -2391,6 +2381,8 @@ def _generate_fast_chunk(
|
|
| 2391 |
"max_len": hard_stop_steps,
|
| 2392 |
"retry_badcase": False,
|
| 2393 |
}
|
|
|
|
|
|
|
| 2394 |
if _NATIVE_STOP_POLICY:
|
| 2395 |
kwargs["stop_threshold"] = STOP_THRESHOLD
|
| 2396 |
kwargs["stop_consecutive"] = STOP_CONSECUTIVE
|
|
@@ -2431,7 +2423,7 @@ def _assemble_fast_audio(trajectory: tuple[np.ndarray, ...]) -> np.ndarray:
|
|
| 2431 |
|
| 2432 |
def _synthesize(
|
| 2433 |
text: str,
|
| 2434 |
-
centroid: torch.Tensor,
|
| 2435 |
*,
|
| 2436 |
cfg: float,
|
| 2437 |
steps: int,
|
|
@@ -2439,6 +2431,7 @@ def _synthesize(
|
|
| 2439 |
generation_audit: _GlyphCarrierGenerationAudit | None = None,
|
| 2440 |
request_seed: int | None = None,
|
| 2441 |
speaker_projector_scale: float = 1.0,
|
|
|
|
| 2442 |
) -> tuple[int, np.ndarray]:
|
| 2443 |
"""Latency-first single-pass synthesis with no output qualification."""
|
| 2444 |
|
|
@@ -2471,6 +2464,7 @@ def _synthesize(
|
|
| 2471 |
cfg=float(cfg),
|
| 2472 |
steps=int(steps),
|
| 2473 |
request_seed=seed,
|
|
|
|
| 2474 |
)
|
| 2475 |
for chunk in chunks
|
| 2476 |
)
|
|
@@ -3832,30 +3826,15 @@ def tts_reference(
|
|
| 3832 |
):
|
| 3833 |
if not reference_wav:
|
| 3834 |
raise gr.Error("請先錄音或上傳參考音檔。")
|
| 3835 |
-
try:
|
| 3836 |
-
with latency_stage("ecapa"):
|
| 3837 |
-
centroid = extract_windowed_speaker_embedding(
|
| 3838 |
-
reference_wav,
|
| 3839 |
-
_get_ecapa_encoder(),
|
| 3840 |
-
device="cpu",
|
| 3841 |
-
min_duration_seconds=REFERENCE_MIN_DURATION_SECONDS,
|
| 3842 |
-
window_seconds=REFERENCE_WINDOW_SECONDS,
|
| 3843 |
-
hop_seconds=REFERENCE_HOP_SECONDS,
|
| 3844 |
-
max_windows=REFERENCE_MAX_WINDOWS,
|
| 3845 |
-
full_clip_max_seconds=REFERENCE_FULL_CLIP_MAX_SECONDS,
|
| 3846 |
-
)
|
| 3847 |
-
centroid = centroid * REFERENCE_CONDITIONING_SCALE
|
| 3848 |
-
except ValueError as error:
|
| 3849 |
-
raise gr.Error(str(error)) from error
|
| 3850 |
return pcm16_audio_output(
|
| 3851 |
*_synthesize(
|
| 3852 |
text,
|
| 3853 |
-
|
| 3854 |
cfg=cfg,
|
| 3855 |
steps=steps,
|
| 3856 |
speed=speed,
|
| 3857 |
request_seed=REFERENCE_GENERATION_SEED,
|
| 3858 |
-
|
| 3859 |
)
|
| 3860 |
)
|
| 3861 |
|
|
@@ -3949,7 +3928,7 @@ with gr.Blocks(title="BlueMagpie-TTS Demo", theme=gr.themes.Soft()) as demo:
|
|
| 3949 |
with gr.Tab("參考音色"):
|
| 3950 |
gr.Markdown(
|
| 3951 |
"建議錄製 6–12 秒、單一語者且背景乾淨的授權參考音檔;"
|
| 3952 |
-
"
|
| 3953 |
)
|
| 3954 |
with gr.Row():
|
| 3955 |
with gr.Column():
|
|
|
|
| 106 |
email_domain_mail_generation_variant,
|
| 107 |
endpoint_generation_plan,
|
| 108 |
estimate_step_seconds,
|
|
|
|
| 109 |
fade_variable_internal_edges,
|
| 110 |
finish_audio,
|
| 111 |
join_audio_chunks_variable,
|
|
|
|
| 248 |
QUALITY_FAST_EXACT_MAX_BOUNDARY_SPEAKER_DROP = 0.08
|
| 249 |
QUALITY_MAX_SEQUENCE_PATHS = 3
|
| 250 |
SHORT_AUDIO_SPEAKER_GATE_SECONDS = 1.50
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
SPEAKER_GENERATION_SEED = 56_789
|
| 252 |
SPEAKER_B_LABEL = "內建語者 B"
|
| 253 |
SPEAKER_B_PROJECTOR_SCALE = 1.125
|
|
|
|
| 255 |
"e9556e14723c140985a104c1659d1ff8a5078d2fa28ce2fb756f04906641a8a7"
|
| 256 |
)
|
| 257 |
REFERENCE_GENERATION_SEED = 1_337
|
|
|
|
|
|
|
| 258 |
GLYPH_ASSET_MANIFEST_RELATIVE_PATH = "glyph_assets_v1/manifest.json"
|
| 259 |
# The asset builder replaces this unavailable sentinel with the exact frozen
|
| 260 |
# manifest SHA-256 before a candidate can expose the V5 profile.
|
|
|
|
| 2344 |
|
| 2345 |
def _generate_fast_chunk(
|
| 2346 |
text: str,
|
| 2347 |
+
centroid: torch.Tensor | None,
|
| 2348 |
*,
|
| 2349 |
cfg: float,
|
| 2350 |
steps: int,
|
| 2351 |
request_seed: int,
|
| 2352 |
+
reference_wav_path: str = "",
|
| 2353 |
) -> np.ndarray:
|
| 2354 |
"""Run one model call and return it without candidate qualification."""
|
| 2355 |
|
|
|
|
| 2381 |
"max_len": hard_stop_steps,
|
| 2382 |
"retry_badcase": False,
|
| 2383 |
}
|
| 2384 |
+
if reference_wav_path:
|
| 2385 |
+
kwargs["reference_wav_path"] = reference_wav_path
|
| 2386 |
if _NATIVE_STOP_POLICY:
|
| 2387 |
kwargs["stop_threshold"] = STOP_THRESHOLD
|
| 2388 |
kwargs["stop_consecutive"] = STOP_CONSECUTIVE
|
|
|
|
| 2423 |
|
| 2424 |
def _synthesize(
|
| 2425 |
text: str,
|
| 2426 |
+
centroid: torch.Tensor | None,
|
| 2427 |
*,
|
| 2428 |
cfg: float,
|
| 2429 |
steps: int,
|
|
|
|
| 2431 |
generation_audit: _GlyphCarrierGenerationAudit | None = None,
|
| 2432 |
request_seed: int | None = None,
|
| 2433 |
speaker_projector_scale: float = 1.0,
|
| 2434 |
+
reference_wav_path: str = "",
|
| 2435 |
) -> tuple[int, np.ndarray]:
|
| 2436 |
"""Latency-first single-pass synthesis with no output qualification."""
|
| 2437 |
|
|
|
|
| 2464 |
cfg=float(cfg),
|
| 2465 |
steps=int(steps),
|
| 2466 |
request_seed=seed,
|
| 2467 |
+
reference_wav_path=reference_wav_path,
|
| 2468 |
)
|
| 2469 |
for chunk in chunks
|
| 2470 |
)
|
|
|
|
| 3826 |
):
|
| 3827 |
if not reference_wav:
|
| 3828 |
raise gr.Error("請先錄音或上傳參考音檔。")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3829 |
return pcm16_audio_output(
|
| 3830 |
*_synthesize(
|
| 3831 |
text,
|
| 3832 |
+
None,
|
| 3833 |
cfg=cfg,
|
| 3834 |
steps=steps,
|
| 3835 |
speed=speed,
|
| 3836 |
request_seed=REFERENCE_GENERATION_SEED,
|
| 3837 |
+
reference_wav_path=reference_wav,
|
| 3838 |
)
|
| 3839 |
)
|
| 3840 |
|
|
|
|
| 3928 |
with gr.Tab("參考音色"):
|
| 3929 |
gr.Markdown(
|
| 3930 |
"建議錄製 6–12 秒、單一語者且背景乾淨的授權參考音檔;"
|
| 3931 |
+
"錄音會直接作為模型的 reference audio,不轉成 ECAPA 音色。"
|
| 3932 |
)
|
| 3933 |
with gr.Row():
|
| 3934 |
with gr.Column():
|
tests/test_latency_first_mode.py
CHANGED
|
@@ -203,28 +203,24 @@ def test_builtin_speaker_b_uses_the_pinned_checkpoint_embedding(tmp_path):
|
|
| 203 |
)
|
| 204 |
|
| 205 |
|
| 206 |
-
def
|
| 207 |
source = APP_PATH.read_text(encoding="utf-8")
|
| 208 |
reference_source = ast.get_source_segment(source, _function_node("tts_reference"))
|
|
|
|
|
|
|
| 209 |
|
| 210 |
-
assert "REFERENCE_WINDOW_SECONDS = 3.0" in source
|
| 211 |
-
assert "REFERENCE_HOP_SECONDS = 1.5" in source
|
| 212 |
-
assert "REFERENCE_MAX_WINDOWS = 4" in source
|
| 213 |
-
assert "REFERENCE_FULL_CLIP_MAX_SECONDS = 0.0" in source
|
| 214 |
-
assert "REFERENCE_CONDITIONING_SCALE = 1.25" in source
|
| 215 |
assert "REFERENCE_GENERATION_SEED = 1_337" in source
|
| 216 |
-
assert "REFERENCE_PROJECTOR_SCALE = 1.25" in source
|
| 217 |
assert reference_source is not None
|
| 218 |
-
assert "extract_windowed_speaker_embedding(" in reference_source
|
| 219 |
-
assert "
|
| 220 |
-
assert "
|
| 221 |
-
assert "hop_seconds=REFERENCE_HOP_SECONDS" in reference_source
|
| 222 |
-
assert "max_windows=REFERENCE_MAX_WINDOWS" in reference_source
|
| 223 |
-
assert "full_clip_max_seconds=REFERENCE_FULL_CLIP_MAX_SECONDS" in reference_source
|
| 224 |
-
assert "centroid = centroid * REFERENCE_CONDITIONING_SCALE" in reference_source
|
| 225 |
assert "request_seed=REFERENCE_GENERATION_SEED" in reference_source
|
| 226 |
-
assert "
|
| 227 |
assert "retry" not in reference_source
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
|
| 230 |
def test_reference_projector_gain_is_scoped_and_removed():
|
|
|
|
| 203 |
)
|
| 204 |
|
| 205 |
|
| 206 |
+
def test_reference_conditioning_uses_raw_reference_audio_once():
|
| 207 |
source = APP_PATH.read_text(encoding="utf-8")
|
| 208 |
reference_source = ast.get_source_segment(source, _function_node("tts_reference"))
|
| 209 |
+
fast_source = ast.get_source_segment(source, _function_node("_generate_fast_chunk"))
|
| 210 |
+
synthesize_source = ast.get_source_segment(source, _function_node("_synthesize"))
|
| 211 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
assert "REFERENCE_GENERATION_SEED = 1_337" in source
|
|
|
|
| 213 |
assert reference_source is not None
|
| 214 |
+
assert "extract_windowed_speaker_embedding(" not in reference_source
|
| 215 |
+
assert "_get_ecapa_encoder(" not in reference_source
|
| 216 |
+
assert "None," in reference_source
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
assert "request_seed=REFERENCE_GENERATION_SEED" in reference_source
|
| 218 |
+
assert "reference_wav_path=reference_wav" in reference_source
|
| 219 |
assert "retry" not in reference_source
|
| 220 |
+
assert fast_source is not None
|
| 221 |
+
assert 'kwargs["reference_wav_path"] = reference_wav_path' in fast_source
|
| 222 |
+
assert synthesize_source is not None
|
| 223 |
+
assert "reference_wav_path=reference_wav_path" in synthesize_source
|
| 224 |
|
| 225 |
|
| 226 |
def test_reference_projector_gain_is_scoped_and_removed():
|