Daankular commited on
Commit
b8af0b1
·
verified ·
1 Parent(s): 5b1a120

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +80 -106
app.py CHANGED
@@ -1,48 +1,38 @@
1
  """
2
  Sulphur — Image to Video (HF Spaces).
3
-
4
- POC-proven pattern:
5
- - Worker spawned via multiprocessing.get_context("spawn").Process (ZeroGPU
6
- patches multiprocessing, so children inherit GPU access)
7
- - @spaces.GPU(duration=1) writes a signal file; worker inits CUDA within
8
- that 1-second window and retains the context for the full generation
9
- - Triton is stubbed out in the worker so Quanto uses its pure-PyTorch
10
- INT8 path — no torch._C._cuda_init() calls after lease expiry
11
- - Billing: 1 second per generation regardless of inference time
12
  """
13
 
14
- import json
15
- import multiprocessing
16
  import os
 
 
17
  import shutil
18
  import tempfile
19
  import threading
20
- import time
21
  from pathlib import Path
22
 
23
  import gradio as gr
24
  import spaces
25
 
26
- from generate import _worker_entrypoint
27
-
28
- _HF_TOKEN = os.environ.get("HF_TOKEN")
29
- _PERSISTENT = Path("/data") if Path("/data").exists() else Path(tempfile.gettempdir())
30
- WAN2GP_ROOT = _PERSISTENT / "Wan2GP"
31
- CKPTS_DIR = WAN2GP_ROOT / "ckpts"
32
- LORAS_DIR = WAN2GP_ROOT / "loras" / "ltx2"
33
  FINETUNES_DIR = WAN2GP_ROOT / "finetunes"
34
-
35
- os.environ["WAN2GP_ROOT"] = str(WAN2GP_ROOT)
36
 
37
  SULPHUR_ASSETS = [
38
  ("SulphurAI/Sulphur-2-base", "sulphur_distil_bf16.safetensors", CKPTS_DIR),
39
  ]
40
  LTX_ASSETS = [
41
  ("SulphurAI/Sulphur-2-base", "experimental/sulphur_experimental_lora_v1.safetensors", LORAS_DIR),
42
- ("DeepBeepMeep/LTX-2", "ltx-2.3-22b-distilled-lora-384.safetensors", LORAS_DIR),
43
- ("DeepBeepMeep/LTX-2", "ltx-2.3-22b_vae.safetensors", CKPTS_DIR),
44
- ("DeepBeepMeep/LTX-2", "ltx-2.3-22b_text_embedding_projection.safetensors", CKPTS_DIR),
45
- ("DeepBeepMeep/LTX-2", "ltx-2.3-22b_embeddings_connector.safetensors", CKPTS_DIR),
46
  ]
47
 
48
  SULPHUR_FINETUNE = {
@@ -52,14 +42,15 @@ SULPHUR_FINETUNE = {
52
  "architecture": "ltx2_22B",
53
  "parent_model_type": "ltx2_22B",
54
  "description": "LTX-2.3 fine-tuned i2v. Distilled checkpoint.",
 
55
  "URLs": [str(CKPTS_DIR / "sulphur_distil_bf16.safetensors")],
56
  "preload_URLs": [],
57
  },
58
  "num_inference_steps": 8,
59
  "video_length": 81,
60
  "resolution": "832x480",
61
- "guidance_scale": 5.0,
62
- "alt_guidance_scale": 5.0,
63
  }
64
 
65
  _setup_lock = threading.Lock()
@@ -69,13 +60,14 @@ _setup_done = False
69
  def _download(repo_id, filename, dest_dir):
70
  from huggingface_hub import hf_hub_download
71
  dest_dir.mkdir(parents=True, exist_ok=True)
72
- dest = dest_dir / Path(filename).name
73
  if dest.exists():
74
  print(f"[download] cached: {dest.name}")
75
  return
76
  print(f"[download] {repo_id}/{filename}")
77
  hf_hub_download(repo_id=repo_id, filename=filename,
78
  local_dir=str(dest_dir), token=_HF_TOKEN)
 
79
  downloaded = dest_dir / filename
80
  if downloaded.exists() and not dest.exists():
81
  shutil.move(str(downloaded), str(dest))
@@ -91,7 +83,6 @@ def setup():
91
  if not (WAN2GP_ROOT / "shared" / "api.py").exists():
92
  WAN2GP_ROOT.mkdir(parents=True, exist_ok=True)
93
  print("[setup] Cloning Wan2GP...")
94
- import subprocess
95
  subprocess.run(
96
  ["git", "clone", "--depth=1",
97
  "https://github.com/deepbeepmeep/Wan2GP.git", str(WAN2GP_ROOT)],
@@ -101,9 +92,10 @@ def setup():
101
  for repo, fname, dest in SULPHUR_ASSETS + LTX_ASSETS:
102
  _download(repo, fname, dest)
103
 
 
104
  _gemma_folder = "gemma-3-12b-it-qat-q4_0-unquantized"
105
- _gemma_file = f"{_gemma_folder}_quanto_bf16_int8.safetensors"
106
- gemma_dest = CKPTS_DIR / _gemma_folder / _gemma_file
107
  if not gemma_dest.exists():
108
  from huggingface_hub import hf_hub_download
109
  print("[download] Gemma text encoder...")
@@ -117,9 +109,7 @@ def setup():
117
  print("[download] cached: Gemma text encoder")
118
 
119
  FINETUNES_DIR.mkdir(parents=True, exist_ok=True)
120
- (FINETUNES_DIR / "sulphur_2_base.json").write_text(
121
- json.dumps(SULPHUR_FINETUNE, indent=2)
122
- )
123
  print("[setup] Done.")
124
 
125
 
@@ -128,93 +118,77 @@ setup()
128
  RESOLUTIONS = ["832x480", "480x832", "640x640", "1024x576", "576x1024"]
129
 
130
 
131
- @spaces.GPU(duration=1)
132
- def _signal_cuda_init(signal_path):
133
- """1-second lease — signals worker to init CUDA, then expires."""
134
- Path(signal_path).write_text("go")
135
- time.sleep(0.8)
136
-
137
-
138
  def generate_video(image, prompt, resolution, steps, guidance_scale, frames, seed):
139
  if image is None:
140
  raise gr.Error("Please upload an image.")
141
  if not prompt.strip():
142
  raise gr.Error("Please enter a prompt.")
143
 
144
- signal_path = tempfile.mktemp(suffix=".signal")
145
- result_path = tempfile.mktemp(suffix=".json")
146
- out_dir = tempfile.mkdtemp()
147
- out_file = os.path.join(out_dir, "output.mp4")
148
-
149
- ctx = multiprocessing.get_context("spawn")
150
- proc = ctx.Process(
151
- target=_worker_entrypoint,
152
- args=(image, prompt, out_file, "sulphur-2",
153
- int(steps), float(guidance_scale), int(frames),
154
- resolution, int(seed), signal_path, result_path),
155
- daemon=False,
156
- )
157
- proc.start()
158
-
159
- log_lines = ["Worker spawned, acquiring GPU lease..."]
160
- yield None, "\n".join(log_lines)
161
-
162
- _signal_cuda_init(signal_path)
163
-
164
- log_lines.append("GPU lease expired. Worker retains CUDA context and is generating...")
165
- yield None, "\n".join(log_lines)
166
-
167
- last_log_len = 0
168
- deadline = time.monotonic() + 600
169
-
170
- while time.monotonic() < deadline:
171
- time.sleep(2)
172
-
173
- if os.path.exists(result_path):
174
- try:
175
- with open(result_path) as f:
176
- data = json.load(f)
177
- new_entries = data.get("log", [])[last_log_len:]
178
- if new_entries:
179
- log_lines.extend(new_entries)
180
- last_log_len += len(new_entries)
181
- yield None, "\n".join(log_lines[-40:])
182
- if data.get("done"):
183
- break
184
- except Exception:
185
- pass
186
- else:
187
- yield None, "\n".join(log_lines)
188
-
189
- proc.join(timeout=10)
190
- if proc.is_alive():
191
- proc.terminate()
192
-
193
- for path in (signal_path, result_path):
194
- try:
195
- os.unlink(path)
196
- except Exception:
197
- pass
198
-
199
- if os.path.exists(out_file):
200
- final = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
201
- shutil.copy2(out_file, final.name)
202
- yield final.name, "\n".join(log_lines) + "\n\n[DONE]"
203
- else:
204
- yield None, "\n".join(log_lines) + "\n\n[ERROR] No output file produced."
205
 
206
 
207
  with gr.Blocks(title="Sulphur — Image to Video") as demo:
208
- gr.Markdown("# Sulphur — Image to Video\nUsing Experimental LoRA v1 + Distillation LoRA")
209
  with gr.Row():
210
  with gr.Column(scale=1):
211
  image_in = gr.Image(type="filepath", label="Input Image")
212
  prompt_in = gr.Textbox(label="Prompt", placeholder="Describe the motion…", lines=3)
213
  with gr.Accordion("Advanced", open=False):
214
  resolution_dd = gr.Dropdown(RESOLUTIONS, value="832x480", label="Resolution")
215
- steps_sl = gr.Slider(1, 50, value=8, step=1, label="Steps")
216
  guidance_sl = gr.Slider(1.0, 10.0, value=5.0, step=0.5, label="Guidance Scale")
217
- frames_sl = gr.Slider(17, 257, value=81, step=8, label="Frames")
218
  seed_num = gr.Number(value=-1, label="Seed (-1 = random)", precision=0)
219
  run_btn = gr.Button("Generate", variant="primary")
220
  with gr.Column(scale=1):
@@ -228,4 +202,4 @@ with gr.Blocks(title="Sulphur — Image to Video") as demo:
228
  )
229
 
230
  if __name__ == "__main__":
231
- demo.launch(theme=gr.themes.Soft())
 
1
  """
2
  Sulphur — Image to Video (HF Spaces).
3
+ Clones Wan2GP and downloads models on first run.
4
+ Generation is handled by generate.py called as a subprocess inside @spaces.GPU.
 
 
 
 
 
 
 
5
  """
6
 
 
 
7
  import os
8
+ import sys
9
+ import subprocess
10
  import shutil
11
  import tempfile
12
  import threading
13
+ import json
14
  from pathlib import Path
15
 
16
  import gradio as gr
17
  import spaces
18
 
19
+ _HF_TOKEN = os.environ.get("HF_TOKEN")
20
+ _PERSISTENT = Path("/data") if Path("/data").exists() else Path(tempfile.gettempdir())
21
+ WAN2GP_ROOT = _PERSISTENT / "Wan2GP"
22
+ CKPTS_DIR = WAN2GP_ROOT / "ckpts"
23
+ LORAS_DIR = WAN2GP_ROOT / "loras" / "ltx2"
 
 
24
  FINETUNES_DIR = WAN2GP_ROOT / "finetunes"
25
+ GENERATE_PY = Path(__file__).parent / "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", LORAS_DIR),
33
+ ("DeepBeepMeep/LTX-2", "ltx-2.3-22b_vae.safetensors", CKPTS_DIR),
34
+ ("DeepBeepMeep/LTX-2", "ltx-2.3-22b_text_embedding_projection.safetensors", CKPTS_DIR),
35
+ ("DeepBeepMeep/LTX-2", "ltx-2.3-22b_embeddings_connector.safetensors", CKPTS_DIR),
36
  ]
37
 
38
  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": 3.5,
53
+ "alt_guidance_scale": 3.5,
54
  }
55
 
56
  _setup_lock = threading.Lock()
 
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 # flat — strip any subfolder
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))
 
83
  if not (WAN2GP_ROOT / "shared" / "api.py").exists():
84
  WAN2GP_ROOT.mkdir(parents=True, exist_ok=True)
85
  print("[setup] Cloning Wan2GP...")
 
86
  subprocess.run(
87
  ["git", "clone", "--depth=1",
88
  "https://github.com/deepbeepmeep/Wan2GP.git", str(WAN2GP_ROOT)],
 
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 = f"{_gemma_folder}_quanto_bf16_int8.safetensors"
98
+ gemma_dest = CKPTS_DIR / _gemma_folder / _gemma_file
99
  if not gemma_dest.exists():
100
  from huggingface_hub import hf_hub_download
101
  print("[download] Gemma text encoder...")
 
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(json.dumps(SULPHUR_FINETUNE, indent=2))
 
 
113
  print("[setup] Done.")
114
 
115
 
 
118
  RESOLUTIONS = ["832x480", "480x832", "640x640", "1024x576", "576x1024"]
119
 
120
 
121
+ @spaces.GPU(duration=80)
 
 
 
 
 
 
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
+ out_file = Path(tempfile.mkdtemp()) / "output.mp4"
129
+ env = {**os.environ, "WAN2GP_ROOT": str(WAN2GP_ROOT)}
130
+
131
+ cmd = [
132
+ sys.executable, str(GENERATE_PY),
133
+ "--image", image,
134
+ "--prompt", prompt,
135
+ "--output", str(out_file),
136
+ "--model", "sulphur-2",
137
+ "--seed", str(int(seed)),
138
+ "--resolution", resolution,
139
+ "--steps", str(int(steps)),
140
+ "--guidance_scale", str(float(guidance_scale)),
141
+ "--frames", str(int(frames)),
142
+ ]
143
+
144
+ log_lines = []
145
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
146
+ text=True, bufsize=0, env=env)
147
+
148
+ buf = ""
149
+ while True:
150
+ chunk = proc.stdout.read(256)
151
+ if not chunk:
152
+ break
153
+ buf += chunk
154
+ # Split on \r or \n — tqdm uses \r to overwrite progress lines
155
+ parts = buf.replace("\r", "\n").split("\n")
156
+ buf = parts[-1]
157
+ for part in parts[:-1]:
158
+ stripped = part.strip()
159
+ if not stripped:
160
+ continue
161
+ # Overwrite last line if it looks like a progress bar update
162
+ if log_lines and ("%" in stripped or "it/s" in stripped or "step" in stripped.lower()):
163
+ log_lines[-1] = stripped
164
+ else:
165
+ log_lines.append(stripped)
166
+ print(stripped)
167
+ yield None, "\n".join(log_lines[-30:])
168
+
169
+ proc.wait()
170
+ log = "\n".join(log_lines)
171
+
172
+ if proc.returncode != 0 or not out_file.exists():
173
+ yield None, log + "\n\n[ERROR] Generation failed."
174
+ return
175
+
176
+ final = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
177
+ shutil.copy2(out_file, final.name)
178
+ yield final.name, log + "\n\n[DONE]"
 
 
 
 
 
 
 
 
 
 
179
 
180
 
181
  with gr.Blocks(title="Sulphur — Image to Video") as demo:
182
+ gr.Markdown("# Sulphur — Image to Video\nUsing New Experimental Lora v1")
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, step=1, label="Steps")
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, label="Frames")
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):
 
202
  )
203
 
204
  if __name__ == "__main__":
205
+ demo.launch(theme=gr.themes.Soft())