jacky3102 commited on
Commit
018cafa
·
verified ·
1 Parent(s): 325579d

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -356
app.py DELETED
@@ -1,356 +0,0 @@
1
- import os
2
- import io
3
- import gc
4
- import tempfile
5
- import torch
6
- import soundfile as sf
7
- import numpy as np
8
- import gradio as gr
9
- from fastapi import FastAPI, UploadFile, File, Form, HTTPException
10
- from fastapi.responses import StreamingResponse
11
- from fastapi.middleware.cors import CORSMiddleware
12
- from typing import Optional
13
- import uvicorn
14
-
15
- # ── 设备 & 精度 ───────────────────────────────────────────────────────────────
16
- DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
17
- DTYPE = torch.bfloat16
18
-
19
- print(f"[TTS] Device={DEVICE}, dtype={DTYPE}")
20
- from qwen_tts import Qwen3TTSModel
21
-
22
- # ── 启动只加载 Base ───────────────────────────────────────────────────────────
23
- print("[TTS] Loading Base model...")
24
- model_base = Qwen3TTSModel.from_pretrained(
25
- "Qwen/Qwen3-TTS-12Hz-1.7B-Base",
26
- device_map=DEVICE,
27
- dtype=DTYPE,
28
- )
29
- print("[TTS] Base model ready.")
30
- LANGUAGES = model_base.get_supported_languages()
31
-
32
- # ── CustomVoice 按需加载 ──────────────────────────────────────────────────────
33
- model_custom = None
34
-
35
- def _load_custom():
36
- global model_custom
37
- if model_custom is not None:
38
- return
39
- print("[TTS] Loading CustomVoice...")
40
- model_custom = Qwen3TTSModel.from_pretrained(
41
- "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
42
- device_map=DEVICE,
43
- dtype=DTYPE,
44
- )
45
- print("[TTS] CustomVoice ready.")
46
-
47
- def _unload_custom():
48
- global model_custom
49
- if model_custom is None:
50
- return
51
- print("[TTS] Unloading CustomVoice...")
52
- del model_custom
53
- model_custom = None
54
- gc.collect()
55
- if DEVICE == "cuda":
56
- torch.cuda.empty_cache()
57
- print("[TTS] CustomVoice unloaded.")
58
-
59
- # ── Speaker 列表(启动时获取一次)────────────────────────────────────────────
60
- print("[TTS] Fetching speaker list...")
61
- _load_custom()
62
- SPEAKERS = model_custom.get_supported_speakers()
63
- _unload_custom()
64
- print(f"[TTS] Speakers: {SPEAKERS}")
65
-
66
- # ── 全局 anchor 存储 role -> {sr, audio, text} ───────────────────────────────
67
- _anchors: dict = {}
68
-
69
- # ─────────────────────────────────────────────────────────────────────────────
70
- # 工具
71
- # ─────────────────────────────────────────────────────────────────────────────
72
-
73
- def _wav_bytes(audio: np.ndarray, sr: int) -> bytes:
74
- buf = io.BytesIO()
75
- sf.write(buf, audio, sr, format="WAV")
76
- buf.seek(0)
77
- return buf.read()
78
-
79
- def _wav_response(audio: np.ndarray, sr: int, name: str = "out.wav") -> StreamingResponse:
80
- return StreamingResponse(
81
- io.BytesIO(_wav_bytes(audio, sr)),
82
- media_type="audio/wav",
83
- headers={"Content-Disposition": f"attachment; filename={name}"},
84
- )
85
-
86
- def _tmp_wav(sr: int, audio: np.ndarray) -> str:
87
- with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
88
- sf.write(f.name, audio, sr)
89
- return f.name
90
-
91
- # ─────────────────────────────────────────────────────────────────────────────
92
- # FastAPI
93
- # ─────────────────────────────────────────────────────────────────────────────
94
-
95
- app_api = FastAPI(title="Qwen3-TTS", version="4.0.0")
96
- app_api.add_middleware(CORSMiddleware, allow_origins=["*"],
97
- allow_methods=["*"], allow_headers=["*"])
98
-
99
- @app_api.get("/health")
100
- def health():
101
- return {
102
- "status": "ok", "device": DEVICE,
103
- "custom_loaded": model_custom is not None,
104
- "anchors": list(_anchors.keys()),
105
- "languages": LANGUAGES,
106
- }
107
-
108
- # ── /batch_anchor ─────────────────────────────────────────────────────────────
109
- # 一次性建立所有角色 anchor,CustomVoice 只加载一次
110
- @app_api.post("/batch_anchor")
111
- async def batch_anchor(
112
- roles: str = Form(...), # JSON list
113
- speakers: str = Form(...), # JSON list
114
- texts: str = Form(...), # JSON list
115
- languages: str = Form(...), # JSON list
116
- instructs: str = Form(...), # JSON list
117
- ):
118
- import json, base64
119
- roles_ = json.loads(roles)
120
- speakers_ = json.loads(speakers)
121
- texts_ = json.loads(texts)
122
- languages_ = json.loads(languages)
123
- instructs_ = json.loads(instructs)
124
-
125
- if not (len(roles_) == len(speakers_) == len(texts_) == len(languages_) == len(instructs_)):
126
- raise HTTPException(400, "all lists must be same length")
127
-
128
- results = {}
129
- try:
130
- _load_custom()
131
- for role, spk, text, lang, inst in zip(
132
- roles_, speakers_, texts_, languages_, instructs_):
133
- inst = inst if inst and inst.strip() else None
134
- wavs, sr = model_custom.generate_custom_voice(
135
- text=text, language=lang, speaker=spk, instruct=inst)
136
- audio = wavs[0]
137
- _anchors[role] = {"sr": sr, "audio": audio, "text": text}
138
- wav_b64 = base64.b64encode(_wav_bytes(audio, sr)).decode()
139
- results[role] = {"sr": sr, "wav_b64": wav_b64}
140
- print(f"[batch_anchor] ✓ {role} ({spk})")
141
- finally:
142
- _unload_custom()
143
-
144
- return results
145
-
146
- # ── /tts (兼容旧版,单角色) ───────────────────────────────────────────────────
147
- @app_api.post("/tts")
148
- async def tts_single(
149
- text: str = Form(...),
150
- speaker: str = Form("Vivian"),
151
- language: str = Form("chinese"),
152
- instruct: str = Form(""),
153
- role: str = Form("_narrator"),
154
- set_anchor: bool = Form(False), # FIX: 仅当显式要求时才(重新)写入/覆盖 _anchors[role]
155
- ):
156
- if not text.strip():
157
- raise HTTPException(400, "text is empty")
158
- inst = instruct if instruct.strip() else None
159
- try:
160
- _load_custom()
161
- wavs, sr = model_custom.generate_custom_voice(
162
- text=text, language=language, speaker=speaker, instruct=inst)
163
- audio = wavs[0]
164
- # FIX: 之前这里无条件覆盖 _anchors[role],导致任何一次带 instruct 的
165
- # 情绪台词(speak_emotion / dialogue 里非 narrator 行)都会把已经建立好的
166
- # anchor 顶替掉,后续 /clone 就会用这句"带情绪"的音频当参考,造成音色跑偏。
167
- # 现在:只有显式传 set_anchor=true(建立/重建锚点时)才写入。
168
- if set_anchor or role not in _anchors:
169
- _anchors[role] = {"sr": sr, "audio": audio, "text": text}
170
- return _wav_response(audio, sr, "speech.wav")
171
- except Exception as e:
172
- raise HTTPException(500, str(e))
173
- finally:
174
- _unload_custom()
175
-
176
- # ── /clone ────────────────────────────────────────────────────────────────────
177
- # ⚠️ 修改:支持两种方式 - 使用服务端 anchor 或直接上传参考音频
178
- @app_api.post("/clone")
179
- async def clone(
180
- text: str = Form(...),
181
- language: str = Form("chinese"),
182
- role: str = Form("_narrator"),
183
- reference_text: str = Form(""),
184
- reference_audio: UploadFile = File(None),
185
- ):
186
- if not text.strip():
187
- raise HTTPException(400, "text is empty")
188
-
189
- tmp_path = None
190
- try:
191
- # FIX: 之前这里"服务端 anchor"永远优先,导致只要 role 在 _anchors 里
192
- # (比如用 create 建立过),/clone 就完全无视客户端传来的 reference_audio/
193
- # reference_text —— workflow.sh 里的"滚动参考"机制形同虚设,而且一旦
194
- # _anchors[role] 被别的调用(如情绪台词)覆盖,全程都会跟着跑偏。
195
- # 现在改为:客户端显式传了 reference_audio + reference_text 就优先用它
196
- # (滚动参考 / 强制回锚都通过这个参数控制),客户端没传时才退回服务端 anchor。
197
- if reference_audio is not None and reference_text.strip():
198
- suffix = os.path.splitext(reference_audio.filename)[-1] or ".wav"
199
- with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as f:
200
- f.write(await reference_audio.read())
201
- tmp_path = f.name
202
- ref_text = reference_text
203
- print(f"[clone] 使用客户端参考音频: {reference_audio.filename}")
204
-
205
- elif role in _anchors:
206
- anchor = _anchors[role]
207
- tmp_path = _tmp_wav(anchor["sr"], anchor["audio"])
208
- ref_text = anchor["text"]
209
- print(f"[clone] 使用服务端 anchor: {role}")
210
-
211
- else:
212
- raise HTTPException(
213
- 400, f"No anchor for role='{role}'. Call /batch_anchor or /tts first, or provide reference_audio and reference_text.")
214
-
215
- # 生成克隆
216
- wavs, sr = model_base.generate_voice_clone(
217
- text=text, ref_audio=tmp_path,
218
- ref_text=ref_text, language=language)
219
- return _wav_response(wavs[0], sr, "clone.wav")
220
-
221
- except HTTPException:
222
- raise
223
- except Exception as e:
224
- raise HTTPException(500, str(e))
225
- finally:
226
- if tmp_path and os.path.exists(tmp_path):
227
- os.unlink(tmp_path)
228
-
229
- # ── /clear_anchors ────────────────────────────────────────────────────────────
230
- @app_api.post("/clear_anchors")
231
- def clear_anchors():
232
- _anchors.clear()
233
- return {"status": "cleared"}
234
-
235
- # ─────────────────────────────────────────────────────────────────────────────
236
- # Gradio UI
237
- # ─────────────────────────────────────────────────────────────────────────────
238
-
239
- def ui_first(text, language, speaker, instruct):
240
- if not text.strip():
241
- return None, "❌ 请输入文本"
242
- try:
243
- _load_custom()
244
- wavs, sr = model_custom.generate_custom_voice(
245
- text=text, language=language, speaker=speaker,
246
- instruct=instruct or None)
247
- audio = wavs[0]
248
- _anchors["_ui"] = {"sr": sr, "audio": audio, "text": text}
249
- return (sr, audio), f"✅ 生成完毕,声音已锁定({speaker})"
250
- except Exception as e:
251
- return None, f"❌ {e}"
252
- finally:
253
- _unload_custom()
254
-
255
- def ui_clone(text, language):
256
- if not text.strip():
257
- return None, "❌ 请输入文本"
258
- if "_ui" not in _anchors:
259
- return None, "❌ 请先在「第一段」Tab 生成并锁定声音"
260
- anchor = _anchors["_ui"]
261
- tmp = _tmp_wav(anchor["sr"], anchor["audio"])
262
- try:
263
- wavs, sr = model_base.generate_voice_clone(
264
- text=text, ref_audio=tmp,
265
- ref_text=anchor["text"], language=language)
266
- return (sr, wavs[0]), "✅ 生成成功"
267
- except Exception as e:
268
- return None, f"❌ {e}"
269
- finally:
270
- os.unlink(tmp)
271
-
272
- def ui_manual_clone(text, language, ref_audio, ref_text):
273
- if not text.strip() or ref_audio is None:
274
- return None, "❌ 请填写文本并上传参考音频"
275
- if not ref_text.strip():
276
- return None, "❌ 参考文本为必填项"
277
- sr_r, audio_np = ref_audio
278
- tmp = _tmp_wav(sr_r, audio_np)
279
- try:
280
- wavs, sr = model_base.generate_voice_clone(
281
- text=text, ref_audio=tmp,
282
- ref_text=ref_text, language=language)
283
- return (sr, wavs[0]), "✅ 克隆成功"
284
- except Exception as e:
285
- return None, f"❌ {e}"
286
- finally:
287
- os.unlink(tmp)
288
-
289
- def ui_unlock():
290
- _anchors.pop("_ui", None)
291
- return "🔓 已解锁"
292
-
293
- with gr.Blocks(title="Qwen3-TTS", theme=gr.themes.Soft()) as gradio_ui:
294
- gr.Markdown(
295
- "# 🎙️ Qwen3-TTS 1.7B\n"
296
- "**工作流**:第一段建立 anchor → 后续全部 clone 保持声音一致\n\n"
297
- "> CustomVoice 只在生成第一段时临时加载,用完立刻释放。"
298
- )
299
- with gr.Tabs():
300
- with gr.Tab("① 第一段"):
301
- with gr.Row():
302
- with gr.Column():
303
- f_text = gr.Textbox(label="第一段文本", lines=4)
304
- f_lang = gr.Dropdown(LANGUAGES, value="chinese", label="语言")
305
- f_speaker = gr.Dropdown(SPEAKERS, value=SPEAKERS[0], label="Speaker")
306
- f_inst = gr.Textbox(label="风格指令(可选)",
307
- placeholder="例:用沉稳的播音腔")
308
- with gr.Row():
309
- f_btn = gr.Button("🎵 生成并锁定", variant="primary")
310
- f_unlock = gr.Button("🔓 解锁", variant="secondary")
311
- with gr.Column():
312
- f_audio = gr.Audio(label="输出", type="numpy")
313
- f_status = gr.Textbox(label="状态", interactive=False)
314
- f_btn.click(ui_first, [f_text, f_lang, f_speaker, f_inst], [f_audio, f_status])
315
- f_unlock.click(ui_unlock, [], f_status)
316
-
317
- with gr.Tab("② 后续段落"):
318
- gr.Markdown("使用锁定声音,每次只需输入文本,声音完全一致。")
319
- with gr.Row():
320
- with gr.Column():
321
- b_text = gr.Textbox(label="目标文本", lines=5)
322
- b_lang = gr.Dropdown(LANGUAGES, value="chinese", label="语言")
323
- b_btn = gr.Button("🎵 生成", variant="primary")
324
- with gr.Column():
325
- b_audio = gr.Audio(label="输出", type="numpy")
326
- b_status = gr.Textbox(label="状态", interactive=False)
327
- b_btn.click(ui_clone, [b_text, b_lang], [b_audio, b_status])
328
-
329
- with gr.Tab("③ 手动克隆"):
330
- with gr.Row():
331
- with gr.Column():
332
- c_ref = gr.Audio(label="参考音频(3~10 秒)", type="numpy")
333
- c_rtext = gr.Textbox(label="参考音频对应文本(必填)", lines=2)
334
- c_text = gr.Textbox(label="目标文本", lines=4)
335
- c_lang = gr.Dropdown(LANGUAGES, value="chinese", label="语言")
336
- c_btn = gr.Button("🎵 生成", variant="primary")
337
- with gr.Column():
338
- c_audio = gr.Audio(label="输出", type="numpy")
339
- c_status = gr.Textbox(label="状态", interactive=False)
340
- c_btn.click(ui_manual_clone,
341
- [c_text, c_lang, c_ref, c_rtext],
342
- [c_audio, c_status])
343
-
344
- gr.Markdown(
345
- "---\n### API\n"
346
- "- `POST /batch_anchor` — 批量建立所有角色 anchor(CustomVoice 只加载一次)\n"
347
- "- `POST /tts` — 单角色建立 anchor(兼容旧版)\n"
348
- "- `POST /clone` — 用 anchor 或参考音频克隆生成\n"
349
- "- `POST /clear_anchors` — 清除所有 anchor\n"
350
- "- `GET /health` — 状态\n"
351
- )
352
-
353
- final_app = gr.mount_gradio_app(app_api, gradio_ui, path="/")
354
-
355
- if __name__ == "__main__":
356
- uvicorn.run(final_app, host="0.0.0.0", port=7860)