YanTianlong commited on
Commit
2cc8b04
·
1 Parent(s): dd08490

Add subtitle outputs and model preflight

Browse files
Files changed (3) hide show
  1. app.py +117 -9
  2. docs/todo.md +6 -2
  3. docs/work-log.md +29 -2
app.py CHANGED
@@ -40,6 +40,14 @@ COMFY_PROCESS: subprocess.Popen | None = None
40
  PREPARE_PROCESS: subprocess.Popen | None = None
41
  BOOTSTRAPPED = False
42
  BOOTSTRAP_LOG = Path("/tmp/voicegate_bootstrap.log")
 
 
 
 
 
 
 
 
43
  TARGET_LANGUAGES = [
44
  "Arabic",
45
  "Burmese",
@@ -133,6 +141,42 @@ def run_bootstrap(lines: list[str], *, allow_heavy: bool = True) -> None:
133
  BOOTSTRAPPED = True
134
 
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  def ensure_comfy(lines: list[str], *, timeout: float = 240) -> dict[str, Any]:
137
  global COMFY_PROCESS
138
 
@@ -346,6 +390,26 @@ def first_output_audio_path(history: dict[str, Any]) -> str | None:
346
  return None
347
 
348
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  def melband_workflow(audio_filename: str, prefix: str) -> dict[str, Any]:
350
  return {
351
  "1": {
@@ -489,14 +553,14 @@ def full_voicegate_workflow(audio_filename: str, prefix: str, target_language: s
489
  )
490
 
491
 
492
- def run_full_voicegate(audio_path: str | None, target_language: str, *, timeout: float = 880) -> tuple[list[str], str | None]:
493
  lines = gpu_status_lines()
494
  started = time.time()
495
- output_audio = None
496
  if not audio_path:
497
  raise ValueError("Please upload an audio file before running VoiceGate.")
498
  if not os.environ.get("DEEPSEEK_API_KEY"):
499
  raise RuntimeError("DEEPSEEK_API_KEY is not configured in the Space.")
 
500
  ensure_comfy(lines)
501
  prefix = f"full_{uuid.uuid4().hex[:8]}"
502
  audio_filename = copy_audio_to_comfy_input(audio_path, prefix)
@@ -507,10 +571,25 @@ def run_full_voicegate(audio_path: str | None, target_language: str, *, timeout:
507
  lines.extend(timing_lines)
508
  lines.extend(history_summary(history))
509
  output_audio = first_output_audio_path(history)
 
 
 
 
510
  if output_audio:
511
  lines.append(f"output_audio_path={output_audio}")
 
 
 
 
512
  lines.append(f"elapsed_sec={time.time() - started:.1f}")
513
- return lines, output_audio
 
 
 
 
 
 
 
514
 
515
 
516
  def prepare_runtime() -> str:
@@ -650,7 +729,8 @@ def asr_gpu_test(audio_path: str | None) -> str:
650
  @spaces.GPU(duration=900)
651
  def full_voicegate_gpu_test(audio_path: str | None, target_language: str) -> str:
652
  try:
653
- lines, _output_audio = run_full_voicegate(audio_path, target_language, timeout=880)
 
654
  except Exception as exc:
655
  lines = gpu_status_lines()
656
  lines.append(f"error={type(exc).__name__}: {exc}")
@@ -661,19 +741,35 @@ def full_voicegate_gpu_test(audio_path: str | None, target_language: str) -> str
661
 
662
 
663
  @spaces.GPU(duration=900)
664
- def voicegate_user_run(audio_path: str | None, target_language: str) -> tuple[str | None, str]:
 
 
 
 
 
 
 
665
  try:
666
- lines, output_audio = run_full_voicegate(audio_path, target_language, timeout=880)
 
 
667
  if not output_audio:
668
  lines.append("warning=No output audio file was found in ComfyUI history.")
669
- return output_audio, "\n".join(lines)
 
 
 
 
 
 
 
670
  except Exception as exc:
671
  lines = gpu_status_lines()
672
  lines.append(f"error={type(exc).__name__}: {exc}")
673
  if COMFY_LOG.exists():
674
  lines.append("comfy_log_tail:")
675
  lines.extend(COMFY_LOG.read_text(encoding="utf-8", errors="replace").splitlines()[-160:])
676
- return None, "\n".join(lines)
677
 
678
 
679
  with gr.Blocks(title="VoiceGate") as demo:
@@ -687,11 +783,23 @@ with gr.Blocks(title="VoiceGate") as demo:
687
  )
688
  user_run = gr.Button("Generate translated dubbing", variant="primary")
689
  user_output_audio = gr.Audio(label="Translated dubbing audio", type="filepath")
 
 
 
 
 
690
  user_status = gr.Textbox(label="Status", lines=16)
691
  user_run.click(
692
  fn=voicegate_user_run,
693
  inputs=[user_audio, user_target_language],
694
- outputs=[user_output_audio, user_status],
 
 
 
 
 
 
 
695
  )
696
 
697
  with gr.Tab("Diagnostics"):
 
40
  PREPARE_PROCESS: subprocess.Popen | None = None
41
  BOOTSTRAPPED = False
42
  BOOTSTRAP_LOG = Path("/tmp/voicegate_bootstrap.log")
43
+ USER_OUTPUT_DIR = ROOT / "user_outputs"
44
+ REQUIRED_MODEL_PATHS = [
45
+ COMFY_DIR / "models" / "diffusion_models" / "MelBandRoFormer_comfy" / "MelBandRoformer_fp32.safetensors",
46
+ COMFY_DIR / "models" / "voxcpm" / "VoxCPM2" / "model.safetensors",
47
+ COMFY_DIR / "models" / "voxcpm" / "VoxCPM2" / "audiovae.pth",
48
+ COMFY_DIR / "models" / "Qwen3-ASR" / "Qwen3-ASR-1.7B",
49
+ COMFY_DIR / "models" / "Qwen3-ASR" / "Qwen3-ForcedAligner-0.6B",
50
+ ]
51
  TARGET_LANGUAGES = [
52
  "Arabic",
53
  "Burmese",
 
141
  BOOTSTRAPPED = True
142
 
143
 
144
+ def missing_required_models() -> list[Path]:
145
+ return [path for path in REQUIRED_MODEL_PATHS if not path.exists()]
146
+
147
+
148
+ def ensure_runtime_assets(lines: list[str]) -> None:
149
+ missing = missing_required_models()
150
+ if not missing:
151
+ lines.append("models=ready")
152
+ return
153
+
154
+ lines.append("models=missing")
155
+ lines.extend(f"missing_model={path}" for path in missing)
156
+ started = time.time()
157
+ command = [sys.executable, str(ROOT / "scripts" / "bootstrap_comfy.py"), "--with-models"]
158
+ result = subprocess.run(
159
+ command,
160
+ cwd=ROOT,
161
+ text=True,
162
+ stdout=subprocess.PIPE,
163
+ stderr=subprocess.STDOUT,
164
+ timeout=1800,
165
+ )
166
+ lines.append(f"model_prepare_returncode={result.returncode}")
167
+ lines.append(f"model_prepare_elapsed_sec={time.time() - started:.1f}")
168
+ if result.returncode != 0:
169
+ lines.append("model_prepare_tail:")
170
+ lines.extend(result.stdout.splitlines()[-100:])
171
+ raise RuntimeError("Could not prepare required VoiceGate models.")
172
+ remaining = missing_required_models()
173
+ if remaining:
174
+ lines.append("models_still_missing:")
175
+ lines.extend(str(path) for path in remaining)
176
+ raise RuntimeError("Required VoiceGate models are still missing after preparation.")
177
+ lines.append("models=ready_after_prepare")
178
+
179
+
180
  def ensure_comfy(lines: list[str], *, timeout: float = 240) -> dict[str, Any]:
181
  global COMFY_PROCESS
182
 
 
390
  return None
391
 
392
 
393
+ def text_outputs_for_node(history: dict[str, Any], node_id: str) -> list[str]:
394
+ node_output = (history.get("outputs", {}) or {}).get(node_id, {})
395
+ values: list[str] = []
396
+ for key in ("text", "string"):
397
+ raw_values = node_output.get(key, []) or []
398
+ if isinstance(raw_values, str):
399
+ raw_values = [raw_values]
400
+ values.extend(str(value) for value in raw_values if str(value).strip())
401
+ return values
402
+
403
+
404
+ def write_srt_file(prefix: str, name: str, text: str) -> str | None:
405
+ if not text.strip():
406
+ return None
407
+ USER_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
408
+ path = USER_OUTPUT_DIR / f"{prefix}_{name}.srt"
409
+ path.write_text(text, encoding="utf-8")
410
+ return str(path)
411
+
412
+
413
  def melband_workflow(audio_filename: str, prefix: str) -> dict[str, Any]:
414
  return {
415
  "1": {
 
553
  )
554
 
555
 
556
+ def run_full_voicegate(audio_path: str | None, target_language: str, *, timeout: float = 880) -> dict[str, Any]:
557
  lines = gpu_status_lines()
558
  started = time.time()
 
559
  if not audio_path:
560
  raise ValueError("Please upload an audio file before running VoiceGate.")
561
  if not os.environ.get("DEEPSEEK_API_KEY"):
562
  raise RuntimeError("DEEPSEEK_API_KEY is not configured in the Space.")
563
+ ensure_runtime_assets(lines)
564
  ensure_comfy(lines)
565
  prefix = f"full_{uuid.uuid4().hex[:8]}"
566
  audio_filename = copy_audio_to_comfy_input(audio_path, prefix)
 
571
  lines.extend(timing_lines)
572
  lines.extend(history_summary(history))
573
  output_audio = first_output_audio_path(history)
574
+ source_subtitle = "\n\n".join(text_outputs_for_node(history, "61"))
575
+ translated_subtitle = "\n\n".join(text_outputs_for_node(history, "179") or text_outputs_for_node(history, "107"))
576
+ source_subtitle_file = write_srt_file(prefix, "source", source_subtitle)
577
+ translated_subtitle_file = write_srt_file(prefix, "translated", translated_subtitle)
578
  if output_audio:
579
  lines.append(f"output_audio_path={output_audio}")
580
+ if source_subtitle_file:
581
+ lines.append(f"source_subtitle_file={source_subtitle_file}")
582
+ if translated_subtitle_file:
583
+ lines.append(f"translated_subtitle_file={translated_subtitle_file}")
584
  lines.append(f"elapsed_sec={time.time() - started:.1f}")
585
+ return {
586
+ "lines": lines,
587
+ "audio": output_audio,
588
+ "source_subtitle": source_subtitle,
589
+ "translated_subtitle": translated_subtitle,
590
+ "source_subtitle_file": source_subtitle_file,
591
+ "translated_subtitle_file": translated_subtitle_file,
592
+ }
593
 
594
 
595
  def prepare_runtime() -> str:
 
729
  @spaces.GPU(duration=900)
730
  def full_voicegate_gpu_test(audio_path: str | None, target_language: str) -> str:
731
  try:
732
+ result = run_full_voicegate(audio_path, target_language, timeout=880)
733
+ lines = result["lines"]
734
  except Exception as exc:
735
  lines = gpu_status_lines()
736
  lines.append(f"error={type(exc).__name__}: {exc}")
 
741
 
742
 
743
  @spaces.GPU(duration=900)
744
+ def voicegate_user_run(audio_path: str | None, target_language: str) -> tuple[
745
+ str | None,
746
+ str,
747
+ str | None,
748
+ str | None,
749
+ str,
750
+ str,
751
+ ]:
752
  try:
753
+ result = run_full_voicegate(audio_path, target_language, timeout=880)
754
+ lines = result["lines"]
755
+ output_audio = result["audio"]
756
  if not output_audio:
757
  lines.append("warning=No output audio file was found in ComfyUI history.")
758
+ return (
759
+ output_audio,
760
+ "\n".join(lines),
761
+ result["source_subtitle_file"],
762
+ result["translated_subtitle_file"],
763
+ result["source_subtitle"],
764
+ result["translated_subtitle"],
765
+ )
766
  except Exception as exc:
767
  lines = gpu_status_lines()
768
  lines.append(f"error={type(exc).__name__}: {exc}")
769
  if COMFY_LOG.exists():
770
  lines.append("comfy_log_tail:")
771
  lines.extend(COMFY_LOG.read_text(encoding="utf-8", errors="replace").splitlines()[-160:])
772
+ return None, "\n".join(lines), None, None, "", ""
773
 
774
 
775
  with gr.Blocks(title="VoiceGate") as demo:
 
783
  )
784
  user_run = gr.Button("Generate translated dubbing", variant="primary")
785
  user_output_audio = gr.Audio(label="Translated dubbing audio", type="filepath")
786
+ with gr.Row():
787
+ user_source_file = gr.File(label="Original subtitle file")
788
+ user_translated_file = gr.File(label="Translated subtitle file")
789
+ user_source_text = gr.Textbox(label="Original subtitles", lines=8)
790
+ user_translated_text = gr.Textbox(label="Translated subtitles", lines=8)
791
  user_status = gr.Textbox(label="Status", lines=16)
792
  user_run.click(
793
  fn=voicegate_user_run,
794
  inputs=[user_audio, user_target_language],
795
+ outputs=[
796
+ user_output_audio,
797
+ user_status,
798
+ user_source_file,
799
+ user_translated_file,
800
+ user_source_text,
801
+ user_translated_text,
802
+ ],
803
  )
804
 
805
  with gr.Tab("Diagnostics"):
docs/todo.md CHANGED
@@ -95,10 +95,11 @@ from scaffold to a working Gradio app.
95
  - [x] Add target language input.
96
  - [x] Add status/log output.
97
  - [x] Add generated audio output.
98
- - [ ] Add generated SRT file outputs.
99
  - [ ] Wrap GPU-heavy execution with `@spaces.GPU(duration=...)`.
100
  - [x] Add and run a minimal GPU smoke test button.
101
  - [x] Keep a diagnostics tab for internal workflow tests.
 
102
  - [ ] Add guardrails for file type and duration.
103
 
104
  ## Phase 6: Minimal Runtime Tests
@@ -121,7 +122,7 @@ from scaffold to a working Gradio app.
121
  - [ ] Run the full short-audio VoiceGate workflow on the organization ZeroGPU
122
  Space after quota recovers.
123
  - [x] Confirm output audio is downloadable/playable from Gradio.
124
- - [ ] Confirm SRT files are downloadable from Gradio.
125
 
126
  ## Phase 7: Full VoiceGate Path
127
 
@@ -144,6 +145,9 @@ from scaffold to a working Gradio app.
144
  ZeroGPU quota before execution.
145
  - [x] Can a full short-audio workflow run on T4 Small? Yes, on the duplicated
146
  personal Space with a warm ComfyUI process, the test completed in `24.4s`.
 
 
 
147
  - [x] Where should large model files live? `/data/voicegate_models`, with
148
  symlinks into ComfyUI's expected model directories.
149
  - [ ] Should the first public demo disable background separation to reduce
 
95
  - [x] Add target language input.
96
  - [x] Add status/log output.
97
  - [x] Add generated audio output.
98
+ - [x] Add generated SRT file outputs.
99
  - [ ] Wrap GPU-heavy execution with `@spaces.GPU(duration=...)`.
100
  - [x] Add and run a minimal GPU smoke test button.
101
  - [x] Keep a diagnostics tab for internal workflow tests.
102
+ - [x] Add automatic model-path preparation for the user-facing run path.
103
  - [ ] Add guardrails for file type and duration.
104
 
105
  ## Phase 6: Minimal Runtime Tests
 
122
  - [ ] Run the full short-audio VoiceGate workflow on the organization ZeroGPU
123
  Space after quota recovers.
124
  - [x] Confirm output audio is downloadable/playable from Gradio.
125
+ - [ ] Confirm SRT files are downloadable from Gradio after redeploy.
126
 
127
  ## Phase 7: Full VoiceGate Path
128
 
 
145
  ZeroGPU quota before execution.
146
  - [x] Can a full short-audio workflow run on T4 Small? Yes, on the duplicated
147
  personal Space with a warm ComfyUI process, the test completed in `24.4s`.
148
+ - [x] How do we avoid user-facing failure after hardware changes? The user path
149
+ now checks required model paths and runs `bootstrap_comfy.py --with-models`
150
+ before submitting to ComfyUI.
151
  - [x] Where should large model files live? `/data/voicegate_models`, with
152
  symlinks into ComfyUI's expected model directories.
153
  - [ ] Should the first public demo disable background separation to reduce
docs/work-log.md CHANGED
@@ -841,7 +841,8 @@ and may still hit runtime or memory limits.
841
  The first user-facing Gradio interface now has two tabs:
842
 
843
  - `Translate`: simple user flow with audio upload, target language dropdown,
844
- generated translated dubbing audio output, and status.
 
845
  - `Diagnostics`: retained internal test controls for Prepare, GPU, ComfyUI,
846
  MelBand, VoxCPM TTS, ASR, and full workflow timing.
847
 
@@ -854,6 +855,31 @@ Norwegian, Polish, Portuguese, Russian, Spanish, Swahili, Swedish, Tagalog,
854
  Thai, Turkish, Vietnamese
855
  ```
856
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
857
  ## Remaining Work
858
 
859
  Next recommended steps:
@@ -862,5 +888,6 @@ Next recommended steps:
862
  - SRT split and merge
863
  - full short-audio VoiceGate workflow on the organization ZeroGPU Space after
864
  quota recovers
865
- 2. Polish the first Gradio user interface and add downloadable subtitle outputs.
 
866
  3. Reduce runtime dependency installation and model reload overhead.
 
841
  The first user-facing Gradio interface now has two tabs:
842
 
843
  - `Translate`: simple user flow with audio upload, target language dropdown,
844
+ generated translated dubbing audio output, original/translated subtitle text,
845
+ downloadable `.srt` subtitle files, and status.
846
  - `Diagnostics`: retained internal test controls for Prepare, GPU, ComfyUI,
847
  MelBand, VoxCPM TTS, ASR, and full workflow timing.
848
 
 
855
  Thai, Turkish, Vietnamese
856
  ```
857
 
858
+ User-facing reliability pitfall found on the duplicated personal Space:
859
+
860
+ - After switching/rebuilding hardware, ComfyUI started successfully but the
861
+ MelBand model list was empty.
862
+ - `/prompt` failed with:
863
+
864
+ ```text
865
+ MelBandRoFormerModelLoader 96
866
+ model_name: 'MelBandRoFormer_comfy/MelBandRoformer_fp32.safetensors' not in []
867
+ ```
868
+
869
+ Root cause: the runtime container had ComfyUI/custom nodes, but required model
870
+ files were not present or linked under ComfyUI's model directories yet. Internal
871
+ diagnostic usage can tolerate a manual `Prepare` step, but the user-facing
872
+ `Translate` path must not require that.
873
+
874
+ Mitigation added:
875
+
876
+ - Before running full VoiceGate, the app checks required MelBand, VoxCPM, and
877
+ Qwen3-ASR model paths.
878
+ - If any are missing, it runs `scripts/bootstrap_comfy.py --with-models`
879
+ synchronously and rechecks the paths.
880
+ - If models still cannot be prepared, the app returns a clear preparation error
881
+ instead of a raw ComfyUI prompt-validation failure.
882
+
883
  ## Remaining Work
884
 
885
  Next recommended steps:
 
888
  - SRT split and merge
889
  - full short-audio VoiceGate workflow on the organization ZeroGPU Space after
890
  quota recovers
891
+ 2. Polish the first Gradio user interface and validate the automatic model
892
+ preparation path after Space rebuilds/hardware changes.
893
  3. Reduce runtime dependency installation and model reload overhead.