---
title: preprocess_ltx2_overfit
---

<div class="python-reference">
<a id="fastvideo.pipelines.preprocess.preprocess_ltx2_overfit-functions"></a>
<a id="fastvideo.pipelines.preprocess.preprocess_ltx2_overfit"></a>
<p>Preprocess LTX-2 overfit data into parquet format.</p>
<p>Encodes videos with the LTX-2 causal video VAE and captions with the
Gemma text encoder (feature extractor + embedding connector) into the
t2v parquet schema expected by the training framework.</p>
<p>The stored text embeddings are POST-connector: the connector replaces
pad positions with learnable registers and returns an all-valid mask,
so the parquet collate's ones/zeros mask stays semantically correct
and training needs no text encoder at all. Captions are encoded via
the encoder's forward() (the exact inference path), which handles both
LTX-2.0 (shared 3840-d features) and LTX-2.3 (separate 4096-d video /
2048-d audio feature extractors).</p>
<p>Videos are resampled to TRAIN_FPS and the preprocessed clip is also
saved as an mp4 next to the parquet so overfit tests can use it as
the SSIM reference.</p>
<h2>Usage</h2>
<p>CUDA_VISIBLE_DEVICES=0 python fastvideo/pipelines/preprocess/preprocess_ltx2_overfit.py</p>
<h2 id="fastvideo.pipelines.preprocess.preprocess_ltx2_overfit.maybe_download_model"><code>fastvideo.pipelines.preprocess.preprocess_ltx2_overfit.maybe_download_model</code></h2>
<pre class="doc-signature"><code>maybe_download_model(model_name_or_path: str, local_dir: str | None = None, download: bool = True, revision: str | None = None, allow_patterns: list&#91;str&#93; | None = None) -&gt; str</code></pre>
<p>Check if the model path is a Hugging Face Hub model ID and download it if needed.</p>
<p>Supports an "umbrella" repo layout where a single HF repo holds multiple
pipeline variants under sibling subfolders. If the input is shaped as
<code>org/repo/subfolder</code> (i.e. a non-existent local path with 3+ slash-
separated components and at least one segment that does not look like a
posix-absolute path), treat the first two components as the HF repo id
and the remainder as a subfolder; only the subfolder's blobs are
downloaded, and the returned local path points inside that subfolder.</p>
<h3>Parameters</h3>
<dl>
<dt><code>model_name_or_path: str</code></dt><dd><p>Local path, Hugging Face Hub model ID, or
<code>org/repo/subfolder</code> umbrella-repo reference.</p></dd>
<dt><code>local_dir: str | None</code></dt><dd><p>Local directory to save the model</p></dd>
<dt><code>download: bool</code></dt><dd><p>Whether to download the model from Hugging Face Hub</p></dd>
<dt><code>revision: str | None</code></dt><dd><p>Optional immutable Hub revision.</p></dd>
<dt><code>allow_patterns: list[str] | None</code></dt><dd><p>Optional Hub glob patterns limiting downloaded files.
Local paths are returned unchanged. For umbrella references, the
patterns are interpreted relative to the selected subfolder.</p></dd>
</dl>
<h3>Returns</h3>
<dl>
<dt><code>str</code></dt><dd><p>Local path to the model (or to the subfolder inside the snapshot).</p></dd>
</dl>
<h2 id="fastvideo.pipelines.preprocess.preprocess_ltx2_overfit.verify_model_config_and_directory"><code>fastvideo.pipelines.preprocess.preprocess_ltx2_overfit.verify_model_config_and_directory</code></h2>
<pre class="doc-signature"><code>verify_model_config_and_directory(model_path: str, required_component_dirs: list&#91;str&#93; | tuple&#91;str, ...&#93; | None = None) -&gt; dict&#91;str, Any&#93;</code></pre>
<p>Verify that the model directory contains a valid Diffusers configuration.</p>
<h3>Parameters</h3>
<dl>
<dt><code>model_path: str</code></dt><dd><p>Path to the model directory</p></dd>
<dt><code>required_component_dirs: list[str] | tuple[str, ...] | None</code></dt><dd><p>Component directories required by the selected
pipeline. <code>None</code> preserves full-snapshot validation; an empty
collection validates only the manifest.</p></dd>
</dl>
<h3>Returns</h3>
<dl>
<dt><code>dict[str, Any]</code></dt><dd><p>The loaded model configuration as a dictionary</p></dd>
</dl>
<h2 id="fastvideo.pipelines.preprocess.preprocess_ltx2_overfit.load_video"><code>fastvideo.pipelines.preprocess.preprocess_ltx2_overfit.load_video</code></h2>
<pre class="doc-signature"><code>load_video(path: str, num_frames: int, target_fps: float, height: int, width: int) -&gt; tuple&#91;torch.Tensor, np.ndarray&#93;</code></pre>
<p>Load a video as [1, C, T, H, W] in [-1, 1], resampled to target_fps.</p>
<p>Also returns the uint8 RGB frames [T, H, W, C] for reference-video export.</p>
<details><summary>Source</summary><pre><code>def load_video(path: str, num_frames: int, target_fps: float, height: int,
               width: int) -&gt; tuple&#91;torch.Tensor, np.ndarray&#93;:
    &quot;&quot;&quot;Load a video as &#91;1, C, T, H, W&#93; in &#91;-1, 1&#93;, resampled to target_fps.

    Also returns the uint8 RGB frames &#91;T, H, W, C&#93; for reference-video export.
    &quot;&quot;&quot;
    with av.open(path) as container:
        if not container.streams.video:
            raise RuntimeError(f&quot;No video stream found in &#123;path&#125;&quot;)
        stream = container.streams.video&#91;0&#93;
        native_fps = float(stream.average_rate or target_fps)
        decoded = &#91;torch.from_numpy(frame.to_ndarray(format=&quot;rgb24&quot;)) for frame in container.decode(video=0)&#93;
    if not decoded:
        raise RuntimeError(f&quot;Could not read any frames from &#123;path&#125;&quot;)
    raw = torch.stack(decoded).permute(0, 3, 1, 2)  # &#91;T, C, H, W&#93; uint8
    step = native_fps / target_fps
    wanted = &#91;min(int(round(i * step)), raw.shape&#91;0&#93; - 1) for i in range(num_frames)&#93;

    frames = raw&#91;wanted&#93;.float()  # &#91;T, C, H, W&#93; in &#91;0, 255&#93;
    src_h, src_w = frames.shape&#91;2&#93;, frames.shape&#91;3&#93;
    scale = max(height / src_h, width / src_w)
    new_h, new_w = int(round(src_h * scale)), int(round(src_w * scale))
    frames = torch.nn.functional.interpolate(frames, size=(new_h, new_w), mode=&quot;bilinear&quot;, antialias=True)
    top = (new_h - height) // 2
    left = (new_w - width) // 2
    frames = frames&#91;:, :, top:top + height, left:left + width&#93;

    frames_np = (frames.permute(0, 2, 3, 1).clamp(0, 255).round().to(torch.uint8).numpy())
    video = frames / 127.5 - 1.0  # &#91;0,255&#93; -&gt; &#91;-1,1&#93;
    video = video.permute(1, 0, 2, 3).unsqueeze(0)  # &#91;1,C,T,H,W&#93;
    return video, frames_np</code></pre></details>
<h2 id="fastvideo.pipelines.preprocess.preprocess_ltx2_overfit.main"><code>fastvideo.pipelines.preprocess.preprocess_ltx2_overfit.main</code></h2>
<pre class="doc-signature"><code>main() -&gt; None</code></pre>

<details><summary>Source</summary><pre><code>def main() -&gt; None:
    _init_single_process_distributed()

    from fastvideo.fastvideo_args import FastVideoArgs
    from fastvideo.models.loader.component_loader import (
        PipelineComponentLoader, )
    from fastvideo.pipelines.basic.ltx2.pipeline_configs import LTX2T2VConfig

    data_dir = envs.FASTVIDEO_TEST_LTX2_OVERFIT_DATA_DIR.get()
    caption_json = envs.FASTVIDEO_TEST_LTX2_OVERFIT_CAPTION_JSON.get()
    video_subdir = envs.FASTVIDEO_TEST_LTX2_OVERFIT_VIDEO_SUBDIR.get()
    output_dir = envs.FASTVIDEO_TEST_LTX2_OVERFIT_OUTPUT_DIR.get()
    model_repo = envs.FASTVIDEO_TEST_LTX2_OVERFIT_MODEL.get()
    # The train dataloader samples with drop_last=True across data-parallel
    # groups, so the dataset must hold at least num_sp_groups * batch_size
    # rows or every rank gets zero batches. Replicate the overfit sample so
    # a 4-GPU FSDP run still sees one batch per rank.
    num_copies = envs.FASTVIDEO_TEST_LTX2_OVERFIT_NUM_COPIES.get()

    device = torch.device(&quot;cuda:0&quot;)
    model_path = maybe_download_model(model_repo)
    model_index = verify_model_config_and_directory(model_path)

    os.makedirs(output_dir, exist_ok=True)
    # The map-style dataset caches parquet file metadata; a stale cache
    # next to a regenerated parquet can crash or serve old rows.
    shutil.rmtree(os.path.join(output_dir, &quot;map_style_cache&quot;), ignore_errors=True)

    with open(os.path.join(data_dir, caption_json)) as f:
        caption_data = json.load(f)

    pipeline_config = LTX2T2VConfig()
    fastvideo_args = FastVideoArgs(
        model_path=model_path,
        pipeline_config=pipeline_config,
        num_gpus=1,
        tp_size=1,
        sp_size=1,
        hsdp_shard_dim=1,
        vae_cpu_offload=False,
        text_encoder_cpu_offload=False,
    )

    def load_component(name: str) -&gt; Any:
        transformers_or_diffusers, _ = model_index&#91;name&#93;
        return PipelineComponentLoader.load_module(
            module_name=name,
            component_model_path=os.path.join(model_path, name),
            transformers_or_diffusers=transformers_or_diffusers,
            fastvideo_args=fastvideo_args,
        )

    print(&quot;Loading LTX-2 VAE...&quot;)
    vae = load_component(&quot;vae&quot;)
    vae_dtype = next(vae.parameters()).dtype
    print(f&quot;VAE loaded (&#123;sum(p.numel() for p in vae.parameters())/1e6:.0f&#125;M, &#123;vae_dtype&#125;)&quot;)

    print(&quot;Loading Gemma text encoder + tokenizer...&quot;)
    text_encoder = load_component(&quot;text_encoder&quot;)
    tokenizer = load_component(&quot;tokenizer&quot;)
    tokenizer.padding_side = &quot;left&quot;
    if tokenizer.pad_token is None and tokenizer.eos_token is not None:
        tokenizer.pad_token = tokenizer.eos_token

    encoder_config = pipeline_config.text_encoder_configs&#91;0&#93;
    tokenizer_kwargs = dict(encoder_config.tokenizer_kwargs)
    if &quot;max_length&quot; not in tokenizer_kwargs:
        tokenizer_kwargs&#91;&quot;max_length&quot;&#93; = encoder_config.arch_config.text_len
    preprocess_text = pipeline_config.preprocess_text_funcs&#91;0&#93;

    # --- Process each video ---
    records = &#91;&#93;
    for idx, item in enumerate(caption_data):
        video_name = item&#91;&quot;path&quot;&#93;
        record_id = f&quot;&#123;idx:04d&#125;_&#123;video_name&#125;&quot;
        caption = item&#91;&quot;cap&quot;&#93;&#91;0&#93; if isinstance(item&#91;&quot;cap&quot;&#93;, list) else item&#91;&quot;cap&quot;&#93;
        video_path = os.path.join(data_dir, video_subdir, video_name)

        print(f&quot;\nProcessing: &#123;video_name&#125;&quot;)
        print(f&quot;  Caption: &#123;caption&#91;:80&#93;&#125;...&quot;)

        video, frames_np = load_video(video_path, NUM_FRAMES, TRAIN_FPS, MAX_HEIGHT, MAX_WIDTH)
        video = video.to(device=device, dtype=vae_dtype)
        print(f&quot;  Video shape: &#123;video.shape&#125;&quot;)

        with torch.no_grad():
            # LTX-2 encode() returns a deterministic distribution whose
            # mean is already per-channel normalized; store as-is.
            latent = vae.encode(video).mean.squeeze(0).float().cpu()
        print(f&quot;  Latent shape: &#123;latent.shape&#125;&quot;)

        with torch.no_grad():
            # Encode through forward() — the inference text path. The
            # two-step preprocess_text_embeddings + run_connectors route
            # breaks on LTX-2.3, whose separate audio feature extractor
            # is narrower than the video one.
            text_inputs = tokenizer(&#91;preprocess_text(caption)&#93;, **tokenizer_kwargs)
            encoder_out = text_encoder(
                input_ids=text_inputs&#91;&quot;input_ids&quot;&#93;.to(device),
                attention_mask=text_inputs&#91;&quot;attention_mask&quot;&#93;.to(device),
            )
            text_embedding = encoder_out.last_hidden_state.squeeze(0).float().cpu()
        print(f&quot;  Text embedding shape: &#123;text_embedding.shape&#125;&quot;)

        record = &#123;
            &quot;id&quot;: record_id,
            &quot;vae_latent_bytes&quot;: latent.numpy().tobytes(),
            &quot;vae_latent_shape&quot;: list(latent.shape),
            &quot;vae_latent_dtype&quot;: str(latent.dtype).replace(&quot;torch.&quot;, &quot;&quot;),
            &quot;text_embedding_bytes&quot;: (text_embedding.numpy().tobytes()),
            &quot;text_embedding_shape&quot;: list(text_embedding.shape),
            &quot;text_embedding_dtype&quot;: str(text_embedding.dtype).replace(&quot;torch.&quot;, &quot;&quot;),
            &quot;file_name&quot;: video_name,
            &quot;caption&quot;: caption,
            &quot;media_type&quot;: &quot;video&quot;,
            &quot;width&quot;: MAX_WIDTH,
            &quot;height&quot;: MAX_HEIGHT,
            &quot;num_frames&quot;: NUM_FRAMES,
            &quot;duration_sec&quot;: NUM_FRAMES / TRAIN_FPS,
            &quot;fps&quot;: TRAIN_FPS,
        &#125;
        records.append(record)

        # Save the preprocessed clip so overfit tests can compare
        # validation output against the memorization target.
        import imageio
        ref_path = os.path.join(output_dir, f&quot;training_sample_&#123;idx&#125;.mp4&quot;)
        with imageio.get_writer(ref_path, fps=TRAIN_FPS) as writer:
            for frame in frames_np:
                writer.append_data(frame)
        print(f&quot;  Wrote reference clip to &#123;ref_path&#125;&quot;)

    # Clean up
    del text_encoder, tokenizer, vae
    torch.cuda.empty_cache()

    # Write parquet (replicated num_copies times; see the comment above num_copies)
    replicated = &#91;&#93;
    for copy_idx in range(max(1, num_copies)):
        for r in records:
            row = dict(r)
            row&#91;&quot;id&quot;&#93; = f&quot;&#123;r&#91;&#x27;id&#x27;&#93;&#125;_copy&#123;copy_idx&#125;&quot;
            replicated.append(row)
    table = pa.table(
        &#123;k: &#91;r&#91;k&#93; for r in replicated&#93;
         for k in replicated&#91;0&#93;&#125;,
        schema=pyarrow_schema_t2v,
    )
    output_path = os.path.join(output_dir, &quot;data_00000.parquet&quot;)
    pq.write_table(table, output_path)
    print(f&quot;\nWrote &#123;len(replicated)&#125; records &quot;
          f&quot;(&#123;len(records)&#125; unique x &#123;max(1, num_copies)&#125; copies) to &#123;output_path&#125;&quot;)

    # Write validation prompts for the validation callback
    val_prompts = &#123;
        &quot;data&quot;: &#91;&#123;
            &quot;caption&quot;: (item&#91;&quot;cap&quot;&#93;&#91;0&#93; if isinstance(item&#91;&quot;cap&quot;&#93;, list) else item&#91;&quot;cap&quot;&#93;),
        &#125; for item in caption_data&#93;,
    &#125;
    val_path = os.path.join(output_dir, &quot;validation_prompts.json&quot;)
    with open(val_path, &quot;w&quot;) as f:
        json.dump(val_prompts, f, indent=2)
    print(f&quot;Wrote validation prompts to &#123;val_path&#125;&quot;)

    print(&quot;\nDone! Use data_path: &quot; + output_dir + &quot; in training config.&quot;)</code></pre></details>
</div>
