Spaces:
Running on Zero
feat(spaces+style): hf_oauth + gr.Progress + style 2-input + DWPose deps
Browse filesSpaces — 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.
- README.md +2 -1
- app.py +23 -7
- backend.py +28 -1
- modes.py +2 -0
- requirements.txt +2 -0
|
@@ -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:
|
|
|
|
| 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
|
|
@@ -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,
|
|
|
|
|
|
|
| 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.
|
| 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.
|
| 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
|
|
@@ -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
|
|
@@ -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 |
|
|
@@ -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 |
|