Spaces:
Paused
Paused
daKhosa commited on
Commit ·
b840bd7
1
Parent(s): 144ea05
Switch to ZeroGPU POC pattern: duration=1 billing, subprocess retains CUDA context
Browse files- app.py: pre-spawn generate.py from main process, @spaces.GPU(duration=1) only
for CUDA init signal; stream incremental logs from JSON result file
- generate.py: add --signal-path polling, --result-path incremental JSON logging,
add distillation LoRA (ltx-2.3-22b-distilled-lora-384.safetensors at 0.5),
experimental LoRA at 1.0; guidance_scale default 5.0
- app.py +98 -69
- generate.py +44 -8
app.py
CHANGED
|
@@ -1,38 +1,43 @@
|
|
| 1 |
"""
|
| 2 |
Sulphur — Image to Video (HF Spaces).
|
| 3 |
-
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
|
|
|
|
|
|
|
| 7 |
import os
|
| 8 |
-
import sys
|
| 9 |
-
import subprocess
|
| 10 |
import shutil
|
|
|
|
|
|
|
| 11 |
import tempfile
|
| 12 |
import threading
|
| 13 |
-
import
|
| 14 |
from pathlib import Path
|
| 15 |
|
| 16 |
import gradio as gr
|
| 17 |
import spaces
|
| 18 |
|
| 19 |
-
_HF_TOKEN
|
| 20 |
-
_PERSISTENT
|
| 21 |
-
WAN2GP_ROOT
|
| 22 |
-
CKPTS_DIR
|
| 23 |
-
LORAS_DIR
|
| 24 |
FINETUNES_DIR = WAN2GP_ROOT / "finetunes"
|
| 25 |
-
GENERATE_PY
|
| 26 |
|
| 27 |
SULPHUR_ASSETS = [
|
| 28 |
("SulphurAI/Sulphur-2-base", "sulphur_distil_bf16.safetensors", CKPTS_DIR),
|
| 29 |
]
|
| 30 |
LTX_ASSETS = [
|
| 31 |
("SulphurAI/Sulphur-2-base", "experimental/sulphur_experimental_lora_v1.safetensors", LORAS_DIR),
|
| 32 |
-
("DeepBeepMeep/LTX-2", "ltx-2.3-22b-distilled-lora-384.safetensors",
|
| 33 |
-
("DeepBeepMeep/LTX-2", "ltx-2.3-22b_vae.safetensors",
|
| 34 |
-
("DeepBeepMeep/LTX-2", "ltx-2.3-22b_text_embedding_projection.safetensors",
|
| 35 |
-
("DeepBeepMeep/LTX-2", "ltx-2.3-22b_embeddings_connector.safetensors",
|
| 36 |
]
|
| 37 |
|
| 38 |
SULPHUR_FINETUNE = {
|
|
@@ -42,15 +47,14 @@ SULPHUR_FINETUNE = {
|
|
| 42 |
"architecture": "ltx2_22B",
|
| 43 |
"parent_model_type": "ltx2_22B",
|
| 44 |
"description": "LTX-2.3 fine-tuned i2v. Distilled checkpoint.",
|
| 45 |
-
# Full distilled model — do NOT also preload the rank-768 LoRA (README: use one or the other)
|
| 46 |
"URLs": [str(CKPTS_DIR / "sulphur_distil_bf16.safetensors")],
|
| 47 |
"preload_URLs": [],
|
| 48 |
},
|
| 49 |
"num_inference_steps": 8,
|
| 50 |
"video_length": 81,
|
| 51 |
"resolution": "832x480",
|
| 52 |
-
"guidance_scale":
|
| 53 |
-
"alt_guidance_scale":
|
| 54 |
}
|
| 55 |
|
| 56 |
_setup_lock = threading.Lock()
|
|
@@ -60,14 +64,13 @@ _setup_done = False
|
|
| 60 |
def _download(repo_id, filename, dest_dir):
|
| 61 |
from huggingface_hub import hf_hub_download
|
| 62 |
dest_dir.mkdir(parents=True, exist_ok=True)
|
| 63 |
-
dest = dest_dir / Path(filename).name
|
| 64 |
if dest.exists():
|
| 65 |
print(f"[download] cached: {dest.name}")
|
| 66 |
return
|
| 67 |
print(f"[download] {repo_id}/{filename}")
|
| 68 |
hf_hub_download(repo_id=repo_id, filename=filename,
|
| 69 |
local_dir=str(dest_dir), token=_HF_TOKEN)
|
| 70 |
-
# hf_hub_download preserves subfolder structure; flatten to dest_dir root
|
| 71 |
downloaded = dest_dir / filename
|
| 72 |
if downloaded.exists() and not dest.exists():
|
| 73 |
shutil.move(str(downloaded), str(dest))
|
|
@@ -92,10 +95,9 @@ def setup():
|
|
| 92 |
for repo, fname, dest in SULPHUR_ASSETS + LTX_ASSETS:
|
| 93 |
_download(repo, fname, dest)
|
| 94 |
|
| 95 |
-
# Gemma text encoder — must stay in its subfolder (Wan2GP looks there by name)
|
| 96 |
_gemma_folder = "gemma-3-12b-it-qat-q4_0-unquantized"
|
| 97 |
-
_gemma_file
|
| 98 |
-
gemma_dest
|
| 99 |
if not gemma_dest.exists():
|
| 100 |
from huggingface_hub import hf_hub_download
|
| 101 |
print("[download] Gemma text encoder...")
|
|
@@ -109,7 +111,9 @@ def setup():
|
|
| 109 |
print("[download] cached: Gemma text encoder")
|
| 110 |
|
| 111 |
FINETUNES_DIR.mkdir(parents=True, exist_ok=True)
|
| 112 |
-
(FINETUNES_DIR / "sulphur_2_base.json").write_text(
|
|
|
|
|
|
|
| 113 |
print("[setup] Done.")
|
| 114 |
|
| 115 |
|
|
@@ -118,77 +122,102 @@ setup()
|
|
| 118 |
RESOLUTIONS = ["832x480", "480x832", "640x640", "1024x576", "576x1024"]
|
| 119 |
|
| 120 |
|
| 121 |
-
@spaces.GPU(duration=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
def generate_video(image, prompt, resolution, steps, guidance_scale, frames, seed):
|
| 123 |
if image is None:
|
| 124 |
raise gr.Error("Please upload an image.")
|
| 125 |
if not prompt.strip():
|
| 126 |
raise gr.Error("Please enter a prompt.")
|
| 127 |
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
| 130 |
|
| 131 |
cmd = [
|
| 132 |
sys.executable, str(GENERATE_PY),
|
| 133 |
-
"--image",
|
| 134 |
-
"--prompt",
|
| 135 |
-
"--output",
|
| 136 |
-
"--model",
|
| 137 |
-
"--seed",
|
| 138 |
-
"--resolution",
|
| 139 |
-
"--steps",
|
| 140 |
"--guidance_scale", str(float(guidance_scale)),
|
| 141 |
-
"--frames",
|
|
|
|
|
|
|
| 142 |
]
|
| 143 |
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
|
| 169 |
proc.wait()
|
| 170 |
-
log = "\n".join(log_lines)
|
| 171 |
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
| 175 |
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
|
| 181 |
with gr.Blocks(title="Sulphur — Image to Video") as demo:
|
| 182 |
-
gr.Markdown("# Sulphur — Image to Video\nUsing
|
| 183 |
with gr.Row():
|
| 184 |
with gr.Column(scale=1):
|
| 185 |
image_in = gr.Image(type="filepath", label="Input Image")
|
| 186 |
prompt_in = gr.Textbox(label="Prompt", placeholder="Describe the motion…", lines=3)
|
| 187 |
with gr.Accordion("Advanced", open=False):
|
| 188 |
resolution_dd = gr.Dropdown(RESOLUTIONS, value="832x480", label="Resolution")
|
| 189 |
-
steps_sl = gr.Slider(1, 50, value=8,
|
| 190 |
guidance_sl = gr.Slider(1.0, 10.0, value=5.0, step=0.5, label="Guidance Scale")
|
| 191 |
-
frames_sl = gr.Slider(17, 257, value=81, step=8,
|
| 192 |
seed_num = gr.Number(value=-1, label="Seed (-1 = random)", precision=0)
|
| 193 |
run_btn = gr.Button("Generate", variant="primary")
|
| 194 |
with gr.Column(scale=1):
|
|
|
|
| 1 |
"""
|
| 2 |
Sulphur — Image to Video (HF Spaces).
|
| 3 |
+
|
| 4 |
+
POC-proven pattern: subprocess is pre-spawned from the main Gradio process,
|
| 5 |
+
@spaces.GPU(duration=1) is used only to signal CUDA initialisation.
|
| 6 |
+
The subprocess retains GPU access for the full generation duration.
|
| 7 |
+
Billing cost: 1 second per generation regardless of inference time.
|
| 8 |
"""
|
| 9 |
|
| 10 |
+
import json
|
| 11 |
+
import multiprocessing
|
| 12 |
import os
|
|
|
|
|
|
|
| 13 |
import shutil
|
| 14 |
+
import subprocess
|
| 15 |
+
import sys
|
| 16 |
import tempfile
|
| 17 |
import threading
|
| 18 |
+
import time
|
| 19 |
from pathlib import Path
|
| 20 |
|
| 21 |
import gradio as gr
|
| 22 |
import spaces
|
| 23 |
|
| 24 |
+
_HF_TOKEN = os.environ.get("HF_TOKEN")
|
| 25 |
+
_PERSISTENT = Path("/data") if Path("/data").exists() else Path(tempfile.gettempdir())
|
| 26 |
+
WAN2GP_ROOT = _PERSISTENT / "Wan2GP"
|
| 27 |
+
CKPTS_DIR = WAN2GP_ROOT / "ckpts"
|
| 28 |
+
LORAS_DIR = WAN2GP_ROOT / "loras" / "ltx2"
|
| 29 |
FINETUNES_DIR = WAN2GP_ROOT / "finetunes"
|
| 30 |
+
GENERATE_PY = Path(__file__).parent / "generate.py"
|
| 31 |
|
| 32 |
SULPHUR_ASSETS = [
|
| 33 |
("SulphurAI/Sulphur-2-base", "sulphur_distil_bf16.safetensors", CKPTS_DIR),
|
| 34 |
]
|
| 35 |
LTX_ASSETS = [
|
| 36 |
("SulphurAI/Sulphur-2-base", "experimental/sulphur_experimental_lora_v1.safetensors", LORAS_DIR),
|
| 37 |
+
("DeepBeepMeep/LTX-2", "ltx-2.3-22b-distilled-lora-384.safetensors", LORAS_DIR),
|
| 38 |
+
("DeepBeepMeep/LTX-2", "ltx-2.3-22b_vae.safetensors", CKPTS_DIR),
|
| 39 |
+
("DeepBeepMeep/LTX-2", "ltx-2.3-22b_text_embedding_projection.safetensors", CKPTS_DIR),
|
| 40 |
+
("DeepBeepMeep/LTX-2", "ltx-2.3-22b_embeddings_connector.safetensors", CKPTS_DIR),
|
| 41 |
]
|
| 42 |
|
| 43 |
SULPHUR_FINETUNE = {
|
|
|
|
| 47 |
"architecture": "ltx2_22B",
|
| 48 |
"parent_model_type": "ltx2_22B",
|
| 49 |
"description": "LTX-2.3 fine-tuned i2v. Distilled checkpoint.",
|
|
|
|
| 50 |
"URLs": [str(CKPTS_DIR / "sulphur_distil_bf16.safetensors")],
|
| 51 |
"preload_URLs": [],
|
| 52 |
},
|
| 53 |
"num_inference_steps": 8,
|
| 54 |
"video_length": 81,
|
| 55 |
"resolution": "832x480",
|
| 56 |
+
"guidance_scale": 5.0,
|
| 57 |
+
"alt_guidance_scale": 5.0,
|
| 58 |
}
|
| 59 |
|
| 60 |
_setup_lock = threading.Lock()
|
|
|
|
| 64 |
def _download(repo_id, filename, dest_dir):
|
| 65 |
from huggingface_hub import hf_hub_download
|
| 66 |
dest_dir.mkdir(parents=True, exist_ok=True)
|
| 67 |
+
dest = dest_dir / Path(filename).name
|
| 68 |
if dest.exists():
|
| 69 |
print(f"[download] cached: {dest.name}")
|
| 70 |
return
|
| 71 |
print(f"[download] {repo_id}/{filename}")
|
| 72 |
hf_hub_download(repo_id=repo_id, filename=filename,
|
| 73 |
local_dir=str(dest_dir), token=_HF_TOKEN)
|
|
|
|
| 74 |
downloaded = dest_dir / filename
|
| 75 |
if downloaded.exists() and not dest.exists():
|
| 76 |
shutil.move(str(downloaded), str(dest))
|
|
|
|
| 95 |
for repo, fname, dest in SULPHUR_ASSETS + LTX_ASSETS:
|
| 96 |
_download(repo, fname, dest)
|
| 97 |
|
|
|
|
| 98 |
_gemma_folder = "gemma-3-12b-it-qat-q4_0-unquantized"
|
| 99 |
+
_gemma_file = f"{_gemma_folder}_quanto_bf16_int8.safetensors"
|
| 100 |
+
gemma_dest = CKPTS_DIR / _gemma_folder / _gemma_file
|
| 101 |
if not gemma_dest.exists():
|
| 102 |
from huggingface_hub import hf_hub_download
|
| 103 |
print("[download] Gemma text encoder...")
|
|
|
|
| 111 |
print("[download] cached: Gemma text encoder")
|
| 112 |
|
| 113 |
FINETUNES_DIR.mkdir(parents=True, exist_ok=True)
|
| 114 |
+
(FINETUNES_DIR / "sulphur_2_base.json").write_text(
|
| 115 |
+
json.dumps(SULPHUR_FINETUNE, indent=2)
|
| 116 |
+
)
|
| 117 |
print("[setup] Done.")
|
| 118 |
|
| 119 |
|
|
|
|
| 122 |
RESOLUTIONS = ["832x480", "480x832", "640x640", "1024x576", "576x1024"]
|
| 123 |
|
| 124 |
|
| 125 |
+
@spaces.GPU(duration=1)
|
| 126 |
+
def _signal_cuda_init(signal_path):
|
| 127 |
+
"""Acquire a 1-second GPU lease just long enough for the worker to init CUDA."""
|
| 128 |
+
Path(signal_path).write_text("go")
|
| 129 |
+
time.sleep(0.5)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
def generate_video(image, prompt, resolution, steps, guidance_scale, frames, seed):
|
| 133 |
if image is None:
|
| 134 |
raise gr.Error("Please upload an image.")
|
| 135 |
if not prompt.strip():
|
| 136 |
raise gr.Error("Please enter a prompt.")
|
| 137 |
|
| 138 |
+
signal_path = tempfile.mktemp(suffix=".signal")
|
| 139 |
+
result_path = tempfile.mktemp(suffix=".json")
|
| 140 |
+
out_dir = tempfile.mkdtemp()
|
| 141 |
+
out_file = os.path.join(out_dir, "output.mp4")
|
| 142 |
|
| 143 |
cmd = [
|
| 144 |
sys.executable, str(GENERATE_PY),
|
| 145 |
+
"--image", image,
|
| 146 |
+
"--prompt", prompt,
|
| 147 |
+
"--output", out_file,
|
| 148 |
+
"--model", "sulphur-2",
|
| 149 |
+
"--seed", str(int(seed)),
|
| 150 |
+
"--resolution", resolution,
|
| 151 |
+
"--steps", str(int(steps)),
|
| 152 |
"--guidance_scale", str(float(guidance_scale)),
|
| 153 |
+
"--frames", str(int(frames)),
|
| 154 |
+
"--signal-path", signal_path,
|
| 155 |
+
"--result-path", result_path,
|
| 156 |
]
|
| 157 |
|
| 158 |
+
env = {**os.environ, "WAN2GP_ROOT": str(WAN2GP_ROOT)}
|
| 159 |
+
|
| 160 |
+
# Spawn from main Gradio process (not inside ZeroGPU daemon — avoids daemon-child restriction)
|
| 161 |
+
proc = subprocess.Popen(cmd, env=env)
|
| 162 |
+
|
| 163 |
+
log_lines = ["Worker spawned, acquiring GPU lease..."]
|
| 164 |
+
yield None, "\n".join(log_lines)
|
| 165 |
+
|
| 166 |
+
# 1-second lease — signals worker to init CUDA, then expires
|
| 167 |
+
_signal_cuda_init(signal_path)
|
| 168 |
+
|
| 169 |
+
log_lines.append("GPU lease expired. Worker retains CUDA context and is generating...")
|
| 170 |
+
yield None, "\n".join(log_lines)
|
| 171 |
+
|
| 172 |
+
last_log_len = 0
|
| 173 |
+
deadline = time.monotonic() + 600 # 10 min hard timeout
|
| 174 |
+
|
| 175 |
+
while time.monotonic() < deadline:
|
| 176 |
+
time.sleep(2)
|
| 177 |
+
|
| 178 |
+
if os.path.exists(result_path):
|
| 179 |
+
try:
|
| 180 |
+
with open(result_path) as f:
|
| 181 |
+
data = json.load(f)
|
| 182 |
+
new_entries = data.get("log", [])[last_log_len:]
|
| 183 |
+
if new_entries:
|
| 184 |
+
log_lines.extend(new_entries)
|
| 185 |
+
last_log_len += len(new_entries)
|
| 186 |
+
yield None, "\n".join(log_lines[-40:])
|
| 187 |
+
if data.get("done"):
|
| 188 |
+
break
|
| 189 |
+
except Exception:
|
| 190 |
+
pass
|
| 191 |
+
else:
|
| 192 |
+
yield None, "\n".join(log_lines)
|
| 193 |
|
| 194 |
proc.wait()
|
|
|
|
| 195 |
|
| 196 |
+
for path in (signal_path, result_path):
|
| 197 |
+
try:
|
| 198 |
+
os.unlink(path)
|
| 199 |
+
except Exception:
|
| 200 |
+
pass
|
| 201 |
|
| 202 |
+
if os.path.exists(out_file):
|
| 203 |
+
final = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
|
| 204 |
+
shutil.copy2(out_file, final.name)
|
| 205 |
+
yield final.name, "\n".join(log_lines) + "\n\n[DONE]"
|
| 206 |
+
else:
|
| 207 |
+
yield None, "\n".join(log_lines) + "\n\n[ERROR] No output file produced."
|
| 208 |
|
| 209 |
|
| 210 |
with gr.Blocks(title="Sulphur — Image to Video") as demo:
|
| 211 |
+
gr.Markdown("# Sulphur — Image to Video\nUsing Experimental LoRA v1 + Distillation LoRA")
|
| 212 |
with gr.Row():
|
| 213 |
with gr.Column(scale=1):
|
| 214 |
image_in = gr.Image(type="filepath", label="Input Image")
|
| 215 |
prompt_in = gr.Textbox(label="Prompt", placeholder="Describe the motion…", lines=3)
|
| 216 |
with gr.Accordion("Advanced", open=False):
|
| 217 |
resolution_dd = gr.Dropdown(RESOLUTIONS, value="832x480", label="Resolution")
|
| 218 |
+
steps_sl = gr.Slider(1, 50, value=8, step=1, label="Steps")
|
| 219 |
guidance_sl = gr.Slider(1.0, 10.0, value=5.0, step=0.5, label="Guidance Scale")
|
| 220 |
+
frames_sl = gr.Slider(17, 257, value=81, step=8, label="Frames")
|
| 221 |
seed_num = gr.Number(value=-1, label="Seed (-1 = random)", precision=0)
|
| 222 |
run_btn = gr.Button("Generate", variant="primary")
|
| 223 |
with gr.Column(scale=1):
|
generate.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
| 1 |
"""
|
| 2 |
-
HF Spaces version of generate.py —
|
| 3 |
-
Called as a subprocess from app.py
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
import argparse
|
|
|
|
| 7 |
import os
|
| 8 |
import sys
|
|
|
|
| 9 |
from pathlib import Path
|
| 10 |
|
| 11 |
WAN2GP_ROOT = Path(os.environ.get("WAN2GP_ROOT", "/tmp/Wan2GP"))
|
|
@@ -23,9 +26,21 @@ DEFAULTS = {
|
|
| 23 |
},
|
| 24 |
}
|
| 25 |
|
|
|
|
|
|
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
|
| 31 |
def parse_args():
|
|
@@ -39,18 +54,32 @@ def parse_args():
|
|
| 39 |
ap.add_argument("--frames", type=int, default=None)
|
| 40 |
ap.add_argument("--resolution", default=None)
|
| 41 |
ap.add_argument("--seed", type=int, default=-1)
|
|
|
|
|
|
|
| 42 |
return ap.parse_args()
|
| 43 |
|
| 44 |
|
| 45 |
def main():
|
|
|
|
|
|
|
| 46 |
args = parse_args()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
model_type = MODEL_SHORTHANDS.get(args.model, args.model)
|
| 49 |
defaults = DEFAULTS.get(model_type, DEFAULTS["sulphur_2_base"])
|
| 50 |
|
| 51 |
image_path = str(Path(args.image.strip()).resolve())
|
| 52 |
if not Path(image_path).exists():
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
| 54 |
sys.exit(1)
|
| 55 |
|
| 56 |
resolution = args.resolution or defaults["resolution"]
|
|
@@ -83,9 +112,10 @@ def main():
|
|
| 83 |
"image_prompt_type": "S",
|
| 84 |
"input_video_strength": 1.0,
|
| 85 |
"activated_loras": [
|
|
|
|
| 86 |
"sulphur_experimental_lora_v1.safetensors",
|
| 87 |
],
|
| 88 |
-
"loras_multipliers": ["0.5"],
|
| 89 |
}
|
| 90 |
|
| 91 |
p(f"Model: {model_type}")
|
|
@@ -102,7 +132,7 @@ def main():
|
|
| 102 |
output_dir = Path(args.output).parent
|
| 103 |
output_dir.mkdir(parents=True, exist_ok=True)
|
| 104 |
|
| 105 |
-
p("Starting
|
| 106 |
session = WanGPSession(root=WAN2GP_ROOT, output_dir=output_dir, console_output=True)
|
| 107 |
|
| 108 |
p("Running generation...")
|
|
@@ -115,7 +145,6 @@ def main():
|
|
| 115 |
if src and Path(src).exists():
|
| 116 |
output_file = src
|
| 117 |
|
| 118 |
-
# Fallback: scan the output dir for any video file Wan2GP may have written
|
| 119 |
if output_file is None:
|
| 120 |
candidates = sorted(output_dir.glob("**/*.mp4"), key=lambda f: f.stat().st_mtime, reverse=True)
|
| 121 |
if candidates:
|
|
@@ -130,10 +159,17 @@ def main():
|
|
| 130 |
p(f"No output found in {output_dir}")
|
| 131 |
if result.errors:
|
| 132 |
p(f"Errors: {result.errors}")
|
|
|
|
|
|
|
|
|
|
| 133 |
sys.exit(1)
|
| 134 |
|
| 135 |
session.close()
|
| 136 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
if __name__ == "__main__":
|
| 139 |
main()
|
|
|
|
| 1 |
"""
|
| 2 |
+
HF Spaces version of generate.py — paths adapted for /data/Wan2GP.
|
| 3 |
+
Called as a subprocess from app.py.
|
| 4 |
+
Waits for --signal-path before touching CUDA, writes incremental JSON to --result-path.
|
| 5 |
"""
|
| 6 |
|
| 7 |
import argparse
|
| 8 |
+
import json
|
| 9 |
import os
|
| 10 |
import sys
|
| 11 |
+
import time
|
| 12 |
from pathlib import Path
|
| 13 |
|
| 14 |
WAN2GP_ROOT = Path(os.environ.get("WAN2GP_ROOT", "/tmp/Wan2GP"))
|
|
|
|
| 26 |
},
|
| 27 |
}
|
| 28 |
|
| 29 |
+
_log_entries = []
|
| 30 |
+
_result_path = None
|
| 31 |
|
| 32 |
+
|
| 33 |
+
def p(msg):
|
| 34 |
+
ts = time.strftime("%H:%M:%S")
|
| 35 |
+
entry = f"[{ts}] {msg}"
|
| 36 |
+
_log_entries.append(entry)
|
| 37 |
+
print(entry, flush=True)
|
| 38 |
+
if _result_path:
|
| 39 |
+
try:
|
| 40 |
+
with open(_result_path, "w") as f:
|
| 41 |
+
json.dump({"log": _log_entries, "done": False}, f)
|
| 42 |
+
except Exception:
|
| 43 |
+
pass
|
| 44 |
|
| 45 |
|
| 46 |
def parse_args():
|
|
|
|
| 54 |
ap.add_argument("--frames", type=int, default=None)
|
| 55 |
ap.add_argument("--resolution", default=None)
|
| 56 |
ap.add_argument("--seed", type=int, default=-1)
|
| 57 |
+
ap.add_argument("--signal-path", default=None, dest="signal_path")
|
| 58 |
+
ap.add_argument("--result-path", default=None, dest="result_path")
|
| 59 |
return ap.parse_args()
|
| 60 |
|
| 61 |
|
| 62 |
def main():
|
| 63 |
+
global _result_path
|
| 64 |
+
|
| 65 |
args = parse_args()
|
| 66 |
+
_result_path = args.result_path
|
| 67 |
+
|
| 68 |
+
if args.signal_path:
|
| 69 |
+
p("Waiting for GPU lease signal...")
|
| 70 |
+
while not Path(args.signal_path).exists():
|
| 71 |
+
time.sleep(0.05)
|
| 72 |
+
p("Signal received — GPU lease active, initialising CUDA...")
|
| 73 |
|
| 74 |
model_type = MODEL_SHORTHANDS.get(args.model, args.model)
|
| 75 |
defaults = DEFAULTS.get(model_type, DEFAULTS["sulphur_2_base"])
|
| 76 |
|
| 77 |
image_path = str(Path(args.image.strip()).resolve())
|
| 78 |
if not Path(image_path).exists():
|
| 79 |
+
p(f"Fatal: image not found: {image_path}")
|
| 80 |
+
if _result_path:
|
| 81 |
+
with open(_result_path, "w") as f:
|
| 82 |
+
json.dump({"log": _log_entries, "done": True, "error": "image not found"}, f)
|
| 83 |
sys.exit(1)
|
| 84 |
|
| 85 |
resolution = args.resolution or defaults["resolution"]
|
|
|
|
| 112 |
"image_prompt_type": "S",
|
| 113 |
"input_video_strength": 1.0,
|
| 114 |
"activated_loras": [
|
| 115 |
+
"ltx-2.3-22b-distilled-lora-384.safetensors",
|
| 116 |
"sulphur_experimental_lora_v1.safetensors",
|
| 117 |
],
|
| 118 |
+
"loras_multipliers": ["0.5", "1.0"],
|
| 119 |
}
|
| 120 |
|
| 121 |
p(f"Model: {model_type}")
|
|
|
|
| 132 |
output_dir = Path(args.output).parent
|
| 133 |
output_dir.mkdir(parents=True, exist_ok=True)
|
| 134 |
|
| 135 |
+
p("Starting WanGPSession...")
|
| 136 |
session = WanGPSession(root=WAN2GP_ROOT, output_dir=output_dir, console_output=True)
|
| 137 |
|
| 138 |
p("Running generation...")
|
|
|
|
| 145 |
if src and Path(src).exists():
|
| 146 |
output_file = src
|
| 147 |
|
|
|
|
| 148 |
if output_file is None:
|
| 149 |
candidates = sorted(output_dir.glob("**/*.mp4"), key=lambda f: f.stat().st_mtime, reverse=True)
|
| 150 |
if candidates:
|
|
|
|
| 159 |
p(f"No output found in {output_dir}")
|
| 160 |
if result.errors:
|
| 161 |
p(f"Errors: {result.errors}")
|
| 162 |
+
if _result_path:
|
| 163 |
+
with open(_result_path, "w") as f:
|
| 164 |
+
json.dump({"log": _log_entries, "done": True, "error": "no output"}, f)
|
| 165 |
sys.exit(1)
|
| 166 |
|
| 167 |
session.close()
|
| 168 |
|
| 169 |
+
if _result_path:
|
| 170 |
+
with open(_result_path, "w") as f:
|
| 171 |
+
json.dump({"log": _log_entries, "done": True}, f)
|
| 172 |
+
|
| 173 |
|
| 174 |
if __name__ == "__main__":
|
| 175 |
main()
|