Commit ·
b39ef30
1
Parent(s): 53b61f6
Add ASR diagnostic workflow and deployment guide
Browse files- app.py +100 -1
- docs/hf-space-comfyui-step-by-step.zh.md +372 -0
- docs/work-log.md +84 -0
- scripts/bootstrap_comfy.py +17 -0
app.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import json
|
| 4 |
import math
|
|
|
|
| 5 |
import subprocess
|
| 6 |
import sys
|
| 7 |
import time
|
|
@@ -196,6 +197,17 @@ def history_summary(history: dict[str, Any]) -> list[str]:
|
|
| 196 |
if output_files:
|
| 197 |
lines.append("outputs:")
|
| 198 |
lines.extend(output_files)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
return lines
|
| 200 |
|
| 201 |
|
|
@@ -269,6 +281,66 @@ def voxcpm_tts_workflow(prefix: str) -> dict[str, Any]:
|
|
| 269 |
}
|
| 270 |
|
| 271 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
def prepare_runtime() -> str:
|
| 273 |
global PREPARE_PROCESS
|
| 274 |
|
|
@@ -278,7 +350,7 @@ def prepare_runtime() -> str:
|
|
| 278 |
return "\n".join(lines)
|
| 279 |
BOOTSTRAP_LOG.parent.mkdir(parents=True, exist_ok=True)
|
| 280 |
log = BOOTSTRAP_LOG.open("ab")
|
| 281 |
-
command = [sys.executable, str(ROOT / "scripts" / "bootstrap_comfy.py")]
|
| 282 |
PREPARE_PROCESS = subprocess.Popen(
|
| 283 |
command,
|
| 284 |
cwd=ROOT,
|
|
@@ -379,8 +451,33 @@ def voxcpm_tts_gpu_test() -> str:
|
|
| 379 |
return "\n".join(lines)
|
| 380 |
|
| 381 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
with gr.Blocks(title="VoiceGate") as demo:
|
| 383 |
gr.Markdown("# VoiceGate")
|
|
|
|
| 384 |
with gr.Row():
|
| 385 |
prepare_run = gr.Button("Prepare")
|
| 386 |
prepare_status_run = gr.Button("Prepare Status")
|
|
@@ -388,6 +485,7 @@ with gr.Blocks(title="VoiceGate") as demo:
|
|
| 388 |
comfy_run = gr.Button("ComfyUI")
|
| 389 |
melband_run = gr.Button("MelBand")
|
| 390 |
voxcpm_run = gr.Button("VoxCPM TTS")
|
|
|
|
| 391 |
output = gr.Textbox(label="Status", lines=28)
|
| 392 |
prepare_run.click(fn=prepare_runtime, outputs=output)
|
| 393 |
prepare_status_run.click(fn=prepare_status, outputs=output)
|
|
@@ -395,6 +493,7 @@ with gr.Blocks(title="VoiceGate") as demo:
|
|
| 395 |
comfy_run.click(fn=comfy_runtime_test, outputs=output)
|
| 396 |
melband_run.click(fn=melband_gpu_test, outputs=output)
|
| 397 |
voxcpm_run.click(fn=voxcpm_tts_gpu_test, outputs=output)
|
|
|
|
| 398 |
|
| 399 |
|
| 400 |
if __name__ == "__main__":
|
|
|
|
| 2 |
|
| 3 |
import json
|
| 4 |
import math
|
| 5 |
+
import shutil
|
| 6 |
import subprocess
|
| 7 |
import sys
|
| 8 |
import time
|
|
|
|
| 197 |
if output_files:
|
| 198 |
lines.append("outputs:")
|
| 199 |
lines.extend(output_files)
|
| 200 |
+
text_outputs = []
|
| 201 |
+
for node_output in outputs.values():
|
| 202 |
+
for key in ("text", "string"):
|
| 203 |
+
values = node_output.get(key, []) or []
|
| 204 |
+
if isinstance(values, str):
|
| 205 |
+
values = [values]
|
| 206 |
+
text_outputs.extend(str(value) for value in values)
|
| 207 |
+
if text_outputs:
|
| 208 |
+
lines.append("text_outputs:")
|
| 209 |
+
for value in text_outputs:
|
| 210 |
+
lines.append(value[:2000])
|
| 211 |
return lines
|
| 212 |
|
| 213 |
|
|
|
|
| 281 |
}
|
| 282 |
|
| 283 |
|
| 284 |
+
def copy_audio_to_comfy_input(audio_path: str | Path, prefix: str) -> str:
|
| 285 |
+
source = Path(audio_path)
|
| 286 |
+
if not source.exists():
|
| 287 |
+
raise FileNotFoundError(f"Uploaded audio does not exist: {source}")
|
| 288 |
+
suffix = source.suffix or ".wav"
|
| 289 |
+
filename = f"{prefix}_{uuid.uuid4().hex[:8]}{suffix}"
|
| 290 |
+
COMFY_INPUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 291 |
+
shutil.copyfile(source, COMFY_INPUT_DIR / filename)
|
| 292 |
+
return filename
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def asr_workflow(audio_filename: str, prefix: str) -> dict[str, Any]:
|
| 296 |
+
return {
|
| 297 |
+
"1": {
|
| 298 |
+
"class_type": "LoadAudio",
|
| 299 |
+
"inputs": {"audio": audio_filename, "audioUI": ""},
|
| 300 |
+
},
|
| 301 |
+
"2": {
|
| 302 |
+
"class_type": "VoiceBridgeASRLoader",
|
| 303 |
+
"inputs": {
|
| 304 |
+
"repo_id": "Qwen/Qwen3-ASR-1.7B",
|
| 305 |
+
"source": "HuggingFace",
|
| 306 |
+
"precision": "bf16",
|
| 307 |
+
"attention": "sdpa",
|
| 308 |
+
"max_new_tokens": 256,
|
| 309 |
+
"forced_aligner": "Qwen/Qwen3-ForcedAligner-0.6B",
|
| 310 |
+
"local_model_path_asr": "",
|
| 311 |
+
"local_model_path_fa": "",
|
| 312 |
+
},
|
| 313 |
+
},
|
| 314 |
+
"3": {
|
| 315 |
+
"class_type": "VoiceBridgeASRTranscribe",
|
| 316 |
+
"inputs": {
|
| 317 |
+
"model_key": ["2", 0],
|
| 318 |
+
"audio": ["1", 0],
|
| 319 |
+
"language": "auto",
|
| 320 |
+
"context": "",
|
| 321 |
+
"return_timestamps": True,
|
| 322 |
+
},
|
| 323 |
+
},
|
| 324 |
+
"4": {
|
| 325 |
+
"class_type": "GenerateSRT",
|
| 326 |
+
"inputs": {
|
| 327 |
+
"forced_aligns": ["3", 0],
|
| 328 |
+
"text": ["3", 1],
|
| 329 |
+
"language": ["3", 2],
|
| 330 |
+
"save_srt": True,
|
| 331 |
+
"filename_prefix": f"VoiceBridge/{prefix}",
|
| 332 |
+
},
|
| 333 |
+
},
|
| 334 |
+
"5": {
|
| 335 |
+
"class_type": "easy showAnything",
|
| 336 |
+
"inputs": {
|
| 337 |
+
"text": "",
|
| 338 |
+
"anything": ["4", 0],
|
| 339 |
+
},
|
| 340 |
+
},
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
|
| 344 |
def prepare_runtime() -> str:
|
| 345 |
global PREPARE_PROCESS
|
| 346 |
|
|
|
|
| 350 |
return "\n".join(lines)
|
| 351 |
BOOTSTRAP_LOG.parent.mkdir(parents=True, exist_ok=True)
|
| 352 |
log = BOOTSTRAP_LOG.open("ab")
|
| 353 |
+
command = [sys.executable, str(ROOT / "scripts" / "bootstrap_comfy.py"), "--with-models"]
|
| 354 |
PREPARE_PROCESS = subprocess.Popen(
|
| 355 |
command,
|
| 356 |
cwd=ROOT,
|
|
|
|
| 451 |
return "\n".join(lines)
|
| 452 |
|
| 453 |
|
| 454 |
+
@spaces.GPU(duration=1800)
|
| 455 |
+
def asr_gpu_test(audio_path: str | None) -> str:
|
| 456 |
+
lines = gpu_status_lines()
|
| 457 |
+
started = time.time()
|
| 458 |
+
try:
|
| 459 |
+
if not audio_path:
|
| 460 |
+
raise ValueError("Please upload an audio file before running ASR.")
|
| 461 |
+
ensure_comfy(lines)
|
| 462 |
+
prefix = f"asr_gpu_{uuid.uuid4().hex[:8]}"
|
| 463 |
+
audio_filename = copy_audio_to_comfy_input(audio_path, prefix)
|
| 464 |
+
lines.append(f"input_audio={audio_filename}")
|
| 465 |
+
prompt_id = submit_prompt(asr_workflow(audio_filename, prefix))
|
| 466 |
+
lines.append(f"prompt_id={prompt_id}")
|
| 467 |
+
history = wait_for_history(prompt_id, timeout=1800)
|
| 468 |
+
lines.extend(history_summary(history))
|
| 469 |
+
lines.append(f"elapsed_sec={time.time() - started:.1f}")
|
| 470 |
+
except Exception as exc:
|
| 471 |
+
lines.append(f"error={type(exc).__name__}: {exc}")
|
| 472 |
+
if COMFY_LOG.exists():
|
| 473 |
+
lines.append("comfy_log_tail:")
|
| 474 |
+
lines.extend(COMFY_LOG.read_text(encoding="utf-8", errors="replace").splitlines()[-180:])
|
| 475 |
+
return "\n".join(lines)
|
| 476 |
+
|
| 477 |
+
|
| 478 |
with gr.Blocks(title="VoiceGate") as demo:
|
| 479 |
gr.Markdown("# VoiceGate")
|
| 480 |
+
asr_audio = gr.Audio(label="ASR test audio", type="filepath")
|
| 481 |
with gr.Row():
|
| 482 |
prepare_run = gr.Button("Prepare")
|
| 483 |
prepare_status_run = gr.Button("Prepare Status")
|
|
|
|
| 485 |
comfy_run = gr.Button("ComfyUI")
|
| 486 |
melband_run = gr.Button("MelBand")
|
| 487 |
voxcpm_run = gr.Button("VoxCPM TTS")
|
| 488 |
+
asr_run = gr.Button("ASR")
|
| 489 |
output = gr.Textbox(label="Status", lines=28)
|
| 490 |
prepare_run.click(fn=prepare_runtime, outputs=output)
|
| 491 |
prepare_status_run.click(fn=prepare_status, outputs=output)
|
|
|
|
| 493 |
comfy_run.click(fn=comfy_runtime_test, outputs=output)
|
| 494 |
melband_run.click(fn=melband_gpu_test, outputs=output)
|
| 495 |
voxcpm_run.click(fn=voxcpm_tts_gpu_test, outputs=output)
|
| 496 |
+
asr_run.click(fn=asr_gpu_test, inputs=asr_audio, outputs=output)
|
| 497 |
|
| 498 |
|
| 499 |
if __name__ == "__main__":
|
docs/hf-space-comfyui-step-by-step.zh.md
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 从零部署 ComfyUI Workflow 到 Hugging Face Space 的操作指南
|
| 2 |
+
|
| 3 |
+
本文是 VoiceGate 部署过程沉淀出来的通用指南。目标是:从一个本地
|
| 4 |
+
ComfyUI workflow 出发,快速构建一个 Hugging Face Space,用 Gradio 做
|
| 5 |
+
前端,用 ComfyUI 做后端,并尽量避开已经踩过的坑。
|
| 6 |
+
|
| 7 |
+
## 1. 先确定部署形态
|
| 8 |
+
|
| 9 |
+
推荐形态:
|
| 10 |
+
|
| 11 |
+
- Space SDK:Gradio。
|
| 12 |
+
- GPU:ZeroGPU,适合按需调用。
|
| 13 |
+
- ComfyUI:不要直接提交整个 ComfyUI 目录到 git。
|
| 14 |
+
- Custom nodes:用 bootstrap 脚本按 pinned commit 拉取。
|
| 15 |
+
- 大模型:放在 Space persistent storage,例如 `/data/project_models`。
|
| 16 |
+
- Secrets:只放 Hugging Face Space Secrets,不写进代码或 workflow JSON。
|
| 17 |
+
|
| 18 |
+
不推荐:
|
| 19 |
+
|
| 20 |
+
- 把模型权重提交到 Space git。
|
| 21 |
+
- 把 API key 写进 workflow。
|
| 22 |
+
- 依赖 Dev Mode 里的手动修改作为最终状态。
|
| 23 |
+
- 假设 SSH 能看到 ZeroGPU CUDA。
|
| 24 |
+
|
| 25 |
+
## 2. 创建 Space
|
| 26 |
+
|
| 27 |
+
在 Hugging Face 创建 Space:
|
| 28 |
+
|
| 29 |
+
- SDK 选择 `Gradio`。
|
| 30 |
+
- Hardware 选择 ZeroGPU,例如 `zero-a10g`。
|
| 31 |
+
- 如果需要持久化模型,开启 persistent storage。
|
| 32 |
+
- 如果 Space 是组织名下项目,确认 token 有该组织的 Space 写权限。
|
| 33 |
+
|
| 34 |
+
本地登录:
|
| 35 |
+
|
| 36 |
+
```powershell
|
| 37 |
+
huggingface-cli login
|
| 38 |
+
huggingface-cli whoami
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
添加 Space git remote:
|
| 42 |
+
|
| 43 |
+
```powershell
|
| 44 |
+
git remote add space https://huggingface.co/spaces/<org>/<space-name>
|
| 45 |
+
git push space main
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
## 3. 推荐目录结构
|
| 49 |
+
|
| 50 |
+
```text
|
| 51 |
+
project-hf/
|
| 52 |
+
|-- app.py
|
| 53 |
+
|-- README.md
|
| 54 |
+
|-- requirements.txt
|
| 55 |
+
|-- packages.txt
|
| 56 |
+
|-- scripts/
|
| 57 |
+
| |-- bootstrap_comfy.py
|
| 58 |
+
| |-- run_comfy.py
|
| 59 |
+
| `-- workflow_client.py
|
| 60 |
+
|-- workflows/
|
| 61 |
+
| |-- workflow_api.json
|
| 62 |
+
| `-- workflow_ui.json
|
| 63 |
+
|-- docs/
|
| 64 |
+
| |-- deployment-plan.md
|
| 65 |
+
| |-- dependency-inventory.md
|
| 66 |
+
| |-- ssh-runbook.md
|
| 67 |
+
| |-- todo.md
|
| 68 |
+
| `-- work-log.md
|
| 69 |
+
`-- assets/
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
本地可以保留上游项目 checkout,例如 `VoiceGate/`,但要加入 `.gitignore`。
|
| 73 |
+
它只作为参考,不作为 Space runtime 内容。
|
| 74 |
+
|
| 75 |
+
## 4. 先做 workflow 和依赖盘点
|
| 76 |
+
|
| 77 |
+
从 ComfyUI 导出两个文件:
|
| 78 |
+
|
| 79 |
+
- API workflow:用于 `/prompt` 调用。
|
| 80 |
+
- UI workflow:用于人工参考节点布局。
|
| 81 |
+
|
| 82 |
+
检查 API workflow:
|
| 83 |
+
|
| 84 |
+
```powershell
|
| 85 |
+
python -m json.tool workflows\workflow_api.json > $null
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
列出所有 `class_type`:
|
| 89 |
+
|
| 90 |
+
```powershell
|
| 91 |
+
python -c "import json; w=json.load(open('workflows/workflow_api.json',encoding='utf-8')); print('\n'.join(sorted({v['class_type'] for v in w.values()})))"
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
为每个 custom node 找到:
|
| 95 |
+
|
| 96 |
+
- GitHub repo URL。
|
| 97 |
+
- 可用 commit。
|
| 98 |
+
- requirements.txt。
|
| 99 |
+
- 额外系统包。
|
| 100 |
+
- 模型路径约定。
|
| 101 |
+
|
| 102 |
+
把这些写进 `docs/dependency-inventory.md`。
|
| 103 |
+
|
| 104 |
+
## 5. 写 bootstrap 脚本
|
| 105 |
+
|
| 106 |
+
`scripts/bootstrap_comfy.py` 应该是幂等的:
|
| 107 |
+
|
| 108 |
+
- 如果 ComfyUI 不存在,clone。
|
| 109 |
+
- 如果 custom node 不存在,clone。
|
| 110 |
+
- 每个 repo checkout 到固定 commit。
|
| 111 |
+
- 安装 ComfyUI requirements。
|
| 112 |
+
- 安装 custom node requirements。
|
| 113 |
+
- 创建模型目录和 symlink。
|
| 114 |
+
- 可选下载模型。
|
| 115 |
+
|
| 116 |
+
模型不要默认每次下载。建议做成:
|
| 117 |
+
|
| 118 |
+
```bash
|
| 119 |
+
python scripts/bootstrap_comfy.py
|
| 120 |
+
python scripts/bootstrap_comfy.py --with-models
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
持久化模型路径建议:
|
| 124 |
+
|
| 125 |
+
```text
|
| 126 |
+
/data/project_models
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
再 symlink 到 ComfyUI 需要的位置,例如:
|
| 130 |
+
|
| 131 |
+
```text
|
| 132 |
+
ComfyUI/models/voxcpm/VoxCPM2
|
| 133 |
+
-> /data/project_models/voxcpm/VoxCPM2
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
## 6. 写最小 Gradio 入口
|
| 137 |
+
|
| 138 |
+
ZeroGPU Space 启动时必须能发现至少一个 `@spaces.GPU` 函数,否则可能报:
|
| 139 |
+
|
| 140 |
+
```text
|
| 141 |
+
No @spaces.GPU function detected during startup
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
最小形式:
|
| 145 |
+
|
| 146 |
+
```python
|
| 147 |
+
import gradio as gr
|
| 148 |
+
import spaces
|
| 149 |
+
import torch
|
| 150 |
+
|
| 151 |
+
@spaces.GPU(duration=60)
|
| 152 |
+
def gpu_smoke_test():
|
| 153 |
+
return f"cuda_available={torch.cuda.is_available()}"
|
| 154 |
+
|
| 155 |
+
with gr.Blocks() as demo:
|
| 156 |
+
button = gr.Button("GPU")
|
| 157 |
+
output = gr.Textbox()
|
| 158 |
+
button.click(gpu_smoke_test, outputs=output)
|
| 159 |
+
|
| 160 |
+
demo.launch()
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
先推送这个版本,确认 Space 能运行。
|
| 164 |
+
|
| 165 |
+
## 7. 正确理解 ZeroGPU、SSH、Dev Mode
|
| 166 |
+
|
| 167 |
+
关键结论:
|
| 168 |
+
|
| 169 |
+
- SSH 只适合诊断容器文件和 CPU 模式。
|
| 170 |
+
- SSH 里通常看不到 ZeroGPU CUDA。
|
| 171 |
+
- 真正的 GPU 工作必须从 Gradio 的 `@spaces.GPU(...)` 函数触发。
|
| 172 |
+
- Dev Mode 开启后,运行容器可能停留在旧 commit。
|
| 173 |
+
|
| 174 |
+
SSH 只在 Dev Mode 开启时可用:
|
| 175 |
+
|
| 176 |
+
```powershell
|
| 177 |
+
ssh -i "$env:USERPROFILE\.ssh\<key_name>" <space-user>@ssh.hf.space
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
如果 Space 一直不更新到最新 commit:
|
| 181 |
+
|
| 182 |
+
- 关闭 Dev Mode。
|
| 183 |
+
- push 一个新 commit 或 factory reboot。
|
| 184 |
+
- 用 HF API 确认 runtime sha。
|
| 185 |
+
|
| 186 |
+
## 8. 不要在 Gradio 请求里同步做重 bootstrap
|
| 187 |
+
|
| 188 |
+
踩过的坑:
|
| 189 |
+
|
| 190 |
+
```text
|
| 191 |
+
Gradio click -> bootstrap_comfy.py -> clone -> pip install -> start ComfyUI
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
这种请求可能在几分钟后被外层 queue 中断,只看到:
|
| 195 |
+
|
| 196 |
+
```text
|
| 197 |
+
event: error
|
| 198 |
+
data: {"error": null}
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
推荐:
|
| 202 |
+
|
| 203 |
+
- `Prepare` 按钮:非 GPU,启动后台 bootstrap 进程。
|
| 204 |
+
- `Prepare Status` 按钮:读取 `/tmp/bootstrap.log`。
|
| 205 |
+
- GPU 按钮:只启动 ComfyUI、���交 workflow、等待结果。
|
| 206 |
+
|
| 207 |
+
这样不会把 ZeroGPU 时间浪费在安装依赖上,也更容易看到真实错误。
|
| 208 |
+
|
| 209 |
+
## 9. 启动 ComfyUI 并通过 API 测试
|
| 210 |
+
|
| 211 |
+
ComfyUI 启动命令:
|
| 212 |
+
|
| 213 |
+
```bash
|
| 214 |
+
python main.py --listen 127.0.0.1 --port 8188
|
| 215 |
+
```
|
| 216 |
+
|
| 217 |
+
先测:
|
| 218 |
+
|
| 219 |
+
```text
|
| 220 |
+
GET /system_stats
|
| 221 |
+
```
|
| 222 |
+
|
| 223 |
+
再测最小 workflow:
|
| 224 |
+
|
| 225 |
+
```text
|
| 226 |
+
LoadAudio -> SaveAudioMP3
|
| 227 |
+
```
|
| 228 |
+
|
| 229 |
+
常用 API:
|
| 230 |
+
|
| 231 |
+
- `POST /upload/image`:上传 input 文件,音频也可用这个入口。
|
| 232 |
+
- `POST /prompt`:提交 API workflow。
|
| 233 |
+
- `GET /history/{prompt_id}`:取执行结果。
|
| 234 |
+
|
| 235 |
+
音频上传要注意二进制安全。不要用会破坏字节流的 PowerShell 文本管道。
|
| 236 |
+
上传后最好校验 sha256 或至少确认 `file` 识别为音频。
|
| 237 |
+
|
| 238 |
+
## 10. 分阶段测试 workflow
|
| 239 |
+
|
| 240 |
+
不要一上来跑完整 workflow。推荐顺序:
|
| 241 |
+
|
| 242 |
+
1. GPU smoke test:确认 CUDA 可用。
|
| 243 |
+
2. ComfyUI runtime test:确认 `@spaces.GPU -> ComfyUI -> /system_stats`。
|
| 244 |
+
3. Load/Save audio:确认 ComfyUI API 和音频文件路径。
|
| 245 |
+
4. LLM-only:确认 secret 和外部 API。
|
| 246 |
+
5. MelBand-only:确认模型加载和音频输出。
|
| 247 |
+
6. TTS-only:确认 TTS 模型能进 GPU。
|
| 248 |
+
7. ASR-only:确认 ASR 模型、aligner、SRT 输出。
|
| 249 |
+
8. SRT split/merge。
|
| 250 |
+
9. 完整短音频 workflow。
|
| 251 |
+
10. 最后再做正式 Gradio UI。
|
| 252 |
+
|
| 253 |
+
每一步都要记录:
|
| 254 |
+
|
| 255 |
+
- 输入文件。
|
| 256 |
+
- prompt id。
|
| 257 |
+
- status_str。
|
| 258 |
+
- outputs。
|
| 259 |
+
- elapsed_sec。
|
| 260 |
+
- 错误日志尾部。
|
| 261 |
+
|
| 262 |
+
## 11. Secrets 的安全配置
|
| 263 |
+
|
| 264 |
+
API key 放在 HF Space Secrets,例如:
|
| 265 |
+
|
| 266 |
+
```text
|
| 267 |
+
DEEPSEEK_API_KEY
|
| 268 |
+
```
|
| 269 |
+
|
| 270 |
+
代码里只读环境变量:
|
| 271 |
+
|
| 272 |
+
```python
|
| 273 |
+
import os
|
| 274 |
+
api_key = os.environ.get("DEEPSEEK_API_KEY")
|
| 275 |
+
```
|
| 276 |
+
|
| 277 |
+
不要打印 key。检查时只输出:
|
| 278 |
+
|
| 279 |
+
- 是否存在。
|
| 280 |
+
- 长度。
|
| 281 |
+
- 前后缀也尽量不要输出。
|
| 282 |
+
|
| 283 |
+
## 12. 常见坑和解决办法
|
| 284 |
+
|
| 285 |
+
### `No @spaces.GPU function detected`
|
| 286 |
+
|
| 287 |
+
原因:ZeroGPU Space 启动时没有发现 `@spaces.GPU`。
|
| 288 |
+
|
| 289 |
+
解决:在 `app.py` 保留至少一个 GPU 函数。
|
| 290 |
+
|
| 291 |
+
### SSH 里 `torch.cuda.is_available()` 是 False
|
| 292 |
+
|
| 293 |
+
这是正常的。SSH 不代表 ZeroGPU 执行环境。
|
| 294 |
+
|
| 295 |
+
解决:从 Gradio `@spaces.GPU` 函数里测 CUDA。
|
| 296 |
+
|
| 297 |
+
### Dev Mode 里代码不是最新
|
| 298 |
+
|
| 299 |
+
Dev Mode 容器可能停留在旧 commit。
|
| 300 |
+
|
| 301 |
+
解决:关闭 Dev Mode,重新 rebuild,或 push 新 commit。
|
| 302 |
+
|
| 303 |
+
### Custom node import failed
|
| 304 |
+
|
| 305 |
+
先看 ComfyUI 启动日志,通常是缺 Python 包或系统包。
|
| 306 |
+
|
| 307 |
+
VoiceGate 遇到过:
|
| 308 |
+
|
| 309 |
+
```text
|
| 310 |
+
SoX could not be found
|
| 311 |
+
ModuleNotFoundError: sounddevice
|
| 312 |
+
OSError: PortAudio library not found
|
| 313 |
+
ModuleNotFoundError: easydict
|
| 314 |
+
ModuleNotFoundError: pytorch_lightning
|
| 315 |
+
```
|
| 316 |
+
|
| 317 |
+
对应修复:
|
| 318 |
+
|
| 319 |
+
```text
|
| 320 |
+
packages.txt: sox, libportaudio2, portaudio19-dev
|
| 321 |
+
requirements.txt: sounddevice, easydict, pytorch-lightning
|
| 322 |
+
```
|
| 323 |
+
|
| 324 |
+
### Runtime pip install 改变 torch 版本
|
| 325 |
+
|
| 326 |
+
如果 Gradio 进程已经 import torch,再在运行时 pip install/upgrade torch,
|
| 327 |
+
Gradio 进程和后续子进程可能看到不同 torch 版本。
|
| 328 |
+
|
| 329 |
+
解决:生产版本尽量把重依赖放进 build 阶段,并显式 pin 版本。
|
| 330 |
+
|
| 331 |
+
### 模型下载 CLI 不稳定
|
| 332 |
+
|
| 333 |
+
`huggingface-cli download` 或 `hf download` 可能因为版本兼容问题失败。
|
| 334 |
+
|
| 335 |
+
解决:用 `huggingface_hub` 的 Python API 下载模型。
|
| 336 |
+
|
| 337 |
+
### TTS/ASR 冷启动很慢
|
| 338 |
+
|
| 339 |
+
大模型第一次加载会非常慢。VoiceGate 的 VoxCPM2 TTS-only 冷启动约 766 秒。
|
| 340 |
+
|
| 341 |
+
解决方向:
|
| 342 |
+
|
| 343 |
+
- 缩短测试文本。
|
| 344 |
+
- 避免每个 GPU 调用重复启动 ComfyUI。
|
| 345 |
+
- 尽量复用模型进程。
|
| 346 |
+
- 先做子流程验证,再优化 UX。
|
| 347 |
+
|
| 348 |
+
## 13. 最终正式化建议
|
| 349 |
+
|
| 350 |
+
诊断阶段可以把很多按钮放在 `app.py` 里。正式阶段建议拆分:
|
| 351 |
+
|
| 352 |
+
```text
|
| 353 |
+
app.py
|
| 354 |
+
scripts/bootstrap_comfy.py
|
| 355 |
+
voicegate_runtime/
|
| 356 |
+
|-- comfy_server.py
|
| 357 |
+
|-- workflows.py
|
| 358 |
+
|-- files.py
|
| 359 |
+
`-- ui.py
|
| 360 |
+
```
|
| 361 |
+
|
| 362 |
+
正式 Gradio UI 至少提供:
|
| 363 |
+
|
| 364 |
+
- 音频上传。
|
| 365 |
+
- 目标语言。
|
| 366 |
+
- 运行状态。
|
| 367 |
+
- 生成音频下载。
|
| 368 |
+
- SRT 下载。
|
| 369 |
+
- 错误日志摘要。
|
| 370 |
+
- 文件类型和时长限制。
|
| 371 |
+
|
| 372 |
+
先限制短音频,等完整链路稳定后再支持视频和更长输入。
|
docs/work-log.md
CHANGED
|
@@ -19,6 +19,32 @@ and how they were resolved.
|
|
| 19 |
Do not commit API keys, model weights, uploaded media, generated outputs, or the
|
| 20 |
local `VoiceGate/` upstream checkout.
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
## Repository Setup Completed
|
| 23 |
|
| 24 |
- Created and pushed the Space wrapper repository.
|
|
@@ -161,6 +187,18 @@ def placeholder():
|
|
| 161 |
...
|
| 162 |
```
|
| 163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
### SSH Does Not Expose ZeroGPU CUDA
|
| 165 |
|
| 166 |
Starting ComfyUI normally through SSH failed with:
|
|
@@ -181,6 +219,52 @@ CPU diagnostic command:
|
|
| 181 |
python scripts/run_comfy.py --cpu
|
| 182 |
```
|
| 183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
## Dependency Pitfalls and Fixes
|
| 185 |
|
| 186 |
`ComfyUI_AudioTools` initially failed to import.
|
|
|
|
| 19 |
Do not commit API keys, model weights, uploaded media, generated outputs, or the
|
| 20 |
local `VoiceGate/` upstream checkout.
|
| 21 |
|
| 22 |
+
## Executive Summary
|
| 23 |
+
|
| 24 |
+
The Space is no longer just a blank scaffold. It can now run Gradio, invoke
|
| 25 |
+
ZeroGPU, prepare a ComfyUI runtime, start ComfyUI from a GPU-backed Gradio
|
| 26 |
+
function, and submit several segmented ComfyUI workflows.
|
| 27 |
+
|
| 28 |
+
Confirmed working:
|
| 29 |
+
|
| 30 |
+
- Hugging Face Space git push and normal rebuild flow.
|
| 31 |
+
- Dev Mode SSH for CPU/container diagnostics.
|
| 32 |
+
- ZeroGPU invocation from Gradio through `@spaces.GPU`.
|
| 33 |
+
- ComfyUI startup from inside a `@spaces.GPU` function.
|
| 34 |
+
- ComfyUI API calls from the Gradio process.
|
| 35 |
+
- DeepSeek-compatible LLM node with the Space secret.
|
| 36 |
+
- MelBand RoFormer smoke tests in CPU mode and ZeroGPU mode.
|
| 37 |
+
- VoxCPM2 TTS-only smoke test in ZeroGPU mode.
|
| 38 |
+
- Persistent model storage for VoxCPM2 and MelBand under `/data`.
|
| 39 |
+
|
| 40 |
+
Not yet confirmed at the start of 2026-06-06:
|
| 41 |
+
|
| 42 |
+
- VoiceBridge ASR-only workflow.
|
| 43 |
+
- Qwen ASR and Qwen forced aligner model download/cache behavior in the Space.
|
| 44 |
+
- SRT split -> VoxCPM -> SRT merge.
|
| 45 |
+
- Full short-audio VoiceGate workflow.
|
| 46 |
+
- Final user-facing Gradio upload/download UI.
|
| 47 |
+
|
| 48 |
## Repository Setup Completed
|
| 49 |
|
| 50 |
- Created and pushed the Space wrapper repository.
|
|
|
|
| 187 |
...
|
| 188 |
```
|
| 189 |
|
| 190 |
+
Later this placeholder was replaced by real diagnostic functions:
|
| 191 |
+
|
| 192 |
+
```python
|
| 193 |
+
@spaces.GPU(duration=60)
|
| 194 |
+
def gpu_smoke_test():
|
| 195 |
+
...
|
| 196 |
+
|
| 197 |
+
@spaces.GPU(duration=900)
|
| 198 |
+
def comfy_runtime_test():
|
| 199 |
+
...
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
### SSH Does Not Expose ZeroGPU CUDA
|
| 203 |
|
| 204 |
Starting ComfyUI normally through SSH failed with:
|
|
|
|
| 219 |
python scripts/run_comfy.py --cpu
|
| 220 |
```
|
| 221 |
|
| 222 |
+
### Gradio Request Timeout During Bootstrap
|
| 223 |
+
|
| 224 |
+
Long bootstrap work should not run synchronously inside a Gradio request. The
|
| 225 |
+
first attempt did this:
|
| 226 |
+
|
| 227 |
+
```text
|
| 228 |
+
Gradio click -> bootstrap_comfy.py -> clone repos -> pip install -> start ComfyUI
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
The request was interrupted by Gradio/ZeroGPU's outer queue after roughly 2.5
|
| 232 |
+
minutes and returned:
|
| 233 |
+
|
| 234 |
+
```text
|
| 235 |
+
event: error
|
| 236 |
+
data: {"error": null}
|
| 237 |
+
```
|
| 238 |
+
|
| 239 |
+
Fix:
|
| 240 |
+
|
| 241 |
+
- Add a non-GPU `Prepare` action that starts `scripts/bootstrap_comfy.py` as a
|
| 242 |
+
background process.
|
| 243 |
+
- Add `Prepare Status` to poll `/tmp/voicegate_bootstrap.log`.
|
| 244 |
+
- Keep GPU actions focused on starting ComfyUI and running actual CUDA work.
|
| 245 |
+
|
| 246 |
+
This avoids wasting ZeroGPU time on clone/install steps and prevents the request
|
| 247 |
+
from being killed before diagnostics can return useful logs.
|
| 248 |
+
|
| 249 |
+
### Runtime Pip Install Pitfall
|
| 250 |
+
|
| 251 |
+
The background bootstrap installed a large dependency set and upgraded the
|
| 252 |
+
on-disk Torch package. The already-running Gradio process continued to report:
|
| 253 |
+
|
| 254 |
+
```text
|
| 255 |
+
torch=2.11.0+cu130
|
| 256 |
+
```
|
| 257 |
+
|
| 258 |
+
while the ComfyUI subprocess started afterwards reported:
|
| 259 |
+
|
| 260 |
+
```text
|
| 261 |
+
pytorch_version=2.12.0+cu130
|
| 262 |
+
```
|
| 263 |
+
|
| 264 |
+
This is workable for diagnostics, but final production should avoid heavy
|
| 265 |
+
runtime `pip install` where possible. Prefer moving stable dependencies into
|
| 266 |
+
Space build-time requirements or explicitly controlling pins.
|
| 267 |
+
|
| 268 |
## Dependency Pitfalls and Fixes
|
| 269 |
|
| 270 |
`ComfyUI_AudioTools` initially failed to import.
|
scripts/bootstrap_comfy.py
CHANGED
|
@@ -95,11 +95,13 @@ CUSTOM_NODE_REPOS = [
|
|
| 95 |
MODEL_LINKS = {
|
| 96 |
"voxcpm2": COMFY_DIR / "models" / "voxcpm" / "VoxCPM2",
|
| 97 |
"melband": COMFY_DIR / "models" / "diffusion_models" / "MelBandRoFormer_comfy",
|
|
|
|
| 98 |
}
|
| 99 |
|
| 100 |
MODEL_DIRS = {
|
| 101 |
"voxcpm2": Path("voxcpm") / "VoxCPM2",
|
| 102 |
"melband": Path("diffusion_models") / "MelBandRoFormer_comfy",
|
|
|
|
| 103 |
}
|
| 104 |
|
| 105 |
|
|
@@ -197,6 +199,11 @@ def download_models(dry_run: bool = False) -> None:
|
|
| 197 |
"+ hf_hub_download "
|
| 198 |
f"Kijai/MelBandRoFormer_comfy/MelBandRoformer_fp32.safetensors -> {model_target('melband')}"
|
| 199 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
return
|
| 201 |
|
| 202 |
from huggingface_hub import hf_hub_download, snapshot_download
|
|
@@ -212,6 +219,16 @@ def download_models(dry_run: bool = False) -> None:
|
|
| 212 |
local_dir=model_target("melband"),
|
| 213 |
token=token,
|
| 214 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
|
| 216 |
|
| 217 |
def print_summary() -> None:
|
|
|
|
| 95 |
MODEL_LINKS = {
|
| 96 |
"voxcpm2": COMFY_DIR / "models" / "voxcpm" / "VoxCPM2",
|
| 97 |
"melband": COMFY_DIR / "models" / "diffusion_models" / "MelBandRoFormer_comfy",
|
| 98 |
+
"qwen3_asr": COMFY_DIR / "models" / "Qwen3-ASR",
|
| 99 |
}
|
| 100 |
|
| 101 |
MODEL_DIRS = {
|
| 102 |
"voxcpm2": Path("voxcpm") / "VoxCPM2",
|
| 103 |
"melband": Path("diffusion_models") / "MelBandRoFormer_comfy",
|
| 104 |
+
"qwen3_asr": Path("Qwen3-ASR"),
|
| 105 |
}
|
| 106 |
|
| 107 |
|
|
|
|
| 199 |
"+ hf_hub_download "
|
| 200 |
f"Kijai/MelBandRoFormer_comfy/MelBandRoformer_fp32.safetensors -> {model_target('melband')}"
|
| 201 |
)
|
| 202 |
+
print(f"+ snapshot_download Qwen/Qwen3-ASR-1.7B -> {model_target('qwen3_asr') / 'Qwen3-ASR-1.7B'}")
|
| 203 |
+
print(
|
| 204 |
+
"+ snapshot_download "
|
| 205 |
+
f"Qwen/Qwen3-ForcedAligner-0.6B -> {model_target('qwen3_asr') / 'Qwen3-ForcedAligner-0.6B'}"
|
| 206 |
+
)
|
| 207 |
return
|
| 208 |
|
| 209 |
from huggingface_hub import hf_hub_download, snapshot_download
|
|
|
|
| 219 |
local_dir=model_target("melband"),
|
| 220 |
token=token,
|
| 221 |
)
|
| 222 |
+
snapshot_download(
|
| 223 |
+
repo_id="Qwen/Qwen3-ASR-1.7B",
|
| 224 |
+
local_dir=model_target("qwen3_asr") / "Qwen3-ASR-1.7B",
|
| 225 |
+
token=token,
|
| 226 |
+
)
|
| 227 |
+
snapshot_download(
|
| 228 |
+
repo_id="Qwen/Qwen3-ForcedAligner-0.6B",
|
| 229 |
+
local_dir=model_target("qwen3_asr") / "Qwen3-ForcedAligner-0.6B",
|
| 230 |
+
token=token,
|
| 231 |
+
)
|
| 232 |
|
| 233 |
|
| 234 |
def print_summary() -> None:
|