Upload app.py with huggingface_hub
Browse files
app.py
CHANGED
|
@@ -5,6 +5,7 @@ install `requirements.txt` (torch + diffusers + transformers + gradio + spaces)
|
|
| 5 |
and call:
|
| 6 |
- `app.py:infer` — single image → motion (AnimateDiff MotionLoRA)
|
| 7 |
- `app.py:infer_txt2img` — prompt → 512×512 sprite (SD 1.5)
|
|
|
|
| 8 |
|
| 9 |
`@spaces.GPU` allocates an A10G only for the duration of the call (ZeroGPU
|
| 10 |
model), so the Space is free for the maintainer and shared fairly across users.
|
|
@@ -28,6 +29,7 @@ from studio.backends.animatediff import AnimateDiffAdapter, MOTION_LORA_MAP
|
|
| 28 |
|
| 29 |
_adapter: AnimateDiffAdapter | None = None
|
| 30 |
_sd_pipe = None
|
|
|
|
| 31 |
|
| 32 |
|
| 33 |
def _get_adapter() -> AnimateDiffAdapter:
|
|
@@ -41,18 +43,9 @@ def _get_adapter() -> AnimateDiffAdapter:
|
|
| 41 |
PIXEL_ART_LORA_REPO = "artificialguybr/pixelartredmond-1-5v-pixel-art-loras-for-sd-1-5"
|
| 42 |
PIXEL_ART_LORA_WEIGHT_FILE = "PixelArtRedmond15V-PixelArt-PIXARFK.safetensors"
|
| 43 |
PIXEL_ART_LORA_ADAPTER = "pixart"
|
| 44 |
-
# Trigger words: "pixel art, PixArFK" should appear in the prompt for the LoRA
|
| 45 |
-
# to engage. The probe + UI include them by default.
|
| 46 |
|
| 47 |
|
| 48 |
def _get_sd_pipe():
|
| 49 |
-
"""Lazily load SD 1.5 txt2img pipeline + PixelArtRedmond LoRA (runs inside
|
| 50 |
-
@spaces.GPU context).
|
| 51 |
-
|
| 52 |
-
Cached at module scope so warm calls skip re-loading. Cold start is ~30-60s
|
| 53 |
-
including weight downloads on first call ever (cached in persistent storage
|
| 54 |
-
for subsequent cold starts). LoRA weight is set per-call via set_adapters().
|
| 55 |
-
"""
|
| 56 |
global _sd_pipe
|
| 57 |
if _sd_pipe is not None:
|
| 58 |
return _sd_pipe
|
|
@@ -81,6 +74,29 @@ def _get_sd_pipe():
|
|
| 81 |
return pipe
|
| 82 |
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
@spaces.GPU(duration=90)
|
| 85 |
def infer(
|
| 86 |
image: np.ndarray,
|
|
@@ -126,15 +142,7 @@ def infer_txt2img(
|
|
| 126 |
seed: int,
|
| 127 |
lora_weight: float,
|
| 128 |
) -> str:
|
| 129 |
-
"""Generate a single sprite from a text prompt. Returns path to a PNG.
|
| 130 |
-
|
| 131 |
-
Defaults tuned for pixel-art sprites: 512×512, 25 steps, guidance 7.5,
|
| 132 |
-
LoRA strength 0.9. Caller downscales to target res (256×256 is the Dicer
|
| 133 |
-
sprite size).
|
| 134 |
-
|
| 135 |
-
Prompt must include "pixel art, PixArFK" to engage the LoRA. The UI
|
| 136 |
-
pre-populates these tokens; the probe always includes them.
|
| 137 |
-
"""
|
| 138 |
import torch
|
| 139 |
|
| 140 |
pipe = _get_sd_pipe()
|
|
@@ -158,11 +166,66 @@ def infer_txt2img(
|
|
| 158 |
return f.name
|
| 159 |
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
with gr.Blocks(title="Venture-Studio") as demo:
|
| 162 |
gr.Markdown(
|
| 163 |
"# Venture-Studio · Pixel-Cursor Animation\n"
|
| 164 |
-
"Single image → 24fps animation,
|
| 165 |
-
"Running on Hugging Face ZeroGPU
|
| 166 |
)
|
| 167 |
with gr.Tabs():
|
| 168 |
with gr.Tab("Motion"):
|
|
@@ -229,6 +292,57 @@ with gr.Blocks(title="Venture-Studio") as demo:
|
|
| 229 |
api_name="infer_txt2img",
|
| 230 |
)
|
| 231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
|
| 233 |
if __name__ == "__main__":
|
| 234 |
demo.launch()
|
|
|
|
| 5 |
and call:
|
| 6 |
- `app.py:infer` — single image → motion (AnimateDiff MotionLoRA)
|
| 7 |
- `app.py:infer_txt2img` — prompt → 512×512 sprite (SD 1.5)
|
| 8 |
+
- `app.py:infer_ltx_i2v` — reference portrait → portrait video (LTX-Video I2V)
|
| 9 |
|
| 10 |
`@spaces.GPU` allocates an A10G only for the duration of the call (ZeroGPU
|
| 11 |
model), so the Space is free for the maintainer and shared fairly across users.
|
|
|
|
| 29 |
|
| 30 |
_adapter: AnimateDiffAdapter | None = None
|
| 31 |
_sd_pipe = None
|
| 32 |
+
_ltx_pipe = None
|
| 33 |
|
| 34 |
|
| 35 |
def _get_adapter() -> AnimateDiffAdapter:
|
|
|
|
| 43 |
PIXEL_ART_LORA_REPO = "artificialguybr/pixelartredmond-1-5v-pixel-art-loras-for-sd-1-5"
|
| 44 |
PIXEL_ART_LORA_WEIGHT_FILE = "PixelArtRedmond15V-PixelArt-PIXARFK.safetensors"
|
| 45 |
PIXEL_ART_LORA_ADAPTER = "pixart"
|
|
|
|
|
|
|
| 46 |
|
| 47 |
|
| 48 |
def _get_sd_pipe():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
global _sd_pipe
|
| 50 |
if _sd_pipe is not None:
|
| 51 |
return _sd_pipe
|
|
|
|
| 74 |
return pipe
|
| 75 |
|
| 76 |
|
| 77 |
+
def _get_ltx_pipe():
|
| 78 |
+
"""Lazily load LTX-Video I2V pipeline (bfloat16, CUDA).
|
| 79 |
+
|
| 80 |
+
Model: Lightricks/LTX-Video (~8 GB, cached in persistent storage after first call).
|
| 81 |
+
Supports portrait aspect ratios (height > width) with both dims divisible by 32.
|
| 82 |
+
Frame counts must be of the form 8k+1 (9, 17, 25, 49, 97, 121 ...).
|
| 83 |
+
"""
|
| 84 |
+
global _ltx_pipe
|
| 85 |
+
if _ltx_pipe is not None:
|
| 86 |
+
return _ltx_pipe
|
| 87 |
+
import torch
|
| 88 |
+
from diffusers import LTXImageToVideoPipeline
|
| 89 |
+
pipe = LTXImageToVideoPipeline.from_pretrained(
|
| 90 |
+
"Lightricks/LTX-Video",
|
| 91 |
+
torch_dtype=torch.bfloat16,
|
| 92 |
+
)
|
| 93 |
+
pipe = pipe.to("cuda")
|
| 94 |
+
pipe.set_progress_bar_config(disable=True)
|
| 95 |
+
print("LTX-Video I2V pipeline loaded", flush=True)
|
| 96 |
+
_ltx_pipe = pipe
|
| 97 |
+
return pipe
|
| 98 |
+
|
| 99 |
+
|
| 100 |
@spaces.GPU(duration=90)
|
| 101 |
def infer(
|
| 102 |
image: np.ndarray,
|
|
|
|
| 142 |
seed: int,
|
| 143 |
lora_weight: float,
|
| 144 |
) -> str:
|
| 145 |
+
"""Generate a single sprite from a text prompt. Returns path to a PNG."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
import torch
|
| 147 |
|
| 148 |
pipe = _get_sd_pipe()
|
|
|
|
| 166 |
return f.name
|
| 167 |
|
| 168 |
|
| 169 |
+
@spaces.GPU(duration=150)
|
| 170 |
+
def infer_ltx_i2v(
|
| 171 |
+
image: np.ndarray,
|
| 172 |
+
prompt: str,
|
| 173 |
+
negative_prompt: str,
|
| 174 |
+
height: int,
|
| 175 |
+
width: int,
|
| 176 |
+
num_frames: int,
|
| 177 |
+
num_inference_steps: int,
|
| 178 |
+
guidance_scale: float,
|
| 179 |
+
seed: int,
|
| 180 |
+
) -> str:
|
| 181 |
+
"""LTX-Video image-to-video: reference portrait → portrait video clip.
|
| 182 |
+
|
| 183 |
+
Constraints enforced here (not in UI) so programmatic callers are safe:
|
| 184 |
+
- height and width are rounded up to nearest multiple of 32
|
| 185 |
+
- num_frames is rounded up to nearest 8k+1 value
|
| 186 |
+
|
| 187 |
+
Returns path to an mp4 file at 24fps.
|
| 188 |
+
"""
|
| 189 |
+
import torch
|
| 190 |
+
import imageio.v3 as iio
|
| 191 |
+
|
| 192 |
+
# Enforce divisibility constraints
|
| 193 |
+
h = int(height)
|
| 194 |
+
w = int(width)
|
| 195 |
+
h = ((h + 31) // 32) * 32
|
| 196 |
+
w = ((w + 31) // 32) * 32
|
| 197 |
+
nf = int(num_frames)
|
| 198 |
+
if (nf - 1) % 8 != 0:
|
| 199 |
+
nf = ((nf // 8) * 8) + 1
|
| 200 |
+
|
| 201 |
+
pipe = _get_ltx_pipe()
|
| 202 |
+
pil_img = PILImage.fromarray(image.astype(np.uint8))
|
| 203 |
+
gen = torch.Generator(device="cuda").manual_seed(int(seed))
|
| 204 |
+
|
| 205 |
+
result = pipe(
|
| 206 |
+
image=pil_img,
|
| 207 |
+
prompt=prompt,
|
| 208 |
+
negative_prompt=negative_prompt,
|
| 209 |
+
height=h,
|
| 210 |
+
width=w,
|
| 211 |
+
num_frames=nf,
|
| 212 |
+
num_inference_steps=int(num_inference_steps),
|
| 213 |
+
guidance_scale=float(guidance_scale),
|
| 214 |
+
generator=gen,
|
| 215 |
+
)
|
| 216 |
+
frames_pil = result.frames[0]
|
| 217 |
+
frames_np = np.stack([np.array(f) for f in frames_pil], axis=0)
|
| 218 |
+
|
| 219 |
+
out = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
|
| 220 |
+
iio.imwrite(out.name, frames_np, fps=24)
|
| 221 |
+
return out.name
|
| 222 |
+
|
| 223 |
+
|
| 224 |
with gr.Blocks(title="Venture-Studio") as demo:
|
| 225 |
gr.Markdown(
|
| 226 |
"# Venture-Studio · Pixel-Cursor Animation\n"
|
| 227 |
+
"Single image → 24fps animation, text prompt → sprite, or portrait → locked video.\n"
|
| 228 |
+
"Running on Hugging Face ZeroGPU."
|
| 229 |
)
|
| 230 |
with gr.Tabs():
|
| 231 |
with gr.Tab("Motion"):
|
|
|
|
| 292 |
api_name="infer_txt2img",
|
| 293 |
)
|
| 294 |
|
| 295 |
+
with gr.Tab("Hologram (LTX I2V)"):
|
| 296 |
+
gr.Markdown(
|
| 297 |
+
"### LTX-Video Image-to-Video\n"
|
| 298 |
+
"Reference portrait image → locked-head portrait video clip. "
|
| 299 |
+
"Height must exceed width (portrait). Both dims rounded to nearest 32. "
|
| 300 |
+
"Frames rounded to nearest 8k+1 (9 17 25 49 97 121...)."
|
| 301 |
+
)
|
| 302 |
+
with gr.Row():
|
| 303 |
+
with gr.Column():
|
| 304 |
+
ltx_image = gr.Image(label="Reference portrait", type="numpy", height=384)
|
| 305 |
+
ltx_prompt = gr.Textbox(
|
| 306 |
+
value=(
|
| 307 |
+
"East-Asian man, 30s, dark navy suit, subtle lapel pin, "
|
| 308 |
+
"neutral-formal expression, studio lighting, soft rim light, "
|
| 309 |
+
"portrait frame, subject upper two-thirds of frame. "
|
| 310 |
+
"Speaking naturally, lips 60-70% open, visible lip movement. "
|
| 311 |
+
"Head absolutely still, zero lateral or vertical drift. "
|
| 312 |
+
"Single continuous shot, no cuts. Photorealistic, cinematic."
|
| 313 |
+
),
|
| 314 |
+
lines=4, label="prompt",
|
| 315 |
+
)
|
| 316 |
+
ltx_neg = gr.Textbox(
|
| 317 |
+
value=(
|
| 318 |
+
"head movement, swaying, bobbing, nodding, camera shake, "
|
| 319 |
+
"zoom, pan, closed mouth, jump cut, cartoon, deformed, blurry"
|
| 320 |
+
),
|
| 321 |
+
lines=2, label="negative_prompt",
|
| 322 |
+
)
|
| 323 |
+
with gr.Accordion("Advanced", open=False):
|
| 324 |
+
ltx_height = gr.Slider(256, 768, value=576, step=32,
|
| 325 |
+
label="height (portrait: height > width, div-32)")
|
| 326 |
+
ltx_width = gr.Slider(256, 768, value=320, step=32,
|
| 327 |
+
label="width (div-32)")
|
| 328 |
+
ltx_frames = gr.Slider(9, 121, value=121, step=8,
|
| 329 |
+
label="num_frames (8k+1: 9 17 25 49 97 121)")
|
| 330 |
+
ltx_steps = gr.Slider(10, 50, value=25, step=1, label="num_inference_steps")
|
| 331 |
+
ltx_guidance = gr.Slider(1.0, 10.0, value=3.0, step=0.5, label="guidance_scale")
|
| 332 |
+
ltx_seed = gr.Number(value=42, precision=0, label="seed")
|
| 333 |
+
ltx_run = gr.Button("Generate", variant="primary")
|
| 334 |
+
with gr.Column():
|
| 335 |
+
ltx_out = gr.Video(label="Output", autoplay=True, loop=True)
|
| 336 |
+
|
| 337 |
+
ltx_run.click(
|
| 338 |
+
infer_ltx_i2v,
|
| 339 |
+
inputs=[ltx_image, ltx_prompt, ltx_neg,
|
| 340 |
+
ltx_height, ltx_width, ltx_frames,
|
| 341 |
+
ltx_steps, ltx_guidance, ltx_seed],
|
| 342 |
+
outputs=ltx_out,
|
| 343 |
+
api_name="infer_ltx_i2v",
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
|
| 347 |
if __name__ == "__main__":
|
| 348 |
demo.launch()
|