AlterProgramming commited on
Commit
fad497e
·
verified ·
1 Parent(s): ccc57f0

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +134 -20
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, or text prompt → sprite, via PixelCursor.\n"
165
- "Running on Hugging Face ZeroGPU (free A10G)."
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()