Spaces:
Running on Zero
Running on Zero
File size: 15,984 Bytes
927c1a5 e66cd92 355336d 01153e3 927c1a5 e66cd92 927c1a5 a858c6e 927c1a5 e66cd92 927c1a5 e66cd92 927c1a5 e66cd92 927c1a5 e66cd92 927c1a5 e66cd92 927c1a5 e66cd92 927c1a5 a8231fb 7b111a1 927c1a5 5fc2aef e66cd92 927c1a5 e66cd92 927c1a5 01153e3 a858c6e 01153e3 a858c6e 01153e3 927c1a5 e66cd92 927c1a5 a8231fb 927c1a5 7b111a1 a8231fb 927c1a5 a8231fb e66cd92 a8231fb 355336d 927c1a5 355336d e66cd92 355336d a8231fb 927c1a5 a8231fb 7b111a1 927c1a5 a858c6e e66cd92 927c1a5 01153e3 7b111a1 a858c6e a8231fb a858c6e 01153e3 a858c6e 5e31159 a8231fb 5e31159 01153e3 a858c6e 927c1a5 01153e3 927c1a5 a8231fb 927c1a5 e66cd92 927c1a5 a8231fb 7b111a1 927c1a5 e66cd92 927c1a5 7b111a1 927c1a5 7b111a1 927c1a5 a8231fb 927c1a5 355336d 927c1a5 9a401f5 355336d 9a401f5 927c1a5 a8231fb 927c1a5 01153e3 927c1a5 01153e3 7b111a1 a8231fb a858c6e 927c1a5 7b111a1 927c1a5 01153e3 927c1a5 01153e3 927c1a5 01153e3 a8231fb 7afaa46 927c1a5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 | """BlueMagpie-TTS 線上試用 Space (Gradio + ZeroGPU)。
提供三種試用情境,並一律套用模型官方建議的生成參數:
1. 指定語者 —— 用模型內附、已取得授權的語者向量控制音色(李宏毅/女聲)
2. 聲音複製 —— 一段 3 秒以上的參考音檔直接走 reference_wav_path,免逐字稿
(checkpoint step_0006000 起正式支援,內部評測 CER 8.99%)
3. 長文逐句串流 —— 把長文切句,合成一句播一句,做出串流效果(每句含自動重試)
生成參數:優先讀模型發佈中繼資料 release_metadata.json 的
recommended_generation_defaults(若有);新版中繼資料未提供時,退回模型卡建議值
(cfg=2.0 / steps=10 / retry_badcase=True),確保 demo 永遠跟著模型走。
"""
import json
import os
import time
import numpy as np
import torch
import gradio as gr
from huggingface_hub import snapshot_download
from transformers import PreTrainedTokenizerFast
from bluemagpie import BlueMagpieModel
# --------------------------------------------------------------------------- #
# ZeroGPU:用 @spaces.GPU 動態取得 GPU;本機無 spaces 時退回 cuda/cpu 自動偵測。
# --------------------------------------------------------------------------- #
try:
import spaces
gpu = spaces.GPU(duration=120)
DEVICE = "cuda"
except ImportError:
def gpu(fn):
return fn
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
REPO_ID = "OpenFormosa/BlueMagpie-TTS"
# --------------------------------------------------------------------------- #
# 載入模型(啟動時做一次)
# --------------------------------------------------------------------------- #
print(f"[BlueMagpie] downloading model from {REPO_ID} ...")
MODEL_DIR = snapshot_download(REPO_ID)
tokenizer = PreTrainedTokenizerFast(
tokenizer_file=os.path.join(MODEL_DIR, "tokenizer.json")
)
print(f"[BlueMagpie] loading model on device={DEVICE} ...")
model = BlueMagpieModel.from_local(
MODEL_DIR, tokenizer=tokenizer, training=False, device=DEVICE
)
SR = model.sample_rate
# --------------------------------------------------------------------------- #
# 生成參數:優先讀模型發佈中繼資料(舊版才有 recommended_generation_defaults);
# 新版(step_0006000 起)未提供時,退回模型卡建議值。
# --------------------------------------------------------------------------- #
_GEN_KEYS = {
"cfg_value",
"inference_timesteps",
"max_len",
"min_len",
"retry_badcase",
"retry_badcase_max_times",
"retry_badcase_ratio_threshold",
}
_BEST_FALLBACK = {
"cfg_value": 2.0,
"inference_timesteps": 10,
"max_len": 2000,
"retry_badcase": True,
"retry_badcase_max_times": 3,
"retry_badcase_ratio_threshold": 6.0,
}
SPEAKER_ID = "hung_yi_lee"
CHECKPOINT = "?"
try:
with open(os.path.join(MODEL_DIR, "release_metadata.json"), encoding="utf-8") as f:
_meta = json.load(f)
CHECKPOINT = _meta.get("checkpoint", CHECKPOINT)
_rec = _meta.get("recommended_generation_defaults", {}) or {}
GEN_KWARGS = {k: v for k, v in _rec.items() if k in _GEN_KEYS} or dict(_BEST_FALLBACK)
SPEAKER_ID = _rec.get("speaker_id", SPEAKER_ID)
except Exception as e: # 任何讀取/解析問題都退回安全的建議值
print(f"[BlueMagpie] release_metadata.json unavailable ({e}); using fallback params")
GEN_KWARGS = dict(_BEST_FALLBACK)
print(f"[BlueMagpie] checkpoint={CHECKPOINT}, generation params: {GEN_KWARGS}")
# 介面上可調整的兩個生成參數;預設即官方建議值。
DEFAULT_CFG = float(GEN_KWARGS.get("cfg_value", 2.0))
DEFAULT_STEPS = int(GEN_KWARGS.get("inference_timesteps", 10))
def _gen_kwargs(cfg_value=None, inference_timesteps=None):
"""以官方建議參數為底,套用使用者在介面上調整的 cfg / 取樣步數。"""
kw = dict(GEN_KWARGS)
if cfg_value is not None:
kw["cfg_value"] = float(cfg_value)
if inference_timesteps is not None:
kw["inference_timesteps"] = int(inference_timesteps)
return kw
# 語者向量:載入多語者表(優先 speaker_centroids.pt),建成「顯示名稱 -> 向量」。
# 內附的李宏毅向量已取得本人授權;其餘為通用語者向量。
_DISPLAY = {"hung_yi_lee": "李宏毅", "female_voice": "女聲"}
def _load_speakers():
for fn in ("speaker_centroids.pt", "hung_yi_lee_speaker_centroids.pt"):
p = os.path.join(MODEL_DIR, "checkpoints", fn)
if os.path.exists(p):
blob = torch.load(p, map_location="cpu", weights_only=True)
return {_DISPLAY.get(sid, sid): c for sid, c in zip(blob["speaker_ids"], blob["centroids"])}
raise RuntimeError("no speaker centroid file found")
SPEAKERS = _load_speakers()
DEFAULT_SPEAKER = _DISPLAY.get(SPEAKER_ID, SPEAKER_ID)
if DEFAULT_SPEAKER not in SPEAKERS:
DEFAULT_SPEAKER = next(iter(SPEAKERS))
SPK_CENTROID = SPEAKERS[DEFAULT_SPEAKER] # 預設向量(長文串流沿用)
print(f"[BlueMagpie] ready. sample_rate={SR}, speakers={list(SPEAKERS)}")
def _audio_seconds(path):
"""回傳音檔長度(秒);讀不到時回 0。"""
import librosa
try:
return float(librosa.get_duration(path=path))
except Exception:
return 0.0
# 註:repo 的 bluemagpie.serving 加速引擎(torch.compile / 連續批次)是為「常駐
# 專用 GPU、追求吞吐」設計。在 ZeroGPU(GPU 逐請求序列化掛載)上實測後,單一
# 請求延遲沒有改善(warm ~7.0s vs eager ~6.2s),反而帶來首呼叫 ~50s 編譯成本與
# CUDA graph 反覆重編譯的尖峰,故此 demo 維持 eager。
def _to_numpy(audio: torch.Tensor):
return audio.squeeze().float().cpu().numpy()
def _pcm16(x: np.ndarray) -> np.ndarray:
"""float 波形 -> 16-bit PCM,供 Gradio 串流輸出(格式最穩定)。"""
return (np.clip(x, -1.0, 1.0) * 32767.0).astype(np.int16)
# 長文逐句串流:以句末標點/換行切句。逐句串流直到接近 ZeroGPU 單次 GPU 時間配額
# 為止(不是固定句數),盡量把整段長文串完;另設一個防濫用的硬上限。
_SENT_ENDERS = "。!?;!?…\n"
_GPU_BUDGET_S = 100 # @spaces.GPU(duration=120),留約 20s 緩衝給最後一句
_MAX_STREAM_SENTENCES = 80 # 防濫用硬上限(通常時間配額會先到)
def _split_sentences(text):
out, cur = [], []
for ch in text:
cur.append(ch)
if ch in _SENT_ENDERS:
s = "".join(cur).strip()
if s:
out.append(s)
cur = []
s = "".join(cur).strip()
if s:
out.append(s)
# 把落單的標點(如連續「!?」被拆出的單一字元)併回前一句
merged = []
for s in out:
if merged and len(s) <= 1:
merged[-1] += s
else:
merged.append(s)
return merged
# --------------------------------------------------------------------------- #
# 推論:三種模式都套用 GEN_KWARGS(官方建議參數)
# --------------------------------------------------------------------------- #
@gpu
def tts_speaker(text, speaker=DEFAULT_SPEAKER, cfg=DEFAULT_CFG, steps=DEFAULT_STEPS):
text = (text or "").strip()
if not text:
raise gr.Error("請先輸入要合成的文字。")
audio = model.generate(
target_text=text,
speaker_centroid=SPEAKERS.get(speaker, SPK_CENTROID),
**_gen_kwargs(cfg, steps),
)
return (SR, _to_numpy(audio))
@gpu
def _clone_gpu(text, ref_path, cfg, steps):
audio = model.generate(
target_text=text, reference_wav_path=ref_path, **_gen_kwargs(cfg, steps)
)
return (SR, _to_numpy(audio))
def tts_clone(text, recording, cfg=DEFAULT_CFG, steps=DEFAULT_STEPS):
"""聲音複製(免逐字稿):把參考音檔直接交給模型(reference_wav_path 路徑,
checkpoint step_0006000 起正式支援)。一段 3 秒以上的乾淨語音即可。"""
text = (text or "").strip()
if not text:
raise gr.Error("請先輸入要合成的文字。")
if not recording:
raise gr.Error("請先錄音或上傳參考音檔。")
dur = _audio_seconds(recording)
if 0 < dur < 3:
gr.Warning(f"參考音檔只有約 {dur:.0f} 秒,建議 3 秒以上會更穩定。")
return _clone_gpu(text, recording, cfg, steps)
@gpu
def tts_stream(text, speaker=DEFAULT_SPEAKER, cfg=DEFAULT_CFG, steps=DEFAULT_STEPS):
"""長文逐句串流(指定語者向量):把長文切成句子,逐句合成、合成一句就播一句。
逐句串流直到接近單次 GPU 時間配額(約 100s)為止,盡量把整段長文串完,而非固定
句數;真的超過才截斷並提示分批。每句都用一般 generate(含 retry_badcase)產生
完整音檔再 yield;句與句之間可能有短暫間隔(ZeroGPU 約 0.44x 即時所致)。
"""
text = (text or "").strip()
if not text:
raise gr.Error("請先輸入要合成的文字。")
sentences = _split_sentences(text)
if not sentences:
raise gr.Error("找不到可合成的句子。")
centroid = SPEAKERS.get(speaker, SPK_CENTROID)
total = len(sentences)
sentences = sentences[:_MAX_STREAM_SENTENCES]
gen_kw = _gen_kwargs(cfg, steps)
start = time.time()
done = 0
for sent in sentences:
# 至少先產出第一句;之後一旦逼近 GPU 時間配額就停,避免配額被回收而報錯
if done and time.time() - start > _GPU_BUDGET_S:
break
audio = model.generate(
target_text=sent, speaker_centroid=centroid, **gen_kw
)
yield (SR, _pcm16(_to_numpy(audio)))
done += 1
if done < total:
gr.Warning(
f"受單次 GPU 時間配額限制,本次串流前 {done} 句(共 {total} 句);"
"想念完整段可分批貼上。"
)
# --------------------------------------------------------------------------- #
# 介面
# --------------------------------------------------------------------------- #
EXAMPLE_TEXTS = [
"今天天氣真好,我們一起去散步吧。",
"這是 AI TTS code switching 測試,混合中英文也沒問題。",
"台灣藍鵲是一種叫聲響亮、辨識度很高的鳥。",
]
STREAM_EXAMPLES = [
"台灣藍鵲是台灣特有的鳥類,羽色以亮藍為主,尾羽修長。牠們常成群在樹林間活動,"
"叫聲響亮而容易辨認。今天天氣真好,我們一起出去走走,順便看看這些美麗的鳥吧!",
"歡迎使用這個示範。這是一段比較長的文字,會被切成好幾句。"
"系統會合成一句、播放一句,做出串流的效果。希望你會喜歡這個聲音!",
]
_PARAMS_NOTE = (
f"模型 checkpoint:`{CHECKPOINT}`,預設套用官方建議參數:"
f"`cfg_value={DEFAULT_CFG}`、"
f"`inference_timesteps={DEFAULT_STEPS}`、"
f"`retry_badcase={'on' if GEN_KWARGS.get('retry_badcase') else 'off'}`;"
f"可展開下方「⚙️ 生成參數」自行調整。"
)
HEADER = f"""
# 🐦⬛ BlueMagpie-TTS · 文字轉語音線上試用
**OpenFormosa Blue Magpie TTS** —— 針對**台灣華語**與**中英混合**打造的文字轉語音模型,輸出 48 kHz 語音;
支援**免逐字稿聲音複製**(內部評測:語者向量 CER 7.44%、參考音檔 CER 8.99%,較基準 11.45% 顯著降低)。
模型:[OpenFormosa/BlueMagpie-TTS](https://huggingface.co/OpenFormosa/BlueMagpie-TTS) ·
程式碼:[GitHub](https://github.com/OpenFormosa/BlueMagpie-TTS)
> {_PARAMS_NOTE}
"""
with gr.Blocks(title="BlueMagpie-TTS Demo", theme=gr.themes.Soft()) as demo:
gr.Markdown(HEADER)
with gr.Accordion("⚙️ 生成參數(預設為官方建議值,三個分頁共用)", open=False):
with gr.Row():
g_cfg = gr.Slider(
1.0, 4.0, value=DEFAULT_CFG, step=0.1,
label="cfg_value(引導強度)",
info="越大越貼合語者條件、但可能較不自然(建議 2.0)",
)
g_steps = gr.Slider(
4, 20, value=DEFAULT_STEPS, step=1,
label="inference_timesteps(取樣步數)",
info="越多通常品質越好、速度越慢(建議 10)",
)
with gr.Tab("指定語者"):
gr.Markdown(
"用模型內附的**語者向量**控制音色。**李宏毅**老師的向量已取得本人授權;"
"**女聲**為通用語者向量。"
)
with gr.Row():
with gr.Column():
s_spk = gr.Dropdown(
list(SPEAKERS), value=DEFAULT_SPEAKER, label="語者",
)
s_text = gr.Textbox(
label="要合成的文字", lines=3,
placeholder="輸入中文或中英混合的文字…",
)
s_btn = gr.Button("合成", variant="primary")
with gr.Column():
s_out = gr.Audio(label="合成結果", type="numpy")
gr.Examples(EXAMPLE_TEXTS, inputs=s_text, label="範例文字")
s_btn.click(tts_speaker, [s_text, s_spk, g_cfg, g_steps], s_out)
with gr.Tab("聲音複製"):
gr.Markdown(
"**聲音複製,免逐字稿**:錄或上傳一段 **3 秒以上**的乾淨語音,"
"直接交給模型模仿該語者的音色,**不需要**參考音檔的文字稿。\n\n"
"⚠️ **請只使用你已取得授權的聲音**,請勿在未經本人同意下複製他人聲音。"
)
with gr.Row():
with gr.Column():
c_text = gr.Textbox(
label="要合成的文字", lines=3,
placeholder="輸入中文或中英混合的文字…",
)
c_rec = gr.Audio(
label="錄音或上傳參考語音(3 秒以上)",
type="filepath", sources=["microphone", "upload"],
)
c_btn = gr.Button("複製音色合成", variant="primary")
with gr.Column():
c_out = gr.Audio(label="合成結果", type="numpy")
c_btn.click(tts_clone, [c_text, c_rec, g_cfg, g_steps], c_out)
with gr.Tab("長文逐句串流"):
gr.Markdown(
"**長文逐句串流**:把長文切成句子,合成一句就播一句,做出串流(邊聽邊等)效果。"
"第一句很快就能聽到,後面邊播邊合成。用**所選語者向量**。\n\n"
"> ZeroGPU 約 0.44x 即時,句與句之間可能有短暫間隔;每句都套用所選的生成參數(含自動重試)。"
"單次串流以 ZeroGPU 時間配額為限(約可串數十句),超長會自動截斷並提示分批。"
)
with gr.Row():
with gr.Column():
t_spk = gr.Dropdown(
list(SPEAKERS), value=DEFAULT_SPEAKER, label="語者",
)
t_text = gr.Textbox(
label="要合成的長文", lines=5,
placeholder="貼上一段多句的文字,會逐句合成、逐句播放…",
)
t_btn = gr.Button("逐句串流合成", variant="primary")
with gr.Column():
t_out = gr.Audio(label="逐句串流播放", streaming=True, autoplay=True)
gr.Examples(STREAM_EXAMPLES, inputs=t_text, label="長文範例")
t_btn.click(tts_stream, [t_text, t_spk, g_cfg, g_steps], t_out)
gr.Markdown(
"---\n"
"合成的語音僅供研究與評估展示用途,輸出可能不完美;正式使用前請人工檢視。"
" · Apache-2.0"
)
if __name__ == "__main__":
demo.queue().launch()
|