Commit ·
2cc8b04
1
Parent(s): dd08490
Add subtitle outputs and model preflight
Browse files- app.py +117 -9
- docs/todo.md +6 -2
- 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) ->
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
try:
|
| 666 |
-
|
|
|
|
|
|
|
| 667 |
if not output_audio:
|
| 668 |
lines.append("warning=No output audio file was found in ComfyUI history.")
|
| 669 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
- [
|
| 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,
|
|
|
|
| 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
|
|
|
|
| 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.
|