techfreakworm commited on
Commit
27ffbbc
·
unverified ·
1 Parent(s): c3b8732

feat(spaces+style): hf_oauth + gr.Progress + style 2-input + DWPose deps

Browse files

Spaces — Pro quota attribution: enable hf_oauth in README YAML and add a
gr.LoginButton in the drawer (rendered only when OAUTH_CLIENT_ID is set, so
local dev stays clean). Without OAuth the Space scheduler bucketed Pro users
as signed-in/free (300s/day), causing "GPU quota exceeded" errors after a
few generations while the profile dashboard still showed 0/25 used.

Spaces — progress bar through @spaces.GPU subprocess: thread a gr.Progress
instance from _make_handler -> _on_generate -> backend.submit -> _execute_workflow.
The existing event-based ProgressEvent flow can't cross the @spaces.GPU
subprocess boundary on Spaces (asyncio.run_coroutine_threadsafe targets the
parent loop the subprocess never inherits). gr.Progress is the only progress
channel ZeroGPU's IPC wraps. Inside _execute_workflow, install a ComfyUI
PROGRESS_BAR_HOOK that calls progress(v/t, desc=...) and chains to the
prior hook so the local event-driven status banner keeps working.

Style mode — actually use the uploaded image: previously _style_parameterize
patched only the source video; the IC-LoRA reference image stayed at the
baked-in seed (IMG-20210721-WA0008.jpg). Now patches NODE_IMAGE_1 and
exposes a "Style reference" upload alongside "Source video" in the UI;
also resets VHS_LoadVideo.skip_first_frames from the workflow's baked-in
266 to 0 so user-uploaded clips don't fail with "No frames generated".

DWPose runtime deps: comfyui_controlnet_aux's dwpose preprocessor needs
matplotlib (already in requirements), scikit-image (hand keypoint detector),
and onnxruntime (acceleration; without it falls back to OpenCV/CPU which is
much slower). All three are now declared in requirements.txt.

Files changed (5) hide show
  1. README.md +2 -1
  2. app.py +23 -7
  3. backend.py +28 -1
  4. modes.py +2 -0
  5. requirements.txt +2 -0
README.md CHANGED
@@ -8,7 +8,8 @@ sdk_version: "5.50.0"
8
  app_file: app.py
9
  python_version: "3.11"
10
  suggested_hardware: zero-a10g
11
- hf_oauth: false
 
12
  preload_from_hub:
13
  - Comfy-Org/ltx-2 split_files/text_encoders/gemma_3_12B_it.safetensors
14
  - Kijai/LTX2.3_comfy diffusion_models/ltx-2.3-22b-dev_transformer_only_bf16.safetensors,loras/ltx-2.3-22b-distilled-lora-dynamic_fro09_avg_rank_105_bf16.safetensors,text_encoders/ltx-2.3_text_projection_bf16.safetensors,vae/LTX23_audio_vae_bf16.safetensors,vae/LTX23_video_vae_bf16.safetensors,vae/taeltx2_3.safetensors
 
8
  app_file: app.py
9
  python_version: "3.11"
10
  suggested_hardware: zero-a10g
11
+ hf_oauth: true
12
+ hf_oauth_expiration_minutes: 480
13
  preload_from_hub:
14
  - Comfy-Org/ltx-2 split_files/text_encoders/gemma_3_12B_it.safetensors
15
  - Kijai/LTX2.3_comfy diffusion_models/ltx-2.3-22b-dev_transformer_only_bf16.safetensors,loras/ltx-2.3-22b-distilled-lora-dynamic_fro09_avg_rank_105_bf16.safetensors,text_encoders/ltx-2.3_text_projection_bf16.safetensors,vae/LTX23_audio_vae_bf16.safetensors,vae/LTX23_video_vae_bf16.safetensors,vae/taeltx2_3.safetensors
app.py CHANGED
@@ -466,6 +466,13 @@ def build_app() -> gr.Blocks:
466
  # Drawer (drawer behaves as fixed sidebar ≥1024 px;
467
  # absolute-positioned overlay <1024 px — see _CUSTOM_CSS).
468
  with gr.Column(scale=1, min_width=200, elem_classes=["aio-drawer"]):
 
 
 
 
 
 
 
469
  gr.Markdown("Modes", elem_classes=["aio-drawer-heading"])
470
  mode_buttons = {
471
  name: gr.Button(
@@ -632,6 +639,7 @@ def _render_one_mode(name: str) -> dict:
632
  handles["first_frame"] = gr.Image(label="First frame", type="filepath")
633
  handles["last_frame"] = gr.Image(label="Last frame", type="filepath")
634
  elif name == "style":
 
635
  handles["input_video"] = gr.Video(label="Source video")
636
 
637
  handles["preset"] = ui.preset_bar()
@@ -751,8 +759,14 @@ def _seconds_to_frames(seconds: float, fps: int) -> int:
751
  return max(9, int(round(float(seconds) * float(fps) / 8) * 8) + 1)
752
 
753
 
754
- async def _on_generate(mode_name: str, **inputs: Any):
755
- """Generate handler — async generator yielding (status_html, video_path)."""
 
 
 
 
 
 
756
  mode = modes.MODE_REGISTRY[mode_name]
757
 
758
  fps = int(inputs.get("fps", 24))
@@ -867,7 +881,9 @@ async def _on_generate(mode_name: str, **inputs: Any):
867
 
868
  timed_out = False
869
  async for event in backend.submit(
870
- mode_name, workflow, preset=preset, duration_multiplier=multiplier
 
 
871
  ):
872
  if (
873
  isinstance(event, backend_module.ErrorEvent)
@@ -894,7 +910,7 @@ def _input_keys_for_mode(mode_name: str, h: dict) -> list[str]:
894
  elif mode_name == "keyframe":
895
  base.extend(["first_frame", "last_frame"])
896
  elif mode_name == "style":
897
- base.append("input_video")
898
  base.append("negative_prompt")
899
  base.extend(["camera_lora", "camera_strength", "detailer_on", "detailer_strength"])
900
  if h["lora"].ic_lora is not None:
@@ -918,7 +934,7 @@ def _collect_inputs_for_mode(mode_name: str, h: dict) -> list:
918
  elif mode_name == "keyframe":
919
  base.extend([h["first_frame"], h["last_frame"]])
920
  elif mode_name == "style":
921
- base.append(h["input_video"])
922
  base.append(h["negative_prompt"])
923
  base.extend([
924
  h["lora"].camera_lora, h["lora"].camera_strength,
@@ -934,9 +950,9 @@ def _collect_inputs_for_mode(mode_name: str, h: dict) -> list:
934
  def _make_handler(mode_name: str, h: dict):
935
  keys = _input_keys_for_mode(mode_name, h)
936
 
937
- async def handler(*values):
938
  kwargs = dict(zip(keys, values, strict=False))
939
- async for output in _on_generate(mode_name, **kwargs):
940
  yield output
941
 
942
  return handler
 
466
  # Drawer (drawer behaves as fixed sidebar ≥1024 px;
467
  # absolute-positioned overlay <1024 px — see _CUSTOM_CSS).
468
  with gr.Column(scale=1, min_width=200, elem_classes=["aio-drawer"]):
469
+ if os.getenv("OAUTH_CLIENT_ID"):
470
+ gr.Markdown("Account", elem_classes=["aio-drawer-heading"])
471
+ gr.LoginButton(
472
+ value="Sign in for Pro GPU quota",
473
+ size="sm",
474
+ elem_classes=["aio-login-btn"],
475
+ )
476
  gr.Markdown("Modes", elem_classes=["aio-drawer-heading"])
477
  mode_buttons = {
478
  name: gr.Button(
 
639
  handles["first_frame"] = gr.Image(label="First frame", type="filepath")
640
  handles["last_frame"] = gr.Image(label="Last frame", type="filepath")
641
  elif name == "style":
642
+ handles["image"] = gr.Image(label="Style reference", type="filepath")
643
  handles["input_video"] = gr.Video(label="Source video")
644
 
645
  handles["preset"] = ui.preset_bar()
 
759
  return max(9, int(round(float(seconds) * float(fps) / 8) * 8) + 1)
760
 
761
 
762
+ async def _on_generate(mode_name: str, *, progress: Any = None, **inputs: Any):
763
+ """Generate handler — async generator yielding (status_html, video_path).
764
+
765
+ `progress` is a `gr.Progress` instance injected by Gradio. It's the only
766
+ progress channel that survives the @spaces.GPU subprocess boundary on HF
767
+ Spaces; we forward it to the backend so ComfyUI's per-step counter renders
768
+ a real progress bar instead of a generic Gradio spinner.
769
+ """
770
  mode = modes.MODE_REGISTRY[mode_name]
771
 
772
  fps = int(inputs.get("fps", 24))
 
881
 
882
  timed_out = False
883
  async for event in backend.submit(
884
+ mode_name, workflow,
885
+ preset=preset, duration_multiplier=multiplier,
886
+ progress=progress,
887
  ):
888
  if (
889
  isinstance(event, backend_module.ErrorEvent)
 
910
  elif mode_name == "keyframe":
911
  base.extend(["first_frame", "last_frame"])
912
  elif mode_name == "style":
913
+ base.extend(["image", "input_video"])
914
  base.append("negative_prompt")
915
  base.extend(["camera_lora", "camera_strength", "detailer_on", "detailer_strength"])
916
  if h["lora"].ic_lora is not None:
 
934
  elif mode_name == "keyframe":
935
  base.extend([h["first_frame"], h["last_frame"]])
936
  elif mode_name == "style":
937
+ base.extend([h["image"], h["input_video"]])
938
  base.append(h["negative_prompt"])
939
  base.extend([
940
  h["lora"].camera_lora, h["lora"].camera_strength,
 
950
  def _make_handler(mode_name: str, h: dict):
951
  keys = _input_keys_for_mode(mode_name, h)
952
 
953
+ async def handler(*values, progress=gr.Progress()):
954
  kwargs = dict(zip(keys, values, strict=False))
955
+ async for output in _on_generate(mode_name, progress=progress, **kwargs):
956
  yield output
957
 
958
  return handler
backend.py CHANGED
@@ -129,13 +129,39 @@ def _execute_workflow(
129
  mode: str,
130
  preset: str,
131
  multiplier: float = 1.0,
 
132
  ) -> str:
133
  """Run the workflow on GPU and return the path of the first video output.
134
 
135
  Returns just the video path (a plain string, picklable across the
136
  @spaces.GPU subprocess boundary). The `mode`, `preset`, and `multiplier`
137
  args are consumed by `_duration_for` to estimate the GPU slot to reserve.
 
 
 
 
 
 
 
138
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  executor.execute(
140
  workflow,
141
  prompt_id="ltx23-aio",
@@ -351,6 +377,7 @@ class ComfyUILibraryBackend:
351
  preset: str = "balanced",
352
  duration_multiplier: float = 1.0,
353
  gpu_duration: int = 0, # legacy, ignored (now derived from preset+frames)
 
354
  ) -> AsyncIterator[Any]:
355
  """Run a workflow end-to-end. Yields Download/Progress/Output/Error events.
356
 
@@ -431,7 +458,7 @@ class ComfyUILibraryBackend:
431
  # light calls get fast queue priority while heavy ones reserve
432
  # real headroom. Off-Spaces it's a plain call.
433
  video_path = _execute_workflow(
434
- self._executor, workflow, output_ids, mode, preset, duration_multiplier,
435
  )
436
  # Fallback: if history_result didn't surface a path (rare on
437
  # Spaces — happens when ZeroGPU's subprocess boundary drops
 
129
  mode: str,
130
  preset: str,
131
  multiplier: float = 1.0,
132
+ progress: Any = None,
133
  ) -> str:
134
  """Run the workflow on GPU and return the path of the first video output.
135
 
136
  Returns just the video path (a plain string, picklable across the
137
  @spaces.GPU subprocess boundary). The `mode`, `preset`, and `multiplier`
138
  args are consumed by `_duration_for` to estimate the GPU slot to reserve.
139
+
140
+ `progress` is an optional `gr.Progress` instance. It's the only progress
141
+ channel that crosses the @spaces.GPU subprocess boundary on HF Spaces —
142
+ Gradio + the `spaces` library wrap it with cross-process IPC. When set,
143
+ we mirror ComfyUI's step counter into it via the global progress hook,
144
+ chaining to whatever hook was already installed (so the local event-based
145
+ status banner keeps working alongside).
146
  """
147
+ if progress is not None:
148
+ import comfy.utils as _cu
149
+ _saved_hook = getattr(_cu, "PROGRESS_BAR_HOOK", None)
150
+
151
+ def _gp_hook(value, total, _preview=None, **_kw):
152
+ try:
153
+ v, t = int(value), int(total)
154
+ progress(v / max(t, 1), desc=f"Sampling step {v}/{t}")
155
+ except Exception:
156
+ pass
157
+ if _saved_hook is not None:
158
+ try:
159
+ _saved_hook(value, total, _preview)
160
+ except Exception:
161
+ pass
162
+
163
+ _cu.set_progress_bar_global_hook(_gp_hook)
164
+
165
  executor.execute(
166
  workflow,
167
  prompt_id="ltx23-aio",
 
377
  preset: str = "balanced",
378
  duration_multiplier: float = 1.0,
379
  gpu_duration: int = 0, # legacy, ignored (now derived from preset+frames)
380
+ progress: Any = None,
381
  ) -> AsyncIterator[Any]:
382
  """Run a workflow end-to-end. Yields Download/Progress/Output/Error events.
383
 
 
458
  # light calls get fast queue priority while heavy ones reserve
459
  # real headroom. Off-Spaces it's a plain call.
460
  video_path = _execute_workflow(
461
+ self._executor, workflow, output_ids, mode, preset, duration_multiplier, progress,
462
  )
463
  # Fallback: if history_result didn't surface a path (rare on
464
  # Spaces — happens when ZeroGPU's subprocess boundary drops
modes.py CHANGED
@@ -124,7 +124,9 @@ def _keyframe_parameterize(inp: dict[str, Any]) -> list[Patch]:
124
 
125
  def _style_parameterize(inp: dict[str, Any]) -> list[Patch]:
126
  return _shared_patches(inp, "style") + [
 
127
  (NODE_VIDEO, "video", inp["input_video"]),
 
128
  ]
129
 
130
 
 
124
 
125
  def _style_parameterize(inp: dict[str, Any]) -> list[Patch]:
126
  return _shared_patches(inp, "style") + [
127
+ (NODE_IMAGE_1, "image", inp["image"]),
128
  (NODE_VIDEO, "video", inp["input_video"]),
129
+ (NODE_VIDEO, "skip_first_frames", 0),
130
  ]
131
 
132
 
requirements.txt CHANGED
@@ -49,6 +49,8 @@ gguf # ComfyUI-GGUF (UnetLoaderGGUF)
49
  imageio_ffmpeg # ComfyUI-VideoHelperSuite (video write/read backend)
50
  opencv-python # ComfyUI_LayerStyle, multiple custom nodes
51
  matplotlib # comfyui_controlnet_aux dwpose / pose preprocessors
 
 
52
  diffusers # ComfyUI-SeedVR2 (used during init even when the node isn't called)
53
  yt-dlp # ComfyUI-MediaMixer (init-time import)
54
 
 
49
  imageio_ffmpeg # ComfyUI-VideoHelperSuite (video write/read backend)
50
  opencv-python # ComfyUI_LayerStyle, multiple custom nodes
51
  matplotlib # comfyui_controlnet_aux dwpose / pose preprocessors
52
+ scikit-image # comfyui_controlnet_aux dwpose hand keypoint detector
53
+ onnxruntime # comfyui_controlnet_aux dwpose accelerator (CPU/CoreML on Mac, CUDA on Spaces)
54
  diffusers # ComfyUI-SeedVR2 (used during init even when the node isn't called)
55
  yt-dlp # ComfyUI-MediaMixer (init-time import)
56