Modal logo

High-quality text-to-video with LTX-2

LTX-2 is a 19B-parameter diffusion model that generates video with synchronized audio from a text prompt. This example runs LTX-2’s two-stage production-grade pipeline on Modal: stage 1 generates video at half resolution, then stage 2 upscales by 2x and refines with a distilled LoRA. Output is a 1024x1536 MP4 with audio.

Setup 

The text encoder uses Gemma 3 weights that require accepting a license. Visit https://huggingface.co/google/gemma-3-12b-it-qat-q4_0-unquantized, click “Agree and access repository”, then create a token at https://huggingface.co/settings/tokens and add it as a Modal Secret named huggingface-secret with key HF_TOKEN.

Generate a video with:

modal run ltx2_two_stage.py --prompt "A cathedral made of ice, northern lights overhead"

Retrieve the output from the Modal Volume:

modal volume ls ltx2-outputs
modal volume get ltx2-outputs <filename>

Environment setup 

import time
from pathlib import Path

import modal

We pin an LTX-2 commit and install its three subpackages. Torch is installed, along with a Flash-Attention 3 (as a wheel) for faster inference on GPUs with the Hopper SM architecture.

ltx2_commit = "28c3c73"

image = (
    modal.Image.from_registry("nvidia/cuda:12.6.1-devel-ubuntu24.04", add_python="3.12")
    .apt_install("git", "ffmpeg")
    .uv_pip_install(
        "torch==2.7.0",
        "torchaudio==2.7.0",
        "transformers>=4.52,<5",
        f"git+https://github.com/Lightricks/LTX-2.git@{ltx2_commit}#subdirectory=packages/ltx-core",
        f"git+https://github.com/Lightricks/LTX-2.git@{ltx2_commit}#subdirectory=packages/ltx-pipelines",
        f"git+https://github.com/Lightricks/LTX-2.git@{ltx2_commit}#subdirectory=packages/ltx-trainer",
        "https://huggingface.co/alexnasa/flash-attn-3/resolve/main/128/flash_attn_3-3.0.0b1-cp39-abi3-linux_x86_64.whl",
        extra_index_url="https://download.pytorch.org/whl/cu128",
        extra_options="--index-strategy unsafe-best-match",
    )
    .env(
        {
            "HF_XET_HIGH_PERFORMANCE": "1",
            "PYTORCH_ALLOC_CONF": "expandable_segments:True",
        }
    )
    .entrypoint([])
)

Volumes 

Model weights are cached to a Volume at HuggingFace’s default cache path. Generated videos are saved to a separate output Volume.

model_volume = modal.Volume.from_name("ltx2-models", create_if_missing=True)
output_volume = modal.Volume.from_name("ltx2-outputs", create_if_missing=True)

OUTPUT_DIR = Path("/output-videos")

with image.imports():
    import torch
    from huggingface_hub import hf_hub_download, snapshot_download
    from ltx_core.loader import LTXV_LORA_COMFY_RENAMING_MAP, LoraPathStrengthAndSDOps
    from ltx_core.model.video_vae import TilingConfig, get_video_chunks_number
    from ltx_pipelines.ti2vid_two_stages import TI2VidTwoStagesPipeline
    from ltx_pipelines.utils.constants import (
        DEFAULT_AUDIO_GUIDER_PARAMS,
        DEFAULT_NEGATIVE_PROMPT,
        DEFAULT_VIDEO_GUIDER_PARAMS,
    )
    from ltx_pipelines.utils.media_io import encode_video

app = modal.App(
    "example-ltx2-two-stage",
    image=image,
    volumes={
        "/root/.cache/huggingface": model_volume,
        OUTPUT_DIR: output_volume,
    },
    secrets=[modal.Secret.from_name("huggingface-secret")],
)

Inference 

NUM_FRAMES = 121  # ~5s at 24 fps
FRAME_RATE = 24
WIDTH = 1536
HEIGHT = 1024


@app.cls(gpu="H200", timeout=30 * 60, scaledown_window=15 * 60)
class LTX2TwoStage:
    @modal.enter()
    def setup(self):
        """Download model weights and initialize the two-stage pipeline."""
        torch.set_float32_matmul_precision("high")

        repo = "Lightricks/LTX-2"
        checkpoint_path = hf_hub_download(repo, "ltx-2-19b-dev.safetensors")
        upsampler_path = hf_hub_download(
            repo, "ltx-2-spatial-upscaler-x2-1.0.safetensors"
        )
        distilled_lora_path = hf_hub_download(
            repo, "ltx-2-19b-distilled-lora-384.safetensors"
        )
        gemma_dir = snapshot_download("google/gemma-3-12b-it-qat-q4_0-unquantized")
        model_volume.commit()

        distilled_lora = [
            LoraPathStrengthAndSDOps(
                distilled_lora_path, 1.0, LTXV_LORA_COMFY_RENAMING_MAP
            )
        ]

        self.tiling_config = TilingConfig.default()
        self.pipeline = TI2VidTwoStagesPipeline(
            checkpoint_path=checkpoint_path,
            distilled_lora=distilled_lora,
            spatial_upsampler_path=upsampler_path,
            gemma_root=gemma_dir,
            loras=[],
        )

    @modal.method()
    def generate(self, prompt: str) -> None:
        """Generate a video from a text prompt and save it to the output Volume."""
        print(f"Generating {NUM_FRAMES} frames ({NUM_FRAMES / FRAME_RATE:.0f}s) ...")
        print(f"Prompt: {prompt}")
        start = time.time()

        with torch.no_grad():
            video, audio = self.pipeline(
                prompt=prompt,
                negative_prompt=DEFAULT_NEGATIVE_PROMPT,
                seed=42,
                height=HEIGHT,
                width=WIDTH,
                num_frames=NUM_FRAMES,
                frame_rate=FRAME_RATE,
                num_inference_steps=40,
                video_guider_params=DEFAULT_VIDEO_GUIDER_PARAMS,
                audio_guider_params=DEFAULT_AUDIO_GUIDER_PARAMS,
                images=[],
                tiling_config=self.tiling_config,
                enhance_prompt=True,
            )
            print(f"Generated in {time.time() - start:.0f}s")

            safe = "".join(c if c.isalnum() or c == " " else "-" for c in prompt)
            filename = f"{int(time.time())}_{safe[:80].strip().replace(' ', '_')}.mp4"
            output_path = OUTPUT_DIR / filename

            encode_video(
                video=video,
                fps=FRAME_RATE,
                audio=audio,
                audio_sample_rate=24_000,
                output_path=str(output_path),
                video_chunks_number=get_video_chunks_number(
                    NUM_FRAMES, self.tiling_config
                ),
            )
        output_volume.commit()
        print(f"Saved to Volume `ltx2-outputs` at {filename}")


@app.local_entrypoint()
def main(
    prompt: str = "A cathedral made of ice, northern lights dancing overhead, camera slowly pushing forward through the nave",
):
    LTX2TwoStage().generate.remote(prompt=prompt)