voidful commited on
Commit
2fa9373
·
verified ·
1 Parent(s): 95004fc

Automatically handle long text in both voice modes

Browse files
README.md CHANGED
@@ -20,9 +20,10 @@ models:
20
 
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 固定為
28
  `6f7cab914a1e27c56b504ec663c0144dc25cc0a3`,預設 CFG 為 3.0,NFE 為 10。
 
20
 
21
  ## 模式
22
 
23
+ - **內建語者**:預設為內建語者 A;Speaker B 使用模型庫 `checkpoints/speaker_b_embedding.pt`,固定 seed 56789 與 projector scale 1.125。長文會自動以最多 80 個字元分段。
24
+ - **參考音色**:錄音直接送入模型原生的 raw reference-audio conditioning,不經 ECAPA centroid;固定 seed 1337。長同樣自動分段。
25
+
26
+ 兩種模式的每個文字分段都只生成一次,分段間加入短靜音串接;不需要切換獨立長文模式。
27
 
28
  TTS model snapshot 固定為
29
  `6f7cab914a1e27c56b504ec663c0144dc25cc0a3`,預設 CFG 為 3.0,NFE 為 10。
app.py CHANGED
@@ -24,18 +24,39 @@ os.environ["MAMBA_DETERMINISTIC"] = "0"
24
 
25
  INTERACTIVE_GPU_LEASE_SECONDS = 60
26
  LONGFORM_GPU_LEASE_SECONDS = 120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  try:
29
  import spaces
30
 
31
- interactive_gpu = spaces.GPU(duration=INTERACTIVE_GPU_LEASE_SECONDS)
32
- longform_gpu = spaces.GPU(duration=LONGFORM_GPU_LEASE_SECONDS)
33
  USING_ZERO_GPU = True
34
  except ImportError:
35
- def interactive_gpu(function):
36
  return function
37
 
38
- longform_gpu = interactive_gpu
39
  USING_ZERO_GPU = False
40
 
41
  import gradio as gr
@@ -189,7 +210,7 @@ MIN_ENDPOINT_CUE_UNITS = 6
189
  SHORT_TEXT_CFG_MIN = 3.0
190
  SHORT_TEXT_CFG_UNITS = 6
191
  NETWORK_TEXT_CFG_MIN = 3.0
192
- CHUNK_CHARS = 80
193
  ONSET_CLAUSE_SEARCH_CHARS = 0
194
  MIN_CHUNK_CHARS = 12
195
  QUALITY_MEDIUM_TEXT_MAX_UNITS = 48
@@ -3789,7 +3810,7 @@ def _maybe_synthesize_glyph_speaker_request(
3789
  ) from error
3790
 
3791
 
3792
- @interactive_gpu
3793
  @timed_latency_request
3794
  def tts_speaker(
3795
  text: str,
@@ -3815,7 +3836,7 @@ def tts_speaker(
3815
  )
3816
 
3817
 
3818
- @interactive_gpu
3819
  @timed_latency_request
3820
  def tts_reference(
3821
  text: str,
@@ -3838,46 +3859,14 @@ def tts_reference(
3838
  )
3839
  )
3840
 
3841
-
3842
- @longform_gpu
3843
- @timed_latency_request
3844
- def tts_longform(
3845
- text: str,
3846
- speaker: str = DEFAULT_SPEAKER,
3847
- cfg: float = DEFAULT_CFG,
3848
- steps: int = DEFAULT_STEPS,
3849
- speed: float = 1.0,
3850
- ):
3851
- return pcm16_audio_output(
3852
- *_synthesize(
3853
- text,
3854
- SPEAKERS.get(speaker, DEFAULT_CENTROID),
3855
- cfg=cfg,
3856
- steps=steps,
3857
- speed=speed,
3858
- request_seed=SPEAKER_GENERATION_SEED,
3859
- speaker_projector_scale=(
3860
- SPEAKER_B_PROJECTOR_SCALE
3861
- if speaker == SPEAKER_B_LABEL
3862
- else 1.0
3863
- ),
3864
- )
3865
- )
3866
-
3867
-
3868
  EXAMPLE_TEXTS = [
3869
  "今天天氣真好,我們一起去散步吧。",
3870
  "我要吃蚵仔煎,然後去丟垃圾。",
3871
  "這學期的成績包括研究報告和期末考。",
3872
  "這是 AI TTS code switching 測試,混合中英文也沒問題。",
3873
  "注音符號測試:ㄅ、ㄆ、ㄇ、ㄈ。",
3874
- ]
3875
-
3876
- LONGFORM_EXAMPLES = [
3877
  "今天的會議會先整理目前進度,再確認下一階段的工作。遇到需要討論的項目時,"
3878
  "請先記下問題,等報告結束後再一起處理。最後,我們會確認負責人和預計完成時間。",
3879
- "歡迎收聽今天的內容。第一段會介紹背景,第二段整理實際案例,最後一段則說明後續安排。"
3880
- "如果中途聽到英文術語,不必擔心,我們會用中文補充它的意思。",
3881
  ]
3882
 
3883
 
@@ -3947,25 +3936,6 @@ with gr.Blocks(title="BlueMagpie-TTS Demo", theme=gr.themes.Soft()) as demo:
3947
  reference_output,
3948
  )
3949
 
3950
- with gr.Tab("長文"):
3951
- with gr.Row():
3952
- with gr.Column():
3953
- longform_speaker = gr.Dropdown(list(SPEAKERS), value=DEFAULT_SPEAKER, label="語者")
3954
- longform_text = gr.Textbox(
3955
- label="長文(自動分段)",
3956
- lines=8,
3957
- max_lines=12,
3958
- )
3959
- longform_button = gr.Button("合成完整長文", variant="primary")
3960
- with gr.Column():
3961
- longform_output = gr.Audio(label="合成結果", type="numpy")
3962
- gr.Examples(LONGFORM_EXAMPLES, inputs=longform_text, label="長文範例")
3963
- longform_button.click(
3964
- tts_longform,
3965
- [longform_text, longform_speaker, cfg_input, steps_input, speed_input],
3966
- longform_output,
3967
- )
3968
-
3969
  gr.Markdown(
3970
  "合成語音僅供研究與評估展示;正式使用前請人工檢視。 "
3971
  "[模型](https://huggingface.co/OpenFormosa/BlueMagpie-TTS) · "
 
24
 
25
  INTERACTIVE_GPU_LEASE_SECONDS = 60
26
  LONGFORM_GPU_LEASE_SECONDS = 120
27
+ AUTO_LONG_TEXT_CHARS = 80
28
+
29
+
30
+ def _gpu_lease_seconds(text: object) -> int:
31
+ """Reserve the long-form lease only when automatic chunking needs it."""
32
+
33
+ return (
34
+ LONGFORM_GPU_LEASE_SECONDS
35
+ if len(str(text)) > AUTO_LONG_TEXT_CHARS
36
+ else INTERACTIVE_GPU_LEASE_SECONDS
37
+ )
38
+
39
+
40
+ def _speaker_gpu_duration(text, speaker, cfg, steps, speed) -> int:
41
+ del speaker, cfg, steps, speed
42
+ return _gpu_lease_seconds(text)
43
+
44
+
45
+ def _reference_gpu_duration(text, reference_wav, cfg, steps, speed) -> int:
46
+ del reference_wav, cfg, steps, speed
47
+ return _gpu_lease_seconds(text)
48
 
49
  try:
50
  import spaces
51
 
52
+ speaker_gpu = spaces.GPU(duration=_speaker_gpu_duration)
53
+ reference_gpu = spaces.GPU(duration=_reference_gpu_duration)
54
  USING_ZERO_GPU = True
55
  except ImportError:
56
+ def speaker_gpu(function):
57
  return function
58
 
59
+ reference_gpu = speaker_gpu
60
  USING_ZERO_GPU = False
61
 
62
  import gradio as gr
 
210
  SHORT_TEXT_CFG_MIN = 3.0
211
  SHORT_TEXT_CFG_UNITS = 6
212
  NETWORK_TEXT_CFG_MIN = 3.0
213
+ CHUNK_CHARS = AUTO_LONG_TEXT_CHARS
214
  ONSET_CLAUSE_SEARCH_CHARS = 0
215
  MIN_CHUNK_CHARS = 12
216
  QUALITY_MEDIUM_TEXT_MAX_UNITS = 48
 
3810
  ) from error
3811
 
3812
 
3813
+ @speaker_gpu
3814
  @timed_latency_request
3815
  def tts_speaker(
3816
  text: str,
 
3836
  )
3837
 
3838
 
3839
+ @reference_gpu
3840
  @timed_latency_request
3841
  def tts_reference(
3842
  text: str,
 
3859
  )
3860
  )
3861
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3862
  EXAMPLE_TEXTS = [
3863
  "今天天氣真好,我們一起去散步吧。",
3864
  "我要吃蚵仔煎,然後去丟垃圾。",
3865
  "這學期的成績包括研究報告和期末考。",
3866
  "這是 AI TTS code switching 測試,混合中英文也沒問題。",
3867
  "注音符號測試:ㄅ、ㄆ、ㄇ、ㄈ。",
 
 
 
3868
  "今天的會議會先整理目前進度,再確認下一階段的工作。遇到需要討論的項目時,"
3869
  "請先記下問題,等報告結束後再一起處理。最後,我們會確認負責人和預計完成時間。",
 
 
3870
  ]
3871
 
3872
 
 
3936
  reference_output,
3937
  )
3938
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3939
  gr.Markdown(
3940
  "合成語音僅供研究與評估展示;正式使用前請人工檢視。 "
3941
  "[模型](https://huggingface.co/OpenFormosa/BlueMagpie-TTS) · "
tests/test_latency_first_mode.py CHANGED
@@ -88,7 +88,7 @@ def test_public_hot_path_contains_no_quality_gate_or_retry_cascade():
88
  "_generate_fast_chunk",
89
  "_assemble_fast_audio",
90
  "tts_speaker",
91
- "tts_longform",
92
  ):
93
  called = {
94
  node.func.id
@@ -129,7 +129,6 @@ def test_builtin_speaker_a_is_the_default():
129
  source = APP_PATH.read_text(encoding="utf-8")
130
  load_speakers = ast.get_source_segment(source, _function_node("_load_speakers"))
131
  speaker_source = ast.get_source_segment(source, _function_node("tts_speaker"))
132
- longform_source = ast.get_source_segment(source, _function_node("tts_longform"))
133
 
134
  assert load_speakers is not None
135
  assert "default_index = 0" in load_speakers
@@ -145,10 +144,23 @@ def test_builtin_speaker_a_is_the_default():
145
  assert "request_seed=SPEAKER_GENERATION_SEED" in speaker_source
146
  assert "speaker_projector_scale=(" in speaker_source
147
  assert "if speaker == SPEAKER_B_LABEL" in speaker_source
148
- assert longform_source is not None
149
- assert "request_seed=SPEAKER_GENERATION_SEED" in longform_source
150
- assert "speaker_projector_scale=(" in longform_source
151
- assert "if speaker == SPEAKER_B_LABEL" in longform_source
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
 
154
  def test_builtin_speaker_b_uses_the_pinned_checkpoint_embedding(tmp_path):
 
88
  "_generate_fast_chunk",
89
  "_assemble_fast_audio",
90
  "tts_speaker",
91
+ "tts_reference",
92
  ):
93
  called = {
94
  node.func.id
 
129
  source = APP_PATH.read_text(encoding="utf-8")
130
  load_speakers = ast.get_source_segment(source, _function_node("_load_speakers"))
131
  speaker_source = ast.get_source_segment(source, _function_node("tts_speaker"))
 
132
 
133
  assert load_speakers is not None
134
  assert "default_index = 0" in load_speakers
 
144
  assert "request_seed=SPEAKER_GENERATION_SEED" in speaker_source
145
  assert "speaker_projector_scale=(" in speaker_source
146
  assert "if speaker == SPEAKER_B_LABEL" in speaker_source
147
+
148
+
149
+ def test_speaker_and_reference_modes_automatically_reserve_long_text_lease():
150
+ source = APP_PATH.read_text(encoding="utf-8")
151
+ lease = _isolated_function(
152
+ "_gpu_lease_seconds",
153
+ {
154
+ "AUTO_LONG_TEXT_CHARS": 80,
155
+ "INTERACTIVE_GPU_LEASE_SECONDS": 60,
156
+ "LONGFORM_GPU_LEASE_SECONDS": 120,
157
+ },
158
+ )
159
+
160
+ assert lease("甲" * 80) == 60
161
+ assert lease("甲" * 81) == 120
162
+ assert "def tts_longform(" not in source
163
+ assert 'with gr.Tab("長文")' not in source
164
 
165
 
166
  def test_builtin_speaker_b_uses_the_pinned_checkpoint_embedding(tmp_path):
tests/test_latency_timing.py CHANGED
@@ -204,9 +204,8 @@ def test_space_wires_every_requested_latency_stage_without_gate_changes():
204
  assert _decorator_stage(echo_functions["evaluate_echo_smearing"]) == "echo"
205
 
206
  expected_gpu_decorators = {
207
- "tts_speaker": "interactive_gpu",
208
- "tts_reference": "interactive_gpu",
209
- "tts_longform": "longform_gpu",
210
  }
211
  for name, gpu_decorator in expected_gpu_decorators.items():
212
  decorators = {
 
204
  assert _decorator_stage(echo_functions["evaluate_echo_smearing"]) == "echo"
205
 
206
  expected_gpu_decorators = {
207
+ "tts_speaker": "speaker_gpu",
208
+ "tts_reference": "reference_gpu",
 
209
  }
210
  for name, gpu_decorator in expected_gpu_decorators.items():
211
  decorators = {
tests/test_release_pins.py CHANGED
@@ -998,7 +998,7 @@ def test_internal_synthesize_accepts_only_a_keyword_seed_while_ui_stays_unchange
998
  assert synthesize.args.kw_defaults[-1].value is None
999
  assert "request_seed = resolve_request_seed(request_seed, secrets.randbelow)" in source
1000
  assert "run_coverage_adaptive_cascade(\n chunks,\n request_seed," in source
1001
- for wrapper_name in ("tts_speaker", "tts_reference", "tts_longform"):
1002
  wrapper = functions[wrapper_name]
1003
  arguments = wrapper.args.args + wrapper.args.kwonlyargs
1004
  assert "request_seed" not in [argument.arg for argument in arguments]
@@ -1155,7 +1155,7 @@ def test_public_tts_wrappers_serialize_the_same_pcm16_grid_used_for_verification
1155
  assert "return pcm16_verification_waveform(waveform)" in assemble_source
1156
  assert "pcm16_audio_output" not in synthesize_source
1157
  assert "return SR, waveform" in synthesize_source
1158
- for wrapper_name in ("tts_speaker", "tts_reference", "tts_longform"):
1159
  wrapper_source = ast.get_source_segment(source, functions[wrapper_name])
1160
  assert wrapper_source is not None
1161
  assert "return pcm16_audio_output(" in wrapper_source
@@ -1421,22 +1421,23 @@ def test_space_disables_experimental_gradio_ssr():
1421
  )
1422
 
1423
 
1424
- def test_space_uses_shorter_interactive_zero_gpu_lease():
1425
  source = (ROOT / "app.py").read_text(encoding="utf-8")
1426
 
1427
  assert "INTERACTIVE_GPU_LEASE_SECONDS = 60" in source
1428
  assert "LONGFORM_GPU_LEASE_SECONDS = 120" in source
 
1429
  assert (
1430
- "interactive_gpu = spaces.GPU("
1431
- "duration=INTERACTIVE_GPU_LEASE_SECONDS)"
1432
  in source
1433
  )
1434
  assert (
1435
- "longform_gpu = spaces.GPU(duration=LONGFORM_GPU_LEASE_SECONDS)"
1436
  in source
1437
  )
1438
- assert source.count("@interactive_gpu") == 2
1439
- assert source.count("@longform_gpu") == 1
 
1440
 
1441
 
1442
  def test_network_fragment_relaxation_is_range_bound_and_local_only():
 
998
  assert synthesize.args.kw_defaults[-1].value is None
999
  assert "request_seed = resolve_request_seed(request_seed, secrets.randbelow)" in source
1000
  assert "run_coverage_adaptive_cascade(\n chunks,\n request_seed," in source
1001
+ for wrapper_name in ("tts_speaker", "tts_reference"):
1002
  wrapper = functions[wrapper_name]
1003
  arguments = wrapper.args.args + wrapper.args.kwonlyargs
1004
  assert "request_seed" not in [argument.arg for argument in arguments]
 
1155
  assert "return pcm16_verification_waveform(waveform)" in assemble_source
1156
  assert "pcm16_audio_output" not in synthesize_source
1157
  assert "return SR, waveform" in synthesize_source
1158
+ for wrapper_name in ("tts_speaker", "tts_reference"):
1159
  wrapper_source = ast.get_source_segment(source, functions[wrapper_name])
1160
  assert wrapper_source is not None
1161
  assert "return pcm16_audio_output(" in wrapper_source
 
1421
  )
1422
 
1423
 
1424
+ def test_space_automatically_selects_zero_gpu_lease_from_text_length():
1425
  source = (ROOT / "app.py").read_text(encoding="utf-8")
1426
 
1427
  assert "INTERACTIVE_GPU_LEASE_SECONDS = 60" in source
1428
  assert "LONGFORM_GPU_LEASE_SECONDS = 120" in source
1429
+ assert "AUTO_LONG_TEXT_CHARS = 80" in source
1430
  assert (
1431
+ "speaker_gpu = spaces.GPU(duration=_speaker_gpu_duration)"
 
1432
  in source
1433
  )
1434
  assert (
1435
+ "reference_gpu = spaces.GPU(duration=_reference_gpu_duration)"
1436
  in source
1437
  )
1438
+ assert source.count("@speaker_gpu") == 1
1439
+ assert source.count("@reference_gpu") == 1
1440
+ assert "def tts_longform(" not in source
1441
 
1442
 
1443
  def test_network_fragment_relaxation_is_range_bound_and_local_only():