import os import subprocess import sys from concurrent.futures import ThreadPoolExecutor os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1" os.environ["HF_XET_HIGH_PERFORMANCE"] = "1" os.environ["TORCH_COMPILE_DISABLE"] = "1" os.environ["TORCHDYNAMO_DISABLE"] = "1" subprocess.run([sys.executable, "-m", "pip", "install", "xformers==0.0.32.post2", "--no-build-isolation"], check=False) LTX_REPO_URL = "https://github.com/Lightricks/LTX-2.git" LTX_REPO_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "LTX-2") LTX_COMMIT = "ae855f8538843825f9015a419cf4ba5edaf5eec2" if os.path.exists(LTX_REPO_DIR): subprocess.run(["rm", "-rf", LTX_REPO_DIR], check=True) subprocess.run(["git", "clone", LTX_REPO_URL, LTX_REPO_DIR], check=True) subprocess.run(["git", "-C", LTX_REPO_DIR, "checkout", LTX_COMMIT], check=True) subprocess.run( [sys.executable, "-m", "pip", "install", "--force-reinstall", "--no-deps", "-e", os.path.join(LTX_REPO_DIR, "packages", "ltx-core"), "-e", os.path.join(LTX_REPO_DIR, "packages", "ltx-pipelines")], check=True, ) sys.path.insert(0, os.path.join(LTX_REPO_DIR, "packages", "ltx-pipelines", "src")) sys.path.insert(0, os.path.join(LTX_REPO_DIR, "packages", "ltx-core", "src")) # Patch sft_loader: remove non_blocking=True so ZeroGPU CUDA emulation can handle # module-level tensor loading (emulation mode doesn't support async CUDA streams) _sft_path = os.path.join(LTX_REPO_DIR, "packages", "ltx-core", "src", "ltx_core", "loader", "sft_loader.py") with open(_sft_path, "r") as _f: _sft = _f.read() _sft = _sft.replace( 'with safetensors.safe_open(shard_path, framework="pt", device=str(device)) as f:', 'with safetensors.safe_open(shard_path, framework="pt", device="cpu") as f:', ) with open(_sft_path, "w") as _f: _f.write(_sft) import logging import random import tempfile from pathlib import Path import torch torch._dynamo.config.suppress_errors = True torch._dynamo.config.disable = True torch.inference_mode = torch.no_grad import spaces import gradio as gr import numpy as np from huggingface_hub import hf_hub_download, snapshot_download # --- Core LTX imports --- from ltx_core.components.diffusion_steps import EulerDiffusionStep from ltx_core.components.noisers import GaussianNoiser from ltx_core.model.audio_vae import encode_audio as vae_encode_audio from ltx_core.model.upsampler import upsample_video from ltx_core.model.video_vae import TilingConfig, get_video_chunks_number from ltx_core.model.video_vae import decode_video as vae_decode_video from ltx_core.types import Audio, AudioLatentShape, VideoPixelShape from ltx_pipelines.utils import ModelLedger, euler_denoising_loop from ltx_pipelines.utils.args import ImageConditioningInput from ltx_pipelines.utils.constants import DISTILLED_SIGMA_VALUES, STAGE_2_DISTILLED_SIGMA_VALUES from ltx_pipelines.utils.helpers import ( assert_resolution, cleanup_memory, combined_image_conditionings, denoise_video_only, encode_prompts, get_device, simple_denoising_func, ) from ltx_pipelines.utils.media_io import decode_audio_from_file, encode_video from ltx_pipelines.utils.types import PipelineComponents from ltx_core.loader.primitives import LoraPathStrengthAndSDOps from ltx_core.loader.sd_ops import LTXV_LORA_COMFY_RENAMING_MAP # --- Attention backend patch (same as Element-16) --- import torch.nn.functional as F from ltx_core.model.transformer import attention as _attn_mod def _sdpa_as_mea(query, key, value, attn_bias=None, scale=None, **kwargs): q, k, v = query.transpose(1, 2), key.transpose(1, 2), value.transpose(1, 2) return F.scaled_dot_product_attention(q, k, v, scale=scale).transpose(1, 2) _cap = torch.cuda.get_device_capability() if torch.cuda.is_available() else (0, 0) _use_xformers = False if _cap < (12, 0): try: from xformers.ops import memory_efficient_attention as _mea _attn_mod.memory_efficient_attention = _mea _use_xformers = True print(f"[ATTN] Using xformers memory_efficient_attention") except Exception as e: print(f"[ATTN] xformers unavailable ({e}), falling back to SDPA") if not _use_xformers: _attn_mod.memory_efficient_attention = _sdpa_as_mea print(f"[ATTN] Using SDPA fallback (sm_{_cap[0]}{_cap[1]})") logging.getLogger().setLevel(logging.INFO) device = get_device() # --------------------------------------------------------------------------- # Custom pipeline: DistilledPipeline quality + audio guidance conditioning # Uses sulphur_distil_bf16 (same checkpoint as Element-16) with frozen audio # input latent to guide video generation — no mismatched LoRA. # --------------------------------------------------------------------------- class DistilledAudioGuidancePipeline: """ Two-stage distilled pipeline with audio guidance. Identical to DistilledPipeline but replaces joint audio/video denoising with video-only denoising conditioned on a frozen input audio latent. This gives Element-16 quality + audio reactivity. """ def __init__(self, distilled_checkpoint_path, spatial_upsampler_path, gemma_root, loras=(), device=device, quantization=None): self.device = device self.dtype = torch.bfloat16 self.model_ledger = ModelLedger( dtype=self.dtype, device=device, checkpoint_path=distilled_checkpoint_path, spatial_upsampler_path=spatial_upsampler_path, gemma_root_path=gemma_root, loras=loras, quantization=quantization, ) self.pipeline_components = PipelineComponents(dtype=self.dtype, device=device) def __call__( self, prompt: str, seed: int, height: int, width: int, num_frames: int, frame_rate: float, images: list, audio_path: str, audio_start_time: float = 0.0, audio_max_duration: float | None = None, tiling_config: TilingConfig | None = None, enhance_prompt: bool = False, ): assert_resolution(height=height, width=width, is_two_stage=True) generator = torch.Generator(device=self.device).manual_seed(seed) noiser = GaussianNoiser(generator=generator) stepper = EulerDiffusionStep() dtype = torch.bfloat16 duration = audio_max_duration or num_frames / frame_rate # 1. Encode prompts (ctx_p,) = encode_prompts( [prompt], self.model_ledger, enhance_first_prompt=enhance_prompt, enhance_prompt_image=images[0][0] if len(images) > 0 else None, ) video_context, audio_context = ctx_p.video_encoding, ctx_p.audio_encoding # 2. Encode input audio — freeze as conditioning latent audio_encoder = self.model_ledger.audio_encoder() decoded_audio = decode_audio_from_file(audio_path, self.device, audio_start_time, duration) encoded_audio_latent = vae_encode_audio(decoded_audio, audio_encoder) del audio_encoder cleanup_memory() audio_shape = AudioLatentShape.from_duration( batch=1, duration=num_frames / frame_rate, channels=8, mel_bins=16 ) target_frames = audio_shape.frames current_frames = encoded_audio_latent.shape[2] if current_frames < target_frames: # Audio shorter than video — pad with zeros pad = target_frames - current_frames encoded_audio_latent = torch.nn.functional.pad(encoded_audio_latent, (0, 0, 0, pad)) else: encoded_audio_latent = encoded_audio_latent[:, :, :target_frames] # 3. Load transformer + video_encoder ONCE — reused across both stages video_encoder = self.model_ledger.video_encoder() transformer = self.model_ledger.transformer() stage_1_sigmas = torch.Tensor(DISTILLED_SIGMA_VALUES).to(self.device) def make_denoising_loop(t): def loop(sigmas, video_state, audio_state, stepper): return euler_denoising_loop( sigmas=sigmas, video_state=video_state, audio_state=audio_state, stepper=stepper, denoise_fn=simple_denoising_func( video_context=video_context, audio_context=audio_context, transformer=t, ), ) return loop stage_1_shape = VideoPixelShape( batch=1, frames=num_frames, width=width // 2, height=height // 2, fps=frame_rate ) stage_1_conditionings = combined_image_conditionings( images=images, height=stage_1_shape.height, width=stage_1_shape.width, video_encoder=video_encoder, dtype=dtype, device=self.device, ) video_state = denoise_video_only( output_shape=stage_1_shape, conditionings=stage_1_conditionings, noiser=noiser, sigmas=stage_1_sigmas, stepper=stepper, denoising_loop_fn=make_denoising_loop(transformer), components=self.pipeline_components, dtype=dtype, device=self.device, initial_audio_latent=encoded_audio_latent, ) # 4. Upsample — video_encoder stays loaded upscaled_video_latent = upsample_video( latent=video_state.latent[:1], video_encoder=video_encoder, upsampler=self.model_ledger.spatial_upsampler(), ) torch.cuda.synchronize() # 5. Stage 2 — reuse same transformer + video_encoder, no reload stage_2_sigmas = torch.Tensor(STAGE_2_DISTILLED_SIGMA_VALUES).to(self.device) stage_2_shape = VideoPixelShape( batch=1, frames=num_frames, width=width, height=height, fps=frame_rate ) stage_2_conditionings = combined_image_conditionings( images=images, height=stage_2_shape.height, width=stage_2_shape.width, video_encoder=video_encoder, dtype=dtype, device=self.device, ) video_state = denoise_video_only( output_shape=stage_2_shape, conditionings=stage_2_conditionings, noiser=noiser, sigmas=stage_2_sigmas, stepper=stepper, denoising_loop_fn=make_denoising_loop(transformer), components=self.pipeline_components, dtype=dtype, device=self.device, noise_scale=stage_2_sigmas[0], initial_video_latent=upscaled_video_latent, initial_audio_latent=encoded_audio_latent, ) torch.cuda.synchronize() del transformer, video_encoder cleanup_memory() # 6. Decode video, return original input audio (preserve fidelity) decoded_video = vae_decode_video( video_state.latent, self.model_ledger.video_decoder(), tiling_config, generator ) original_audio = Audio( waveform=decoded_audio.waveform.squeeze(0), sampling_rate=decoded_audio.sampling_rate, ) return decoded_video, original_audio # --------------------------------------------------------------------------- # Model setup # --------------------------------------------------------------------------- MAX_SEED = np.iinfo(np.int32).max DEFAULT_PROMPT = ( "A person speaking naturally, lips moving in perfect sync with their voice, " "cinematic lighting, sharp focus, smooth motion." ) DEFAULT_FRAME_RATE = 24.0 CHECKPOINT_REPO = "SulphurAI/Sulphur-2-base" LTX_MODEL_REPO = "Lightricks/LTX-2.3" GEMMA_REPO = "Lightricks/gemma-3-12b-it-qat-q4_0-unquantized" RESOLUTIONS = { "9:16": (512, 896), "16:9": (896, 512), "1:1": (640, 640), } print("=" * 80) print("Downloading Sulphur distilled checkpoint + upsampler + Gemma (parallel)...") print("=" * 80) def download_checkpoint(): return hf_hub_download(repo_id=CHECKPOINT_REPO, filename="sulphur_distil_bf16.safetensors") def download_upsampler(): return hf_hub_download(repo_id=LTX_MODEL_REPO, filename="ltx-2.3-spatial-upscaler-x2-1.0.safetensors") def download_gemma(): return snapshot_download(repo_id=GEMMA_REPO) def download_talking_head_lora(): return hf_hub_download( repo_id="elix3r/LTX-2.3-22b-AV-LoRA-talking-head", filename="LTX-2.3-22b-AV-LoRA-talking-head-v1.safetensors" ) with ThreadPoolExecutor(max_workers=4) as executor: f_ckpt = executor.submit(download_checkpoint) f_upsampler = executor.submit(download_upsampler) f_gemma = executor.submit(download_gemma) f_lora = executor.submit(download_talking_head_lora) checkpoint_path = f_ckpt.result() upsampler_path = f_upsampler.result() gemma_root = f_gemma.result() talking_head_lora = f_lora.result() print(f"Checkpoint: {checkpoint_path}") print(f"Spatial upsampler: {upsampler_path}") print(f"Gemma root: {gemma_root}") pipeline = DistilledAudioGuidancePipeline( distilled_checkpoint_path=checkpoint_path, spatial_upsampler_path=upsampler_path, gemma_root=gemma_root, loras=(LoraPathStrengthAndSDOps( path=talking_head_lora, strength=0.8, sd_ops=LTXV_LORA_COMFY_RENAMING_MAP ),), ) # Preload all models for ZeroGPU tensor packing # Module-level CUDA works via ZeroGPU's CUDA emulation mode (same pattern as Element-16) print("Preloading all pipeline components via model_ledger...") ledger = pipeline.model_ledger _transformer = ledger.transformer() _video_encoder = ledger.video_encoder() _audio_encoder = ledger.audio_encoder() _video_decoder = ledger.video_decoder() _spatial_upsampler = ledger.spatial_upsampler() _text_encoder = ledger.text_encoder() _embeddings_processor = ledger.gemma_embeddings_processor() ledger.transformer = lambda: _transformer ledger.video_encoder = lambda: _video_encoder ledger.audio_encoder = lambda: _audio_encoder ledger.video_decoder = lambda: _video_decoder ledger.spatial_upsampler = lambda: _spatial_upsampler ledger.text_encoder = lambda: _text_encoder ledger.gemma_embeddings_processor = lambda: _embeddings_processor print("All models preloaded!") print("=" * 80) print("Pipeline ready!") print("=" * 80) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def log_memory(tag: str): if torch.cuda.is_available(): alloc = torch.cuda.memory_allocated() / 1024**3 peak = torch.cuda.max_memory_allocated() / 1024**3 free, total = torch.cuda.mem_get_info() print(f"[VRAM {tag}] alloc={alloc:.2f}GB peak={peak:.2f}GB free={free/1024**3:.2f}GB total={total/1024**3:.2f}GB") def detect_aspect_ratio(image) -> str: if image is None: return "9:16" w, h = (image.size if hasattr(image, "size") else (image.shape[1], image.shape[0])) ratio = w / h candidates = {"9:16": 9 / 16, "16:9": 16 / 9, "1:1": 1.0} return min(candidates, key=lambda k: abs(ratio - candidates[k])) def on_image_upload(image): aspect = detect_aspect_ratio(image) w, h = RESOLUTIONS[aspect] return gr.update(value=w), gr.update(value=h) def ensure_stereo(audio_path: str) -> str: """Convert audio to stereo WAV — audio VAE expects 2 channels.""" out_path = tempfile.mktemp(suffix="_stereo.wav") subprocess.run( ["ffmpeg", "-y", "-i", audio_path, "-ac", "2", "-ar", "44100", out_path], check=True, capture_output=True, ) return out_path DEFAULT_NEGATIVE_PROMPT = ( "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量," "JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的," "形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走, " "blurry, glasses, deformed, subtitles, text, captions, worst quality, low quality, " "inconsistent motion, jittery, distorted" ) # --------------------------------------------------------------------------- # Inference # --------------------------------------------------------------------------- @spaces.GPU(duration=90) @torch.inference_mode() def generate_video( first_image, audio_input, prompt: str, duration: float, enhance_prompt: bool, seed: int, randomize_seed: bool, height: int, width: int, negative_prompt: str, progress=gr.Progress(track_tqdm=True), ): if audio_input is None: raise gr.Error("Please provide an audio file.") audio_path = ensure_stereo(audio_input) try: torch.cuda.reset_peak_memory_stats() log_memory("start") current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed) frame_rate = DEFAULT_FRAME_RATE num_frames = int(duration * frame_rate) + 1 num_frames = ((num_frames - 1 + 7) // 8) * 8 + 1 print(f"Generating: {width}x{height}, {num_frames} frames ({duration}s), seed={current_seed}") output_dir = Path("outputs") output_dir.mkdir(exist_ok=True) images = [] if first_image is not None: temp_path = output_dir / f"temp_first_{current_seed}.jpg" if hasattr(first_image, "save"): first_image.save(temp_path) else: temp_path = Path(first_image) images.append(ImageConditioningInput(path=str(temp_path), frame_idx=0, strength=1.0)) tiling_config = TilingConfig.default() video_chunks_number = get_video_chunks_number(num_frames, tiling_config) log_memory("before pipeline call") video_frames_iter, audio = pipeline( prompt=prompt, seed=current_seed, height=int(height), width=int(width), num_frames=num_frames, frame_rate=frame_rate, images=images, audio_path=audio_path, audio_max_duration=num_frames / frame_rate, tiling_config=tiling_config, enhance_prompt=enhance_prompt, ) frames = [frame for frame in video_frames_iter] video_tensor = torch.cat(frames, dim=0) if len(frames) > 1 else frames[0] log_memory("after pipeline call") output_path = tempfile.mktemp(suffix=".mp4") encode_video( video=video_tensor, fps=frame_rate, audio=audio, output_path=output_path, video_chunks_number=video_chunks_number, ) log_memory("after encode_video") return str(output_path), current_seed except Exception as e: import traceback log_memory("on error") print(f"Error: {str(e)}\n{traceback.format_exc()}") raise gr.Error(str(e)) # --------------------------------------------------------------------------- # Gradio UI # --------------------------------------------------------------------------- with gr.Blocks(title="Element-16 Audio Guidance", delete_cache=(3600, 7200)) as demo: gr.Markdown("# Element-16: Audio-Guided Video Generation") gr.Markdown( "Generate audio-reactive video from an audio file and an optional image. " "Powered by Sulphur distilled checkpoint with native audio conditioning. " "[[code]](https://github.com/Lightricks/LTX-2)" ) with gr.Row(): with gr.Column(): first_image = gr.Image(label="First Frame (Optional)", type="pil") audio_input = gr.Audio(label="Audio Input", type="filepath") prompt = gr.Textbox( label="Prompt", info="Describe the scene and motion. Be specific for best results.", value=DEFAULT_PROMPT, lines=3, ) duration = gr.Slider(label="Duration (seconds)", minimum=1.0, maximum=20.0, value=5.0, step=0.5) generate_btn = gr.Button("Generate Video", variant="primary", size="lg") with gr.Accordion("Advanced Settings", open=False): seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, value=42, step=1) randomize_seed = gr.Checkbox(label="Randomize Seed", value=True) with gr.Row(): width = gr.Number(label="Width", value=512, precision=0) height = gr.Number(label="Height", value=896, precision=0) enhance_prompt = gr.Checkbox(label="Enhance Prompt", value=False) negative_prompt = gr.Textbox( label="Negative Prompt", value=DEFAULT_NEGATIVE_PROMPT, lines=3, ) with gr.Column(): output_video = gr.Video(label="Generated Video", autoplay=True) used_seed = gr.Number(label="Used Seed", interactive=False) first_image.change(fn=on_image_upload, inputs=[first_image], outputs=[width, height]) generate_btn.click( fn=generate_video, inputs=[ first_image, audio_input, prompt, duration, enhance_prompt, seed, randomize_seed, height, width, negative_prompt, ], outputs=[output_video, used_seed], ) css = """ .fillable { max-width: 1200px !important } """ if __name__ == "__main__": demo.launch(theme=gr.themes.Citrus(), css=css)