Commit ·
359a8b1
1
Parent(s): 057d7fe
Add full VoiceGate workflow test
Browse files- app.py +47 -0
- scripts/validate_workflow_connections.py +148 -0
app.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import json
|
| 4 |
import math
|
|
|
|
| 5 |
import shutil
|
| 6 |
import subprocess
|
| 7 |
import sys
|
|
@@ -16,6 +17,8 @@ import requests
|
|
| 16 |
import spaces
|
| 17 |
import torch
|
| 18 |
|
|
|
|
|
|
|
| 19 |
|
| 20 |
ROOT = Path(__file__).resolve().parent
|
| 21 |
COMFY_DIR = ROOT / "ComfyUI"
|
|
@@ -341,6 +344,19 @@ def asr_workflow(audio_filename: str, prefix: str) -> dict[str, Any]:
|
|
| 341 |
}
|
| 342 |
|
| 343 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
def prepare_runtime() -> str:
|
| 345 |
global PREPARE_PROCESS
|
| 346 |
|
|
@@ -475,9 +491,38 @@ def asr_gpu_test(audio_path: str | None) -> str:
|
|
| 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")
|
|
@@ -486,6 +531,7 @@ with gr.Blocks(title="VoiceGate") as demo:
|
|
| 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)
|
|
@@ -494,6 +540,7 @@ with gr.Blocks(title="VoiceGate") as demo:
|
|
| 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__":
|
|
|
|
| 2 |
|
| 3 |
import json
|
| 4 |
import math
|
| 5 |
+
import os
|
| 6 |
import shutil
|
| 7 |
import subprocess
|
| 8 |
import sys
|
|
|
|
| 17 |
import spaces
|
| 18 |
import torch
|
| 19 |
|
| 20 |
+
from scripts.workflow_client import load_workflow, patch_voicegate_workflow
|
| 21 |
+
|
| 22 |
|
| 23 |
ROOT = Path(__file__).resolve().parent
|
| 24 |
COMFY_DIR = ROOT / "ComfyUI"
|
|
|
|
| 344 |
}
|
| 345 |
|
| 346 |
|
| 347 |
+
def full_voicegate_workflow(audio_filename: str, prefix: str, target_language: str) -> dict[str, Any]:
|
| 348 |
+
workflow = load_workflow()
|
| 349 |
+
return patch_voicegate_workflow(
|
| 350 |
+
workflow,
|
| 351 |
+
audio_filename=audio_filename,
|
| 352 |
+
target_language=target_language,
|
| 353 |
+
api_key=os.environ.get("DEEPSEEK_API_KEY"),
|
| 354 |
+
api_baseurl=os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
|
| 355 |
+
llm_model=os.environ.get("DEEPSEEK_MODEL", "deepseek-v4-flash"),
|
| 356 |
+
job_id=prefix,
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
|
| 360 |
def prepare_runtime() -> str:
|
| 361 |
global PREPARE_PROCESS
|
| 362 |
|
|
|
|
| 491 |
return "\n".join(lines)
|
| 492 |
|
| 493 |
|
| 494 |
+
@spaces.GPU(duration=900)
|
| 495 |
+
def full_voicegate_gpu_test(audio_path: str | None, target_language: str) -> str:
|
| 496 |
+
lines = gpu_status_lines()
|
| 497 |
+
started = time.time()
|
| 498 |
+
try:
|
| 499 |
+
if not audio_path:
|
| 500 |
+
raise ValueError("Please upload an audio file before running VoiceGate.")
|
| 501 |
+
if not os.environ.get("DEEPSEEK_API_KEY"):
|
| 502 |
+
raise RuntimeError("DEEPSEEK_API_KEY is not configured in the Space.")
|
| 503 |
+
ensure_comfy(lines)
|
| 504 |
+
prefix = f"full_{uuid.uuid4().hex[:8]}"
|
| 505 |
+
audio_filename = copy_audio_to_comfy_input(audio_path, prefix)
|
| 506 |
+
lines.append(f"input_audio={audio_filename}")
|
| 507 |
+
lines.append(f"target_language={target_language}")
|
| 508 |
+
prompt = full_voicegate_workflow(audio_filename, prefix, target_language or "English")
|
| 509 |
+
prompt_id = submit_prompt(prompt)
|
| 510 |
+
lines.append(f"prompt_id={prompt_id}")
|
| 511 |
+
history = wait_for_history(prompt_id, timeout=900)
|
| 512 |
+
lines.extend(history_summary(history))
|
| 513 |
+
lines.append(f"elapsed_sec={time.time() - started:.1f}")
|
| 514 |
+
except Exception as exc:
|
| 515 |
+
lines.append(f"error={type(exc).__name__}: {exc}")
|
| 516 |
+
if COMFY_LOG.exists():
|
| 517 |
+
lines.append("comfy_log_tail:")
|
| 518 |
+
lines.extend(COMFY_LOG.read_text(encoding="utf-8", errors="replace").splitlines()[-220:])
|
| 519 |
+
return "\n".join(lines)
|
| 520 |
+
|
| 521 |
+
|
| 522 |
with gr.Blocks(title="VoiceGate") as demo:
|
| 523 |
gr.Markdown("# VoiceGate")
|
| 524 |
asr_audio = gr.Audio(label="ASR test audio", type="filepath")
|
| 525 |
+
target_language = gr.Textbox(label="Target language", value="English")
|
| 526 |
with gr.Row():
|
| 527 |
prepare_run = gr.Button("Prepare")
|
| 528 |
prepare_status_run = gr.Button("Prepare Status")
|
|
|
|
| 531 |
melband_run = gr.Button("MelBand")
|
| 532 |
voxcpm_run = gr.Button("VoxCPM TTS")
|
| 533 |
asr_run = gr.Button("ASR")
|
| 534 |
+
full_run = gr.Button("Full VoiceGate")
|
| 535 |
output = gr.Textbox(label="Status", lines=28)
|
| 536 |
prepare_run.click(fn=prepare_runtime, outputs=output)
|
| 537 |
prepare_status_run.click(fn=prepare_status, outputs=output)
|
|
|
|
| 540 |
melband_run.click(fn=melband_gpu_test, outputs=output)
|
| 541 |
voxcpm_run.click(fn=voxcpm_tts_gpu_test, outputs=output)
|
| 542 |
asr_run.click(fn=asr_gpu_test, inputs=asr_audio, outputs=output)
|
| 543 |
+
full_run.click(fn=full_voicegate_gpu_test, inputs=[asr_audio, target_language], outputs=output)
|
| 544 |
|
| 545 |
|
| 546 |
if __name__ == "__main__":
|
scripts/validate_workflow_connections.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Validate the API workflow connections against the ComfyUI UI workflow.
|
| 2 |
+
|
| 3 |
+
ComfyUI API exports collapse some UI-only SetNode/GetNode routing pairs. This
|
| 4 |
+
validator resolves those pairs before comparing links so the check reflects the
|
| 5 |
+
actual graph semantics instead of the visual routing helpers.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import argparse
|
| 11 |
+
import json
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 17 |
+
DEFAULT_API = ROOT / "workflows" / "voicegate_api.json"
|
| 18 |
+
DEFAULT_UI = ROOT / "VoiceGate" / "workflows" / "VoiceGate-Workflow.json"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def load_json(path: Path) -> Any:
|
| 22 |
+
with path.open("r", encoding="utf-8") as file:
|
| 23 |
+
return json.load(file)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def is_connection(value: Any) -> bool:
|
| 27 |
+
return (
|
| 28 |
+
isinstance(value, list)
|
| 29 |
+
and len(value) == 2
|
| 30 |
+
and isinstance(value[0], str)
|
| 31 |
+
and isinstance(value[1], int)
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def build_expected_connections(ui_workflow: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
| 36 |
+
nodes = ui_workflow["nodes"]
|
| 37 |
+
links = ui_workflow["links"]
|
| 38 |
+
nodes_by_id = {str(node["id"]): node for node in nodes}
|
| 39 |
+
links_by_id = {
|
| 40 |
+
link[0]: {
|
| 41 |
+
"from": str(link[1]),
|
| 42 |
+
"slot": link[2],
|
| 43 |
+
"to": str(link[3]),
|
| 44 |
+
"to_slot": link[4],
|
| 45 |
+
"type": link[5],
|
| 46 |
+
}
|
| 47 |
+
for link in links
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
set_by_name: dict[str, dict[str, Any]] = {}
|
| 51 |
+
for node in nodes:
|
| 52 |
+
if node.get("type") != "SetNode":
|
| 53 |
+
continue
|
| 54 |
+
values = node.get("widgets_values") or []
|
| 55 |
+
inputs = node.get("inputs") or []
|
| 56 |
+
if not values or not inputs:
|
| 57 |
+
continue
|
| 58 |
+
link_id = inputs[0].get("link")
|
| 59 |
+
link = links_by_id.get(link_id)
|
| 60 |
+
if link:
|
| 61 |
+
set_by_name[values[0]] = {
|
| 62 |
+
"from": link["from"],
|
| 63 |
+
"slot": link["slot"],
|
| 64 |
+
"via_set": str(node["id"]),
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
def resolve_source(from_node: str, slot: int) -> dict[str, Any]:
|
| 68 |
+
node = nodes_by_id.get(from_node)
|
| 69 |
+
if node and node.get("type") == "GetNode":
|
| 70 |
+
values = node.get("widgets_values") or []
|
| 71 |
+
name = values[0] if values else None
|
| 72 |
+
if name in set_by_name:
|
| 73 |
+
resolved = dict(set_by_name[name])
|
| 74 |
+
resolved["via_get"] = from_node
|
| 75 |
+
resolved["name"] = name
|
| 76 |
+
return resolved
|
| 77 |
+
return {"from": from_node, "slot": slot}
|
| 78 |
+
|
| 79 |
+
expected: dict[str, dict[str, Any]] = {}
|
| 80 |
+
for node in nodes:
|
| 81 |
+
for input_def in node.get("inputs") or []:
|
| 82 |
+
link_id = input_def.get("link")
|
| 83 |
+
if link_id is None:
|
| 84 |
+
continue
|
| 85 |
+
link = links_by_id.get(link_id)
|
| 86 |
+
if not link:
|
| 87 |
+
continue
|
| 88 |
+
key = f"{node['id']}.{input_def['name']}"
|
| 89 |
+
expected[key] = {
|
| 90 |
+
**resolve_source(link["from"], link["slot"]),
|
| 91 |
+
"type": link["type"],
|
| 92 |
+
}
|
| 93 |
+
return expected
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def validate(api_workflow: dict[str, Any], ui_workflow: dict[str, Any]) -> list[dict[str, Any]]:
|
| 97 |
+
expected = build_expected_connections(ui_workflow)
|
| 98 |
+
mismatches: list[dict[str, Any]] = []
|
| 99 |
+
for node_id, node in api_workflow.items():
|
| 100 |
+
for input_name, value in (node.get("inputs") or {}).items():
|
| 101 |
+
if not is_connection(value):
|
| 102 |
+
continue
|
| 103 |
+
actual = {"from": value[0], "slot": value[1]}
|
| 104 |
+
expected_connection = expected.get(f"{node_id}.{input_name}")
|
| 105 |
+
if (
|
| 106 |
+
not expected_connection
|
| 107 |
+
or expected_connection["from"] != actual["from"]
|
| 108 |
+
or expected_connection["slot"] != actual["slot"]
|
| 109 |
+
):
|
| 110 |
+
mismatches.append(
|
| 111 |
+
{
|
| 112 |
+
"node": node_id,
|
| 113 |
+
"class_type": node.get("class_type"),
|
| 114 |
+
"input": input_name,
|
| 115 |
+
"actual": actual,
|
| 116 |
+
"expected": expected_connection,
|
| 117 |
+
}
|
| 118 |
+
)
|
| 119 |
+
return mismatches
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def parse_args() -> argparse.Namespace:
|
| 123 |
+
parser = argparse.ArgumentParser()
|
| 124 |
+
parser.add_argument("--api", type=Path, default=DEFAULT_API)
|
| 125 |
+
parser.add_argument("--ui", type=Path, default=DEFAULT_UI)
|
| 126 |
+
return parser.parse_args()
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def main() -> None:
|
| 130 |
+
args = parse_args()
|
| 131 |
+
api_workflow = load_json(args.api)
|
| 132 |
+
ui_workflow = load_json(args.ui)
|
| 133 |
+
mismatches = validate(api_workflow, ui_workflow)
|
| 134 |
+
checked = sum(
|
| 135 |
+
1
|
| 136 |
+
for node in api_workflow.values()
|
| 137 |
+
for value in (node.get("inputs") or {}).values()
|
| 138 |
+
if is_connection(value)
|
| 139 |
+
)
|
| 140 |
+
print(f"checked_connections={checked}")
|
| 141 |
+
print(f"mismatches={len(mismatches)}")
|
| 142 |
+
if mismatches:
|
| 143 |
+
print(json.dumps(mismatches, ensure_ascii=False, indent=2))
|
| 144 |
+
raise SystemExit(1)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
if __name__ == "__main__":
|
| 148 |
+
main()
|