voidful commited on
Commit
d650c1b
·
verified ·
1 Parent(s): 2be32f1

Use raw reference audio for voice cloning

Browse files
Files changed (3) hide show
  1. README.md +1 -1
  2. app.py +10 -31
  3. 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
- - **參考音色**:以 3 秒窗、1.5 秒 hop(最多 4 個窗)平均成單一 speaker embedding,固定 centroid/projector scale 1.25 與 seed 1337;每個文字分段仍只生成一次。
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
- centroid,
3854
  cfg=cfg,
3855
  steps=steps,
3856
  speed=speed,
3857
  request_seed=REFERENCE_GENERATION_SEED,
3858
- speaker_projector_scale=REFERENCE_PROJECTOR_SCALE,
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
- "至少需要 3 秒。"
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 test_reference_conditioning_uses_one_fixed_embedding():
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 "min_duration_seconds=REFERENCE_MIN_DURATION_SECONDS" in reference_source
220
- assert "window_seconds=REFERENCE_WINDOW_SECONDS" in reference_source
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 "speaker_projector_scale=REFERENCE_PROJECTOR_SCALE" in reference_source
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():