studio-video / app.py
Beexly's picture
Upload folder using huggingface_hub
348bd51 verified
Raw History Blame Contribute Delete
5.39 kB
import os
import random
import tempfile
import uuid
import numpy as np
import spaces
import torch
import gradio as gr
from diffusers import LTXConditionPipeline
from diffusers.utils import export_to_video
# ---------------------------------------------------------------------------
# Fix a real bug in gradio_client 1.5.2 (bundled with ZeroGPU's forced
# gradio==5.9.1): its JSON-schema-to-python-type parser assumes every schema
# node is a dict, but valid JSON Schema allows a bare boolean (e.g.
# additionalProperties: true) — that crashes EVERY API call to this Space
# with "TypeError: argument of type 'bool' is not iterable". Patched here,
# not worked around with an old pydantic pin (which broke other packages'
# serialization elsewhere in this account's other Spaces) — this is the
# actual bug, proven across studio-chat and studio-media this session.
import gradio_client.utils as _gc_utils
_orig_json_schema_to_python_type = _gc_utils._json_schema_to_python_type
def _patched_json_schema_to_python_type(schema, defs):
if not isinstance(schema, dict):
return "bool" if isinstance(schema, bool) else "Any"
return _orig_json_schema_to_python_type(schema, defs)
_gc_utils._json_schema_to_python_type = _patched_json_schema_to_python_type
# ---------------------------------------------------------------------------
# Model — loaded once at import time. Under ZeroGPU's patched torch,
# .to("cuda") here is deferred (no real GPU touched yet); the ZeroGPU
# "tensors packing" step prepares it for fast real materialization inside
# the @spaces.GPU-decorated call.
# ---------------------------------------------------------------------------
MODEL_ID = "Lightricks/LTX-Video-0.9.7-distilled"
RESOLUTIONS = {
"704x480 (landscape, fastest)": (704, 480),
"480x704 (portrait, fastest)": (480, 704),
"832x480 (landscape)": (832, 480),
"480x832 (portrait)": (480, 832),
}
pipe = LTXConditionPipeline.from_pretrained(MODEL_ID, torch_dtype=torch.bfloat16)
pipe.to("cuda")
pipe.vae.enable_tiling()
def _video_duration(prompt, negative_prompt, seed, random_seed, num_frames, steps, resolution):
# UNMEASURED estimate ahead of the first real call — the distilled
# checkpoint's own model card recommends ~7 steps for fast generation,
# so this starts conservative and gets recalibrated against real
# wall-clock time once deployed, the same discipline used for
# studio-media's FLUX.1-dev steps slider.
return min(120, 25 + int(num_frames) // 8 * int(steps) * 1.5)
@spaces.GPU(duration=_video_duration)
def generate_video(prompt, negative_prompt, seed, random_seed, num_frames, steps, resolution):
if not prompt or not prompt.strip():
raise gr.Error("Enter a prompt first.")
width, height = RESOLUTIONS.get(resolution, (704, 480))
num_frames = int(num_frames)
steps = int(steps)
s = random.randint(0, 2**31 - 1) if random_seed else int(seed)
generator = torch.Generator("cuda").manual_seed(s)
use_negative = bool(negative_prompt and negative_prompt.strip())
result = pipe(
prompt=prompt,
negative_prompt=negative_prompt if use_negative else None,
width=width,
height=height,
num_frames=num_frames,
num_inference_steps=steps,
generator=generator,
output_type="pil",
)
frames = result.frames[0]
out_path = os.path.join(tempfile.gettempdir(), f"ltxvideo_{uuid.uuid4().hex}.mp4")
export_to_video(frames, out_path, fps=24)
return out_path, str(s)
with gr.Blocks(title="Beexly Studio Video (ZeroGPU)") as demo:
gr.Markdown(
"# Beexly Studio Video (ZeroGPU)\n\n"
"Personal video-generation tool. **Not connected to any Galaxy "
"Sports Edge product, data, or prediction engine.**\n\n"
"`LTX-Video-0.9.7-distilled` (Lightricks) under the LTX-Video Open "
"Weights License — free for personal/non-commercial use; commercial "
"use requires a separate paid agreement above a revenue threshold. "
"This stays a personal tool, same posture as the non-commercial "
"FLUX.1-dev model on studio-media: **never for anything shipped on "
"galaxysportsedge.com.**"
)
with gr.Tab("Text to Video"):
v_prompt = gr.Textbox(label="Prompt", lines=2)
v_negative = gr.Textbox(label="Negative prompt (optional)", value="")
v_resolution = gr.Dropdown(list(RESOLUTIONS.keys()), value="704x480 (landscape, fastest)", label="Resolution")
with gr.Row():
v_seed = gr.Textbox(label="Seed", value="0")
v_random_seed = gr.Checkbox(label="Random seed", value=True)
v_frames = gr.Slider(41, 121, value=65, step=8, label="Frames (num_frames must be 8n+1; ~24fps, so 65 frames ≈ 2.7s)")
v_steps = gr.Slider(4, 12, value=7, step=1, label="Steps (7 is the distilled checkpoint's recommended default)")
v_btn = gr.Button("Generate", variant="primary")
v_out = gr.Video(label="Generated video")
v_seed_used = gr.Textbox(label="Seed used", interactive=False)
v_btn.click(
generate_video,
inputs=[v_prompt, v_negative, v_seed, v_random_seed, v_frames, v_steps, v_resolution],
outputs=[v_out, v_seed_used],
api_name="generate_video",
)
if __name__ == "__main__":
demo.launch()