import gc import json import os import tempfile from pathlib import Path from threading import Lock import gradio as gr import numpy as np import spaces import torch from diffusers import AutoencoderKLWan from diffusers.utils import export_to_video from lingbot_video import ( FlowUniPCMultistepScheduler, LingBotVideoPipeline, LingBotVideoTransformer3DModel, ) from lingbot_video.pipeline_lingbot_video import DEFAULT_NEGATIVE_PROMPT from transformers import Qwen3VLForConditionalGeneration, Qwen3VLProcessor MODEL_ID = "robbyant/lingbot-video-dense-1.3b" MAX_SEED = 2**32 - 1 RESOLUTIONS = { "Landscape 16:9 (832 x 480)": (480, 832), "Portrait 9:16 (480 x 832)": (832, 480), "Landscape 4:3 (640 x 480)": (480, 640), "Portrait 3:4 (480 x 640)": (640, 480), "Square 1:1 (480 x 480)": (480, 480), } CAMERA_DESCRIPTIONS = { "Static": "The camera remains stable and fixed while the subject moves naturally.", "Slow push in": "The camera slowly pushes toward the main subject with smooth cinematic motion.", "Slow pull out": "The camera slowly pulls away to reveal more of the environment.", "Pan left": "The camera performs a smooth pan toward the left while keeping the subject in view.", "Pan right": "The camera performs a smooth pan toward the right while keeping the subject in view.", "Handheld": "The camera uses subtle, natural handheld motion without abrupt shaking.", "Tracking": "The camera smoothly tracks the main subject through the scene.", } pipe = None pipe_lock = Lock() def get_pipe(): global pipe if pipe is not None: return pipe with pipe_lock: if pipe is None: print(f"Loading {MODEL_ID}...", flush=True) common = { "local_files_only": True, "low_cpu_mem_usage": True, } print("Loading LingBot transformer...", flush=True) transformer = LingBotVideoTransformer3DModel.from_pretrained( MODEL_ID, subfolder="transformer", torch_dtype=torch.bfloat16, **common, ).to("cuda") print("Loading Qwen3-VL text encoder...", flush=True) text_encoder = Qwen3VLForConditionalGeneration.from_pretrained( MODEL_ID, subfolder="text_encoder", torch_dtype=torch.bfloat16, **common, ).to("cuda") print("Loading Wan VAE...", flush=True) vae = AutoencoderKLWan.from_pretrained( MODEL_ID, subfolder="vae", torch_dtype=torch.float32, **common, ).to("cuda") processor = Qwen3VLProcessor.from_pretrained( MODEL_ID, subfolder="processor", local_files_only=True, ) scheduler = FlowUniPCMultistepScheduler.from_pretrained( MODEL_ID, subfolder="scheduler", local_files_only=True, ) pipe = LingBotVideoPipeline( transformer=transformer, vae=vae, text_encoder=text_encoder, processor=processor, scheduler=scheduler, ) pipe.set_progress_bar_config(disable=False) print("LingBot-Video pipeline loaded.", flush=True) return pipe def build_structured_caption(prompt, camera_motion, duration): return { "comprehensive_description": { "scene_content_description": prompt.strip(), "camera_movement_description": CAMERA_DESCRIPTIONS[camera_motion], }, "camera_info": { "color": "Natural", "frame_size": "Medium", "shot_type_angle": "Eye level", "lens_size": "Medium", "composition": "Center", "lighting": "Soft light", "lighting_type": "Natural light", }, "world_knowledge": [], "prominent_elements": [], "target_duration_seconds": int(duration), } def resolve_caption(prompt, camera_motion, duration, structured_json): if structured_json and structured_json.strip(): try: parsed = json.loads(structured_json) except json.JSONDecodeError as exc: raise gr.Error(f"Structured caption JSON is invalid: {exc}") from exc caption = parsed.get("caption", parsed) if isinstance(parsed, dict) else parsed if not isinstance(caption, dict): raise gr.Error("Structured caption JSON must be an object or contain a caption object.") else: if not prompt or not prompt.strip(): raise gr.Error("Please enter a prompt.") caption = build_structured_caption(prompt, camera_motion, duration) return caption, json.dumps(caption, ensure_ascii=False, indent=2) def valid_frame_count(duration, fps): requested = max(1, int(round(float(duration) * int(fps)))) # LingBot-Video requires one frame or 4n+1 frames. return max(1, ((requested - 1 + 3) // 4) * 4 + 1) @spaces.GPU(size="large", duration=300) def generate_video( prompt, negative_prompt, structured_json, aspect_ratio, camera_motion, duration, fps, num_inference_steps, guidance_scale, seed, randomize_seed, progress=gr.Progress(track_tqdm=True), ): if not torch.cuda.is_available(): raise RuntimeError("CUDA is not available inside the ZeroGPU worker.") progress(0.02, desc="Preparing LingBot-Video") model = get_pipe() if randomize_seed: seed = torch.randint(0, MAX_SEED, (1,)).item() seed = int(seed) % MAX_SEED height, width = RESOLUTIONS[aspect_ratio] fps = int(fps) num_frames = valid_frame_count(duration, fps) caption, caption_json = resolve_caption( prompt, camera_motion, duration, structured_json ) caption_text = json.dumps(caption, ensure_ascii=False, separators=(",", ":")) negative_text = ( negative_prompt.strip() if isinstance(negative_prompt, str) and negative_prompt.strip() else DEFAULT_NEGATIVE_PROMPT ) generator = torch.Generator(device="cuda").manual_seed(seed) progress(0.08, desc=f"Generating {num_frames} frames") with torch.inference_mode(): result = model( prompt=caption_text, negative_prompt=negative_text, height=height, width=width, num_frames=num_frames, num_inference_steps=int(num_inference_steps), guidance_scale=float(guidance_scale), shift=3.0, generator=generator, output_type="np", batch_cfg=False, ) frames = np.asarray(result.frames[0]) output_dir = Path(tempfile.gettempdir()) / "lingbot_video_outputs" output_dir.mkdir(parents=True, exist_ok=True) output_path = output_dir / f"lingbot_{seed}_{height}x{width}_{num_frames}f.mp4" export_to_video(frames, str(output_path), fps=fps) del result, frames gc.collect() torch.cuda.empty_cache() status = ( f"Seed: {seed} | Resolution: {width} x {height} | " f"Frames: {num_frames} | FPS: {fps} | Steps: {int(num_inference_steps)}" ) progress(1.0, desc="Video ready") return str(output_path), status, caption_json EXAMPLES = [ [ "A humanoid robot walks steadily along a gravel path in a formal garden, realistic materials, cinematic daylight", "Tracking", ], [ "A glass bottle falls from a wooden table and shatters naturally on a concrete floor, realistic physics, high-speed detail", "Static", ], [ "A young woman in a cream cardigan turns toward the camera and adjusts her collar in a bright modern apartment", "Slow push in", ], ] with gr.Blocks(title="LingBot-Video Dense 1.3B") as demo: gr.Markdown( """

🎬 LingBot-Video Dense 1.3B

Generate short physical-world videos with Robbyant LingBot-Video on Hugging Face ZeroGPU.

The app turns a normal prompt into a compact structured caption. Start with 2 seconds / 16 FPS for a faster first run.

""" ) with gr.Row(): with gr.Column(scale=1): prompt = gr.Textbox( label="Prompt", placeholder="Describe the scene, subject, actions and physical motion...", lines=5, ) negative_prompt = gr.Textbox( label="Negative Prompt (optional)", placeholder="Leave empty to use LingBot-Video's official default negative prompt.", lines=2, ) with gr.Accordion("Advanced structured caption", open=False): structured_json = gr.Code( label="Structured Caption JSON (optional)", language="json", value="", lines=10, ) with gr.Row(): aspect_ratio = gr.Dropdown( choices=list(RESOLUTIONS), value="Landscape 16:9 (832 x 480)", label="Aspect Ratio", ) camera_motion = gr.Dropdown( choices=list(CAMERA_DESCRIPTIONS), value="Static", label="Camera Motion", ) with gr.Row(): duration = gr.Slider( minimum=1, maximum=5, value=2, step=1, label="Duration (seconds)", ) fps = gr.Dropdown( choices=[16, 24], value=16, label="FPS", ) with gr.Row(): num_inference_steps = gr.Slider( minimum=20, maximum=40, value=30, step=1, label="Inference Steps", ) guidance_scale = gr.Slider( minimum=1.0, maximum=5.0, value=3.0, step=0.1, label="CFG Guidance Scale", ) with gr.Row(): seed = gr.Number(label="Seed", value=42, precision=0) randomize_seed = gr.Checkbox(label="Randomize Seed", value=False) generate_btn = gr.Button( "🚀 Generate Video", variant="primary", size="lg", elem_classes="generate-btn", ) with gr.Column(scale=1): output_video = gr.Video( label="Generated Video", autoplay=True, loop=True, include_audio=False, ) generation_info = gr.Textbox(label="Generation Info", interactive=False) caption_preview = gr.Code( label="Structured Caption Used", language="json", interactive=False, lines=12, ) gr.Markdown("### 💡 Example Prompts") gr.Examples( examples=EXAMPLES, inputs=[prompt, camera_motion], cache_examples=False, ) gr.Markdown( "Model and inference code by [Robbyant](https://github.com/Robbyant/lingbot-video). " "Model license: Apache-2.0. This community demo uses the Dense 1.3B checkpoint without the separate 27B prompt rewriter." ) inputs = [ prompt, negative_prompt, structured_json, aspect_ratio, camera_motion, duration, fps, num_inference_steps, guidance_scale, seed, randomize_seed, ] outputs = [output_video, generation_info, caption_preview] generate_btn.click( fn=generate_video, inputs=inputs, outputs=outputs, api_name="generate_video", concurrency_limit=1, ) prompt.submit( fn=generate_video, inputs=inputs, outputs=outputs, concurrency_limit=1, ) if __name__ == "__main__": demo.queue(default_concurrency_limit=1, max_size=8).launch( mcp_server=True, show_error=True, )