Skip to content
FastVideo
Esc
↑↓navigate↵open⌘Jpreview
On this page

preprocess_ltx2_overfit

Preprocess LTX-2 overfit data into parquet format.

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.

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).

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.

Usage

CUDA_VISIBLE_DEVICES=0 python fastvideo/pipelines/preprocess/preprocess_ltx2_overfit.py

fastvideo.pipelines.preprocess.preprocess_ltx2_overfit.maybe_download_model

maybe_download_model(model_name_or_path: str, local_dir: str | None = None, download: bool = True, revision: str | None = None, allow_patterns: list[str] | None = None) -> str

Check if the model path is a Hugging Face Hub model ID and download it if needed.

Supports an "umbrella" repo layout where a single HF repo holds multiple pipeline variants under sibling subfolders. If the input is shaped as org/repo/subfolder (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.

Parameters

model_name_or_path: str

Local path, Hugging Face Hub model ID, or org/repo/subfolder umbrella-repo reference.

local_dir: str | None

Local directory to save the model

download: bool

Whether to download the model from Hugging Face Hub

revision: str | None

Optional immutable Hub revision.

allow_patterns: list[str] | None

Optional Hub glob patterns limiting downloaded files. Local paths are returned unchanged. For umbrella references, the patterns are interpreted relative to the selected subfolder.

Returns

str

Local path to the model (or to the subfolder inside the snapshot).

fastvideo.pipelines.preprocess.preprocess_ltx2_overfit.verify_model_config_and_directory

verify_model_config_and_directory(model_path: str, required_component_dirs: list[str] | tuple[str, ...] | None = None) -> dict[str, Any]

Verify that the model directory contains a valid Diffusers configuration.

Parameters

model_path: str

Path to the model directory

required_component_dirs: list[str] | tuple[str, ...] | None

Component directories required by the selected pipeline. None preserves full-snapshot validation; an empty collection validates only the manifest.

Returns

dict[str, Any]

The loaded model configuration as a dictionary

fastvideo.pipelines.preprocess.preprocess_ltx2_overfit.load_video

load_video(path: str, num_frames: int, target_fps: float, height: int, width: int) -> tuple[torch.Tensor, np.ndarray]

Load a video as [1, C, T, H, W] in [-1, 1], resampled to target_fps.

Also returns the uint8 RGB frames [T, H, W, C] for reference-video export.

Source
def load_video(path: str, num_frames: int, target_fps: float, height: int,
               width: int) -> tuple[torch.Tensor, np.ndarray]:
    """Load a video as [1, C, T, H, W] in [-1, 1], resampled to target_fps.
Also returns the uint8 RGB frames [T, H, W, C] for reference-video export.
"""
with av.open(path) as container:
    if not container.streams.video:
        raise RuntimeError(f"No video stream found in {path}")
    stream = container.streams.video[0]
    native_fps = float(stream.average_rate or target_fps)
    decoded = [torch.from_numpy(frame.to_ndarray(format="rgb24")) for frame in container.decode(video=0)]
if not decoded:
    raise RuntimeError(f"Could not read any frames from {path}")
raw = torch.stack(decoded).permute(0, 3, 1, 2)  # [T, C, H, W] uint8
step = native_fps / target_fps
wanted = [min(int(round(i * step)), raw.shape[0] - 1) for i in range(num_frames)]

frames = raw[wanted].float()  # [T, C, H, W] in [0, 255]
src_h, src_w = frames.shape[2], frames.shape[3]
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="bilinear", antialias=True)
top = (new_h - height) // 2
left = (new_w - width) // 2
frames = frames[:, :, top:top + height, left:left + width]

frames_np = (frames.permute(0, 2, 3, 1).clamp(0, 255).round().to(torch.uint8).numpy())
video = frames / 127.5 - 1.0  # [0,255] -> [-1,1]
video = video.permute(1, 0, 2, 3).unsqueeze(0)  # [1,C,T,H,W]
return video, frames_np</code></pre></details>

fastvideo.pipelines.preprocess.preprocess_ltx2_overfit.main

main() -> None
Source
def main() -> 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>

Was this page helpful?