Spaces:
Running on Zero
Running on Zero
File size: 9,063 Bytes
590e6fe afa8596 590e6fe afa8596 590e6fe afa8596 590e6fe afa8596 590e6fe afa8596 590e6fe 635c454 590e6fe 635c454 590e6fe | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | """My Motion Video AI β LTX-Video (text-to-video + image-to-video) on ZeroGPU.
Deploy as a Hugging Face Space with the ZeroGPU hardware option selected.
Everything runs in the cloud: your laptop only needs a browser.
"""
import os
import random
import tempfile
import gradio as gr
import spaces
import torch
from diffusers import LTXImageToVideoPipeline, LTXPipeline
from diffusers.utils import export_to_video
# The 2B LTX-Video checkpoint (0.9.5): much lighter than the 13B main repo
# (~15GB bf16 vs ~38GB), so it fits the 48GB ZeroGPU slice comfortably.
# Un-gated, official diffusers structure, supports text-to-video + image-to-video.
MODEL_ID = "Lightricks/LTX-Video-0.9.5"
DEFAULT_NEGATIVE = (
"worst quality, inconsistent motion, blurry, jittery, distorted, "
"low resolution, watermark, flicker"
)
RESOLUTIONS = {
"Landscape 768x512": (768, 512),
"Portrait 512x768": (512, 768),
"Square 768x768": (768, 768),
"Square 512x512": (512, 512),
}
FPS = 24
# ---------------------------------------------------------------------------
# Load the model ONCE at startup (module level).
#
# ZeroGPU rule: place models on cuda at module level so loading happens OUTSIDE
# the quota-charged generation call. A lazy first load inside @spaces.GPU would
# need a huge reservation (240s -> 360s billed after ZeroGPU's 1.5x factor),
# which exceeds the 300s free daily quota and gets rejected with
# "duration is larger than the maximum allowed". Preloading keeps every call
# small, so the free quota buys ~2-4 videos per day.
# The model files are prefetched at build time via the `models:` key in
# README.md, so this reads from local disk and takes under a minute.
# ---------------------------------------------------------------------------
print("Loading LTX-Video model (once, at startup)...")
_text_pipe = LTXPipeline.from_pretrained(MODEL_ID, torch_dtype=torch.bfloat16)
_text_pipe.to("cuda")
_text_pipe.vae.enable_slicing()
_text_pipe.vae.enable_tiling()
# The image-to-video pipeline reuses the same components, so it adds
# almost no extra memory.
_image_pipe = LTXImageToVideoPipeline.from_pretrained(
MODEL_ID,
transformer=_text_pipe.transformer,
vae=_text_pipe.vae,
text_encoder=_text_pipe.text_encoder,
tokenizer=_text_pipe.tokenizer,
scheduler=_text_pipe.scheduler,
torch_dtype=torch.bfloat16,
)
_image_pipe.to("cuda")
print("Model ready.")
def _get_duration(
prompt, negative_prompt, mode, input_image, num_frames, resolution, seed, num_steps, guidance
):
"""Return the GPU runtime budget for this call (seconds).
The model is already loaded, so this only needs to cover generation.
ZeroGPU bills the reservation (x1.5 on current hardware) against the daily
quota, so keep it tight: shorter durations = more videos per day.
"""
width, height = RESOLUTIONS[resolution]
pixel_scale = (width * height) / (512 * 704)
step_scale = num_steps / 30.0
estimate = num_frames * 0.25 * pixel_scale * step_scale
total = int((estimate + 15) * 1.2) # + VAE decode/export overhead, 20% margin
total = max(45, total)
return min(total, 120)
@spaces.GPU(duration=_get_duration)
def generate_video(
prompt,
negative_prompt,
mode,
input_image,
num_frames,
resolution,
seed,
num_steps,
guidance,
):
"""Generate a video from a text prompt (or an image + prompt)."""
if not prompt or not prompt.strip():
raise gr.Error("Please write a prompt first.")
negative_prompt = (negative_prompt or "").strip() or DEFAULT_NEGATIVE
width, height = RESOLUTIONS[resolution]
seed = int(seed) # gr.Number returns a float
if seed < 0:
seed = random.randint(0, 2**31 - 1)
generator = torch.Generator(device="cuda").manual_seed(seed)
text_pipe, image_pipe = _text_pipe, _image_pipe
# Timestep-aware VAE settings recommended for LTX-Video 0.9.1+.
decode_kwargs = {"decode_timestep": 0.05, "decode_noise_scale": 0.025}
if mode == "Image to Video":
if input_image is None:
raise gr.Error("Upload an image to use Image-to-Video mode.")
result = image_pipe(
prompt=prompt,
negative_prompt=negative_prompt,
image=input_image,
num_frames=num_frames,
height=height,
width=width,
num_inference_steps=num_steps,
guidance_scale=guidance,
image_cond_noise_scale=0.025,
generator=generator,
**decode_kwargs,
)
else:
result = text_pipe(
prompt=prompt,
negative_prompt=negative_prompt,
num_frames=num_frames,
height=height,
width=width,
num_inference_steps=num_steps,
guidance_scale=guidance,
generator=generator,
**decode_kwargs,
)
frames = result.frames[0]
out_path = os.path.join(
tempfile.gettempdir(), f"ltx_{seed}_{random.randint(0, 99999)}.mp4"
)
export_to_video(frames, out_path, fps=FPS)
return out_path
PROMPT_EXAMPLES = [
"Cinematic 3D render, glossy chrome sphere rotating on a dark studio background, volumetric lighting, smooth slow motion, octane render, 8k",
"Seamless abstract loop, flowing liquid metal, iridescent gradient colors, dark background, smooth hypnotic motion",
"Kinetic typography, the word MOTION exploding into view letter by letter, bold neon glowing letters, dark background, energetic dynamic camera",
]
with gr.Blocks(title="My Motion Video AI", theme=gr.themes.Soft()) as demo:
gr.Markdown(
"""# π¬ My Motion Video AI
Your own video generation AI, running 100% in the cloud on Hugging Face GPUs
(open-source **LTX-Video**). Your laptop never does the work.
> **Free tier limit:** ~5 minutes of GPU per day (resets 24h after first use).
> One short clip β 1β2 minutes of that. Choose your prompts wisely!
"""
)
with gr.Row():
with gr.Column(scale=1):
mode = gr.Radio(
["Text to Video", "Image to Video"],
value="Text to Video",
label="Mode",
info="Image to Video animates an uploaded image β great for kinetic typography and logos.",
)
prompt = gr.Textbox(
lines=3,
label="Prompt",
placeholder="Describe the motion graphics you want...",
info="Describe the scene AND the motion. Short clips work best.",
)
negative_prompt = gr.Textbox(
lines=2,
label="Negative prompt (optional)",
placeholder=DEFAULT_NEGATIVE,
)
input_image = gr.Image(
type="pil",
label="Starting image (Image to Video only)",
visible=False,
)
with gr.Accordion("Settings", open=False):
num_frames = gr.Slider(
minimum=49,
maximum=257,
value=121,
step=8,
label="Frames (121 β 5s, up to 257 β 10s)",
)
resolution = gr.Dropdown(
list(RESOLUTIONS.keys()),
value="Landscape 768x512",
label="Resolution",
)
num_steps = gr.Slider(
minimum=10,
maximum=50,
value=30,
step=1,
label="Inference steps (more = slower but higher quality)",
)
guidance = gr.Slider(
minimum=1.0,
maximum=6.0,
value=3.0,
step=0.5,
label="Guidance scale (how strictly it follows the prompt)",
)
seed = gr.Number(
value=-1,
label="Seed (-1 = random, reuse a seed to reproduce a video)",
)
generate_btn = gr.Button("π¬ Generate video", variant="primary")
with gr.Column(scale=1):
output_video = gr.Video(
label="Your video", format="mp4", autoplay=False
)
gr.Examples(
examples=PROMPT_EXAMPLES,
inputs=prompt,
label="Try one of these",
)
def toggle_image_visibility(selected_mode):
return gr.update(visible=(selected_mode == "Image to Video"))
mode.change(toggle_image_visibility, inputs=mode, outputs=input_image)
generate_btn.click(
generate_video,
inputs=[
prompt,
negative_prompt,
mode,
input_image,
num_frames,
resolution,
seed,
num_steps,
guidance,
],
outputs=output_video,
)
if __name__ == "__main__":
demo.queue().launch()
|