Upload studio/backends/hf_space.py with huggingface_hub
Browse files- studio/backends/hf_space.py +77 -51
studio/backends/hf_space.py
CHANGED
|
@@ -1,21 +1,4 @@
|
|
| 1 |
-
"""HF Spaces backend adapter
|
| 2 |
-
|
| 3 |
-
Local CLI client that calls a deployed Hugging Face Space hosting the studio's
|
| 4 |
-
inference pipeline. The Space provides the GPU; the local machine needs only
|
| 5 |
-
`gradio_client` + a HF token. No torch, no diffusers, no model downloads locally.
|
| 6 |
-
|
| 7 |
-
Token resolution order (most-specific wins):
|
| 8 |
-
1. explicit `hf_token=` param
|
| 9 |
-
2. `HF_TOKEN` env var
|
| 10 |
-
3. `huggingface_hub.HfFolder.get_token()` (reads ~/.cache/huggingface/token)
|
| 11 |
-
|
| 12 |
-
The deployed Space is expected to expose a Gradio function (default api_name
|
| 13 |
-
`/infer`) with the signature:
|
| 14 |
-
(image_pil, preset_name, num_frames, num_inference_steps, guidance_scale,
|
| 15 |
-
prompt, negative_prompt, seed) -> video_filepath
|
| 16 |
-
|
| 17 |
-
Pair this with the host-mode `app.py` at the repo root for end-to-end ownership.
|
| 18 |
-
"""
|
| 19 |
from __future__ import annotations
|
| 20 |
from dataclasses import dataclass
|
| 21 |
import os
|
|
@@ -65,7 +48,6 @@ class HFSpaceAdapter:
|
|
| 65 |
def bind_motion_exemplar(
|
| 66 |
self, cursor: PixelCursor, exemplar: Sequence[str]
|
| 67 |
) -> PixelCursor:
|
| 68 |
-
"""Same preset semantics as AnimateDiff; the Space carries the same MotionLoRA map."""
|
| 69 |
if len(exemplar) != 1:
|
| 70 |
raise ValueError(
|
| 71 |
"hf_space.bind_motion_exemplar expects exactly one preset name."
|
|
@@ -108,10 +90,6 @@ class HFSpaceAdapter:
|
|
| 108 |
else "NONE"
|
| 109 |
),
|
| 110 |
"gradio_client_installed": _gradio_client_available(),
|
| 111 |
-
"note": (
|
| 112 |
-
"Anonymous calls work but quota is tight; an HF token raises the limit. "
|
| 113 |
-
"ZeroGPU cold-start ~10-30s; subsequent calls ~5-15s warm."
|
| 114 |
-
),
|
| 115 |
}
|
| 116 |
|
| 117 |
def write_motion(self, cursor: PixelCursor, motion_spec: dict | None = None) -> FrameStack:
|
|
@@ -152,26 +130,63 @@ class HFSpaceAdapter:
|
|
| 152 |
return _video_path_to_framestack(video_path, preset)
|
| 153 |
|
| 154 |
|
| 155 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
try:
|
| 157 |
-
|
| 158 |
except ImportError as e:
|
| 159 |
-
raise ImportError(
|
| 160 |
-
"Loading the Space's video response requires imageio. Install with:\n"
|
| 161 |
-
" pip install -e '.[hf]'"
|
| 162 |
-
) from e
|
| 163 |
-
frames = iio.imread(str(video_path))
|
| 164 |
-
if frames.ndim != 4:
|
| 165 |
-
raise ValueError(f"unexpected video shape from Space: {frames.shape}")
|
| 166 |
-
return _new_framestack(frames.astype(np.uint8), fps=8, name=f"hf_space:{preset}")
|
| 167 |
|
|
|
|
|
|
|
| 168 |
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
|
| 177 |
def generate_sprite_via_space(
|
|
@@ -188,18 +203,7 @@ def generate_sprite_via_space(
|
|
| 188 |
hf_token: Optional[str] = None,
|
| 189 |
api_name: str = "/infer_txt2img",
|
| 190 |
) -> PILImage.Image:
|
| 191 |
-
"""Call the Space's txt2img endpoint and return the generated sprite.
|
| 192 |
-
|
| 193 |
-
This is a free-standing helper (not bound to the cursor/motion abstraction).
|
| 194 |
-
A sprite is a fresh artifact, not a transform — wrapping it in a PixelCursor
|
| 195 |
-
would add ceremony without benefit.
|
| 196 |
-
|
| 197 |
-
Prompt should include the PixelArtRedmond trigger words "pixel art, PixArFK"
|
| 198 |
-
to engage the loaded LoRA. lora_weight scales LoRA strength (0.0 disables,
|
| 199 |
-
1.5 is the upper end; default 0.9).
|
| 200 |
-
|
| 201 |
-
Raises ImportError if gradio_client is missing locally.
|
| 202 |
-
"""
|
| 203 |
try:
|
| 204 |
from gradio_client import Client
|
| 205 |
except ImportError as e:
|
|
@@ -219,3 +223,25 @@ def generate_sprite_via_space(
|
|
| 219 |
api_name=api_name,
|
| 220 |
)
|
| 221 |
return PILImage.open(png_path).convert("RGB")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HF Spaces backend adapter — extended with LTX-Video I2V endpoint."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
from dataclasses import dataclass
|
| 4 |
import os
|
|
|
|
| 48 |
def bind_motion_exemplar(
|
| 49 |
self, cursor: PixelCursor, exemplar: Sequence[str]
|
| 50 |
) -> PixelCursor:
|
|
|
|
| 51 |
if len(exemplar) != 1:
|
| 52 |
raise ValueError(
|
| 53 |
"hf_space.bind_motion_exemplar expects exactly one preset name."
|
|
|
|
| 90 |
else "NONE"
|
| 91 |
),
|
| 92 |
"gradio_client_installed": _gradio_client_available(),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
}
|
| 94 |
|
| 95 |
def write_motion(self, cursor: PixelCursor, motion_spec: dict | None = None) -> FrameStack:
|
|
|
|
| 130 |
return _video_path_to_framestack(video_path, preset)
|
| 131 |
|
| 132 |
|
| 133 |
+
def generate_headlocked_via_space(
|
| 134 |
+
image_path: str,
|
| 135 |
+
*,
|
| 136 |
+
space_id: str,
|
| 137 |
+
prompt: str,
|
| 138 |
+
negative_prompt: str = (
|
| 139 |
+
"head movement, swaying, bobbing, nodding, camera shake, "
|
| 140 |
+
"zoom, pan, jump cut, cartoon, deformed, blurry"
|
| 141 |
+
),
|
| 142 |
+
height: int = 576,
|
| 143 |
+
width: int = 320,
|
| 144 |
+
num_frames: int = 121,
|
| 145 |
+
num_inference_steps: int = 25,
|
| 146 |
+
guidance_scale: float = 3.0,
|
| 147 |
+
seed: int = 42,
|
| 148 |
+
hf_token: Optional[str] = None,
|
| 149 |
+
api_name: str = "/infer_ltx_i2v",
|
| 150 |
+
) -> str:
|
| 151 |
+
"""Call the Space's LTX I2V endpoint and return the path to the generated mp4.
|
| 152 |
+
|
| 153 |
+
Designed for hologram head-locked clip generation. Default params:
|
| 154 |
+
- 576×320 portrait (9:16-ish, both div-32, confirmed working on A10G)
|
| 155 |
+
- 121 frames = 5.04s @ 24fps (8*15+1)
|
| 156 |
+
- guidance_scale=3.0 (LTX-Video optimal range is 2-4)
|
| 157 |
+
|
| 158 |
+
The Space enforces div-32 and 8k+1 constraints internally — caller values
|
| 159 |
+
are rounded up, not rejected.
|
| 160 |
+
|
| 161 |
+
Example:
|
| 162 |
+
path = generate_headlocked_via_space(
|
| 163 |
+
"/path/to/chancellor-li-hq-smoothLIGHT-2144x3840.jpg",
|
| 164 |
+
space_id="AlterProgramming/venture-studio",
|
| 165 |
+
prompt="East-Asian man, 30s, dark navy suit ... head absolutely still ...",
|
| 166 |
+
)
|
| 167 |
+
# path is a local mp4 file → copy to v2-compatible/ and run motion_grammar
|
| 168 |
+
"""
|
| 169 |
try:
|
| 170 |
+
from gradio_client import Client, handle_file
|
| 171 |
except ImportError as e:
|
| 172 |
+
raise ImportError(INSTALL_HINT) from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
+
token = _resolve_hf_token(hf_token)
|
| 175 |
+
client = Client(space_id, token=token)
|
| 176 |
|
| 177 |
+
result = client.predict(
|
| 178 |
+
handle_file(image_path),
|
| 179 |
+
prompt,
|
| 180 |
+
negative_prompt,
|
| 181 |
+
float(height),
|
| 182 |
+
float(width),
|
| 183 |
+
float(num_frames),
|
| 184 |
+
float(num_inference_steps),
|
| 185 |
+
float(guidance_scale),
|
| 186 |
+
float(seed),
|
| 187 |
+
api_name=api_name,
|
| 188 |
+
)
|
| 189 |
+
return result if isinstance(result, str) else result[0]
|
| 190 |
|
| 191 |
|
| 192 |
def generate_sprite_via_space(
|
|
|
|
| 203 |
hf_token: Optional[str] = None,
|
| 204 |
api_name: str = "/infer_txt2img",
|
| 205 |
) -> PILImage.Image:
|
| 206 |
+
"""Call the Space's txt2img endpoint and return the generated sprite."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
try:
|
| 208 |
from gradio_client import Client
|
| 209 |
except ImportError as e:
|
|
|
|
| 223 |
api_name=api_name,
|
| 224 |
)
|
| 225 |
return PILImage.open(png_path).convert("RGB")
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def _video_path_to_framestack(video_path: str | Path, preset: str) -> FrameStack:
|
| 229 |
+
try:
|
| 230 |
+
import imageio.v3 as iio
|
| 231 |
+
except ImportError as e:
|
| 232 |
+
raise ImportError(
|
| 233 |
+
"Loading the Space's video response requires imageio. Install with:\n"
|
| 234 |
+
" pip install -e '.[hf]'"
|
| 235 |
+
) from e
|
| 236 |
+
frames = iio.imread(str(video_path))
|
| 237 |
+
if frames.ndim != 4:
|
| 238 |
+
raise ValueError(f"unexpected video shape from Space: {frames.shape}")
|
| 239 |
+
return _new_framestack(frames.astype(np.uint8), fps=8, name=f"hf_space:{preset}")
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def _gradio_client_available() -> bool:
|
| 243 |
+
try:
|
| 244 |
+
import gradio_client # noqa: F401
|
| 245 |
+
return True
|
| 246 |
+
except ImportError:
|
| 247 |
+
return False
|