YanTianlong commited on
Commit
359a8b1
·
1 Parent(s): 057d7fe

Add full VoiceGate workflow test

Browse files
Files changed (2) hide show
  1. app.py +47 -0
  2. 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()