Spaces:
Running on Zero
Running on Zero
Automatically handle long text in both voice modes
Browse files- README.md +4 -3
- app.py +28 -58
- tests/test_latency_first_mode.py +18 -6
- tests/test_latency_timing.py +2 -3
- tests/test_release_pins.py +9 -8
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 |
-
|
|
|
|
| 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 |
-
|
| 32 |
-
|
| 33 |
USING_ZERO_GPU = True
|
| 34 |
except ImportError:
|
| 35 |
-
def
|
| 36 |
return function
|
| 37 |
|
| 38 |
-
|
| 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 =
|
| 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 |
-
@
|
| 3793 |
@timed_latency_request
|
| 3794 |
def tts_speaker(
|
| 3795 |
text: str,
|
|
@@ -3815,7 +3836,7 @@ def tts_speaker(
|
|
| 3815 |
)
|
| 3816 |
|
| 3817 |
|
| 3818 |
-
@
|
| 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 |
-
"
|
| 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 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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": "
|
| 208 |
-
"tts_reference": "
|
| 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"
|
| 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"
|
| 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
|
| 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 |
-
"
|
| 1431 |
-
"duration=INTERACTIVE_GPU_LEASE_SECONDS)"
|
| 1432 |
in source
|
| 1433 |
)
|
| 1434 |
assert (
|
| 1435 |
-
"
|
| 1436 |
in source
|
| 1437 |
)
|
| 1438 |
-
assert source.count("@
|
| 1439 |
-
assert source.count("@
|
|
|
|
| 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():
|