unsloth/studio/backend/core/inference/video_ltx2.py
Daniel Han b671038f2d Stop staging the dense text encoder for an fp8 video load
Two halves of the same gap, found while measuring the LTX-2.3 download plan:

- The video download plan and the scoped pre-download never saw
  text_encoder_quant. An fp8 request loads a hosted pre-cast encoder, so
  asking for one still staged and downloaded the base repo's dense Gemma3
  (48.79 GB of Lightricks/LTX-2 on the 2.3 distilled pick) that the pipeline
  then never opened. The plan now drops those shards and stages the pre-cast
  checkpoint instead; their configs stay, since the pre-cast loader still
  meta-inits the encoder from the base repo's component config.

- The LTX-2.3 assembly builds every component itself, so pipe_kwargs (which
  carries the pre-cast encoder for from_pretrained) never reached it and an
  fp8 request silently loaded the dense encoder anyway. It is passed across
  explicitly now.

The dense skip is earned, not assumed: only a pre-cast checkpoint that
resolves on the Hub lets the plan drop the dense shards, and only one already
fetched to disk lets the pull drop them, so an unpublished or gated artifact
leaves both exactly as they were. If injection still fails after that, the
load tops the dense weights back up rather than handing from_pretrained a
snapshot with no encoder in it.

Measured against the real Hub on the 2.3 distilled Q4_K_M pick: 67.24 GB
before, 18.92 GB with a 0.43 GB stand-in for the pre-cast artifact (the base
entry drops from 24 files / 48.79 GB to 13 files / 0.04 GB).
2026-07-27 04:28:12 +00:00

633 lines
24 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""LTX-2.3 pipeline assembly for diffusers 0.39.
diffusers 0.39 ships every LTX-2.3 model class but its single-file loader maps every LTX-2
checkpoint to the 2.0 config, so 2.3 checkpoints fail a shape check at load. The community
transformer-only GGUFs also carry the DiT + connectors but NOT the text projections, VAEs, or
vocoder that 2.3 moved out of the transformer. This assembles the full 2.3 pipeline:
- transformer: from the checkpoint via ``from_single_file`` with the 2.3 config overrides and the
``prompt_adaln_single`` keys pre-renamed (the library converter doesn't know them).
- connectors: from the checkpoint's connector keys plus the ``text_embedding_projection`` tensors,
fetched from the companion file in ``unsloth/LTX-2.3-GGUF`` when not bundled.
- video/audio VAE, vocoder: from the checkpoint when bundled, else the companion files.
- scheduler, text encoder (Gemma3), tokenizer: from the LTX-2.0 base repo, which 2.3 shares.
Every config and rename table mirrors diffusers' ``scripts/convert_ltx2_to_diffusers.py`` (the
authoritative 2.3 mapping the loader hasn't absorbed). Assembled through the constructor, not
``from_pretrained``, because the vocoder class differs from the base pin (``LTX2VocoderWithBWE`` vs
``LTX2Vocoder``) and the type gate would reject it.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Optional
from loggers import get_logger
logger = get_logger(__name__)
# Companion files (text projections, VAEs incl. vocoder) next to the quants in unsloth's GGUF
# repo: the official Lightricks weights split out of the combined checkpoint. Keyed by variant.
LTX23_EXTRAS_REPO = "unsloth/LTX-2.3-GGUF"
_EXTRAS_TEXT_PROJ = "text_encoders/ltx-2.3-22b-{variant}_embeddings_connectors.safetensors"
_EXTRAS_VIDEO_VAE = "vae/ltx-2.3-22b-{variant}_video_vae.safetensors"
_EXTRAS_AUDIO_VAE = "vae/ltx-2.3-22b-{variant}_audio_vae.safetensors"
# ── configs + rename tables, verbatim from scripts/convert_ltx2_to_diffusers.py ──
# from_single_file config overrides on top of the base 2.0 transformer config.
LTX_2_3_TRANSFORMER_CONFIG_OVERRIDES: dict[str, Any] = {
"gated_attn": True,
"cross_attn_mod": True,
"audio_gated_attn": True,
"audio_cross_attn_mod": True,
"use_prompt_embeddings": False,
"perturbed_attn": True,
}
# Keys the 2.0-era converter doesn't know; renamed before from_single_file. Audio prefix first.
_TRANSFORMER_PRERENAME = (
("audio_prompt_adaln_single.", "audio_prompt_adaln."),
("prompt_adaln_single.", "prompt_adaln."),
)
_CONNECTOR_KEY_PREFIXES = (
"video_embeddings_connector",
"audio_embeddings_connector",
"transformer_1d_blocks",
"text_embedding_projection",
"connectors.",
"video_connector",
"audio_connector",
"text_proj_in",
)
_CONNECTORS_RENAME = {
"connectors.": "",
"video_embeddings_connector": "video_connector",
"audio_embeddings_connector": "audio_connector",
"transformer_1d_blocks": "transformer_blocks",
"text_embedding_projection.audio_aggregate_embed": "audio_text_proj_in",
"text_embedding_projection.video_aggregate_embed": "video_text_proj_in",
"q_norm": "norm_q",
"k_norm": "norm_k",
}
_CONNECTORS_CONFIG: dict[str, Any] = {
"caption_channels": 3840,
"text_proj_in_factor": 49,
"video_connector_num_attention_heads": 32,
"video_connector_attention_head_dim": 128,
"video_connector_num_layers": 8,
"video_connector_num_learnable_registers": 128,
"video_gated_attn": True,
"audio_connector_num_attention_heads": 32,
"audio_connector_attention_head_dim": 64,
"audio_connector_num_layers": 8,
"audio_connector_num_learnable_registers": 128,
"audio_gated_attn": True,
"connector_rope_base_seq_len": 4096,
"rope_theta": 10000.0,
"rope_double_precision": True,
"causal_temporal_positioning": False,
"rope_type": "split",
"per_modality_projections": True,
"video_hidden_dim": 4096,
"audio_hidden_dim": 2048,
"proj_bias": True,
}
_VIDEO_VAE_RENAME = {
# Encoder
"down_blocks.0": "down_blocks.0",
"down_blocks.1": "down_blocks.0.downsamplers.0",
"down_blocks.2": "down_blocks.1",
"down_blocks.3": "down_blocks.1.downsamplers.0",
"down_blocks.4": "down_blocks.2",
"down_blocks.5": "down_blocks.2.downsamplers.0",
"down_blocks.6": "down_blocks.3",
"down_blocks.7": "down_blocks.3.downsamplers.0",
"down_blocks.8": "mid_block",
# Decoder (2.3 adds up_blocks.7/8: a 4th decoder stage)
"up_blocks.0": "mid_block",
"up_blocks.1": "up_blocks.0.upsamplers.0",
"up_blocks.2": "up_blocks.0",
"up_blocks.3": "up_blocks.1.upsamplers.0",
"up_blocks.4": "up_blocks.1",
"up_blocks.5": "up_blocks.2.upsamplers.0",
"up_blocks.6": "up_blocks.2",
"up_blocks.7": "up_blocks.3.upsamplers.0",
"up_blocks.8": "up_blocks.3",
"last_time_embedder": "time_embedder",
"last_scale_shift_table": "scale_shift_table",
# Common
"res_blocks": "resnets",
"per_channel_statistics.mean-of-means": "latents_mean",
"per_channel_statistics.std-of-means": "latents_std",
}
_VIDEO_VAE_REMOVE_SUFFIXES = (
"per_channel_statistics.channel",
"per_channel_statistics.mean-of-stds",
)
_VIDEO_VAE_CONFIG: dict[str, Any] = {
"in_channels": 3,
"out_channels": 3,
"latent_channels": 128,
"block_out_channels": (256, 512, 1024, 1024),
"down_block_types": (
"LTX2VideoDownBlock3D",
"LTX2VideoDownBlock3D",
"LTX2VideoDownBlock3D",
"LTX2VideoDownBlock3D",
),
"decoder_block_out_channels": (256, 512, 512, 1024),
"layers_per_block": (4, 6, 4, 2, 2),
"decoder_layers_per_block": (4, 6, 4, 2, 2),
"spatio_temporal_scaling": (True, True, True, True),
"decoder_spatio_temporal_scaling": (True, True, True, True),
"decoder_inject_noise": (False, False, False, False, False),
"downsample_type": ("spatial", "temporal", "spatiotemporal", "spatiotemporal"),
"upsample_type": ("spatiotemporal", "spatiotemporal", "temporal", "spatial"),
"upsample_residual": (False, False, False, False),
"upsample_factor": (2, 2, 1, 2),
"timestep_conditioning": False,
"patch_size": 4,
"patch_size_t": 1,
"resnet_norm_eps": 1e-6,
"encoder_causal": True,
"decoder_causal": False,
"encoder_spatial_padding_mode": "zeros",
"decoder_spatial_padding_mode": "zeros",
"spatial_compression_ratio": 32,
"temporal_compression_ratio": 8,
}
_AUDIO_VAE_RENAME = {
"per_channel_statistics.mean-of-means": "latents_mean",
"per_channel_statistics.std-of-means": "latents_std",
}
# Same config as LTX-2.0 (upstream's comment); the weights are still 2.3-specific.
_AUDIO_VAE_CONFIG: dict[str, Any] = {
"base_channels": 128,
"output_channels": 2,
"ch_mult": (1, 2, 4),
"num_res_blocks": 2,
"attn_resolutions": None,
"in_channels": 2,
"resolution": 256,
"latent_channels": 8,
"norm_type": "pixel",
"causality_axis": "height",
"dropout": 0.0,
"mid_block_add_attention": False,
"sample_rate": 16000,
"mel_hop_length": 160,
"is_causal": True,
"mel_bins": 64,
"double_z": True,
}
_VOCODER_RENAME = {
"resblocks": "resnets",
"conv_pre": "conv_in",
"conv_post": "conv_out",
"act_post": "act_out",
"downsample.lowpass": "downsample",
}
_VOCODER_CONFIG: dict[str, Any] = {
"in_channels": 128,
"hidden_channels": 1536,
"out_channels": 2,
"upsample_kernel_sizes": [11, 4, 4, 4, 4, 4],
"upsample_factors": [5, 2, 2, 2, 2, 2],
"resnet_kernel_sizes": [3, 7, 11],
"resnet_dilations": [[1, 3, 5], [1, 3, 5], [1, 3, 5]],
"act_fn": "snakebeta",
"leaky_relu_negative_slope": 0.1,
"antialias": True,
"antialias_ratio": 2,
"antialias_kernel_size": 12,
"final_act_fn": None,
"final_bias": False,
"bwe_in_channels": 128,
"bwe_hidden_channels": 512,
"bwe_out_channels": 2,
"bwe_upsample_kernel_sizes": [12, 11, 4, 4, 4],
"bwe_upsample_factors": [6, 5, 2, 2, 2],
"bwe_resnet_kernel_sizes": [3, 7, 11],
"bwe_resnet_dilations": [[1, 3, 5], [1, 3, 5], [1, 3, 5]],
"bwe_act_fn": "snakebeta",
"bwe_leaky_relu_negative_slope": 0.1,
"bwe_antialias": True,
"bwe_antialias_ratio": 2,
"bwe_antialias_kernel_size": 12,
"bwe_final_act_fn": None,
"bwe_final_bias": False,
"filter_length": 512,
"hop_length": 80,
"window_length": 512,
"num_mel_channels": 64,
"input_sampling_rate": 16000,
"output_sampling_rate": 48000,
}
_DIT_PREFIX = "model.diffusion_model."
# ── checkpoint inspection ────────────────────────────────────────────────────
def read_checkpoint_header(checkpoint_path: Path | str) -> dict[str, tuple[int, ...]]:
"""Tensor name -> shape from the checkpoint HEADER only (no weight data). GGUF shapes come back
in GGML (reversed) order, so callers should membership-test, not assume a dimension position."""
names_shapes: dict[str, tuple[int, ...]] = {}
path = str(checkpoint_path)
if path.lower().endswith(".gguf"):
from gguf import GGUFReader
for tensor in GGUFReader(path).tensors:
names_shapes[str(tensor.name)] = tuple(int(x) for x in tensor.shape)
else:
from safetensors import safe_open
with safe_open(path, framework = "pt") as handle:
for name in handle.keys():
names_shapes[name] = tuple(handle.get_slice(name).get_shape())
return names_shapes
def is_ltx23_checkpoint(checkpoint_path: Path | str) -> bool:
"""True when the checkpoint carries the 9-row LTX-2.3 modulation tables (2.0 has 6-row
per-block scale/shift tables; 2.3 widens them to 9). An unreadable header returns False so the
caller falls back to the stock 2.0 path."""
try:
header = read_checkpoint_header(checkpoint_path)
except Exception as exc: # noqa: BLE001
logger.warning("video.ltx2_header_probe_failed: %s", exc)
return False
for name, shape in header.items():
if name.endswith("transformer_blocks.0.scale_shift_table"):
return 9 in shape
return False
# ── state-dict plumbing ──────────────────────────────────────────────────────
def _apply_rename(state: dict[str, Any], rename: dict[str, str]) -> dict[str, Any]:
out: dict[str, Any] = {}
for key, value in state.items():
new_key = key
for old, new in rename.items():
new_key = new_key.replace(old, new)
out[new_key] = value
return out
def _to_plain_dtype(state: dict[str, Any], torch_dtype: Any) -> dict[str, Any]:
"""Materialise every tensor as a plain torch tensor in torch_dtype. GGUF tensors arrive as
block-quantized GGUFParameter; the small non-DiT components run dense, so dequantize here."""
import torch
try:
from diffusers.quantizers.gguf.utils import GGUFParameter, dequantize_gguf_tensor
except Exception: # noqa: BLE001 -- gguf support not installed; plain tensors only
GGUFParameter, dequantize_gguf_tensor = (), None
out: dict[str, Any] = {}
for key, value in state.items():
if dequantize_gguf_tensor is not None and isinstance(value, GGUFParameter):
value = dequantize_gguf_tensor(value)
out[key] = value.to(torch_dtype) if isinstance(value, torch.Tensor) else value
return out
def _split_checkpoint(state: dict[str, Any]) -> dict[str, dict[str, Any]]:
"""Partition a combined LTX checkpoint into per-component state dicts. Handles both layouts: the
official combined single file (``vae.*`` / ``audio_vae.*`` / ``vocoder.*`` / DiT + projections)
and transformer-only GGUFs (bare DiT + connector keys)."""
groups: dict[str, dict[str, Any]] = {
"dit": {},
"connectors": {},
"vae": {},
"audio_vae": {},
"vocoder": {},
}
for key, value in state.items():
bare = key[len(_DIT_PREFIX) :] if key.startswith(_DIT_PREFIX) else key
if bare.startswith("vae."):
groups["vae"][bare[len("vae.") :]] = value
elif bare.startswith("audio_vae."):
groups["audio_vae"][bare[len("audio_vae.") :]] = value
elif bare.startswith("vocoder."):
groups["vocoder"][bare[len("vocoder.") :]] = value
elif bare.startswith(_CONNECTOR_KEY_PREFIXES):
groups["connectors"][bare] = value
else:
groups["dit"][bare] = value
return groups
def _load_extras_file(filename: str, hf_token: Optional[str]) -> dict[str, Any]:
from safetensors.torch import load_file
from utils.hf_xet_fallback import hf_hub_download_with_xet_fallback
path = hf_hub_download_with_xet_fallback(LTX23_EXTRAS_REPO, filename, hf_token)
return load_file(path)
def checkpoint_variant(checkpoint_path: Path | str) -> str:
"""Which companion-weight set a checkpoint pairs with ("dev"/"distilled"). The distilled-1.1
refresh only retrained the DiT, so it shares the distilled companions."""
return "dev" if "dev" in Path(checkpoint_path).name.lower() else "distilled"
def ltx23_extras_files(checkpoint_path: Path | str) -> tuple[str, ...]:
"""The companion files in ``LTX23_EXTRAS_REPO`` a 2.3 checkpoint loads alongside itself.
Same variant rule as the assembly, so the download plan stages exactly what the load reads
(they are otherwise fetched inline, outside the panel's progress, cancel and disk preflight)."""
variant = checkpoint_variant(checkpoint_path)
return tuple(
template.format(variant = variant)
for template in (_EXTRAS_TEXT_PROJ, _EXTRAS_VIDEO_VAE, _EXTRAS_AUDIO_VAE)
)
# Upstream ltx_core's DISTILLED_SIGMA_VALUES: the fixed 8-step sampling curve the 22B distilled
# DiT was trained against (the scheduler appends the terminal 0 itself). The base scheduler's
# resolution-shifted spacing lands far from it at every mu the pipeline can compute, so the
# distilled default of 8 steps must pass this list verbatim.
LTX23_DISTILLED_SIGMAS: tuple[float, ...] = (
1.0,
0.99375,
0.9875,
0.98125,
0.975,
0.909375,
0.725,
0.421875,
)
def ltx2_distilled_ids(*ids: Optional[str]) -> bool:
"""True when any loaded-checkpoint id names the distilled DiT (same substring the
generation-defaults table keys on, so sigmas and the 8-step default stay in lockstep)."""
return any("distilled" in str(i or "").lower() for i in ids)
def ltx23_verbatim_sigmas(pipe: Any) -> Any:
"""Context manager neutralising the scheduler transforms that re-shape even explicit
``sigmas`` (FlowMatchEulerDiscreteScheduler applies dynamic time-shift and the
shift_terminal stretch to caller-provided lists): dynamic shifting off, shift 1.0
(identity), no terminal stretch, restored on exit. Without this the calibrated curve
above would arrive at the DiT distorted (its 0.421875 tail clamped to 0.1)."""
import contextlib
@contextlib.contextmanager
def _ctx():
sched = getattr(pipe, "scheduler", None)
cfg = getattr(sched, "config", None)
register = getattr(sched, "register_to_config", None)
if cfg is None or not callable(register):
yield
return
saved = {
"use_dynamic_shifting": cfg.get("use_dynamic_shifting", False),
"shift": cfg.get("shift", 1.0),
"shift_terminal": cfg.get("shift_terminal", None),
}
register(use_dynamic_shifting = False, shift = 1.0, shift_terminal = None)
try:
yield
finally:
register(**saved)
return _ctx()
# ── component builders ───────────────────────────────────────────────────────
def _build_from_config(
model_cls: Any,
config: dict[str, Any],
state: dict[str, Any],
rename: dict[str, str],
torch_dtype: Any,
remove_suffixes: tuple[str, ...] = (),
) -> Any:
from accelerate import init_empty_weights
state = _apply_rename(_to_plain_dtype(state, torch_dtype), rename)
for key in [k for k in state if k.endswith(remove_suffixes)] if remove_suffixes else []:
state.pop(key)
with init_empty_weights():
model = model_cls.from_config(config)
model.load_state_dict(state, strict = True, assign = True)
return model.to(torch_dtype)
def load_ltx23_transformer(
dit_state: dict[str, Any],
*,
base_repo: str,
torch_dtype: Any,
is_gguf: bool,
hf_token: Optional[str],
) -> Any:
import diffusers
from diffusers import LTX2VideoTransformer3DModel
# Pre-rename the 2.3-only keys the converter doesn't know, then from_single_file merges the
# config overrides into the base 2.0 config and runs the stock key conversion.
for old, new in _TRANSFORMER_PRERENAME:
for key in [k for k in dit_state if k.startswith(old)]:
dit_state[new + key[len(old) :]] = dit_state.pop(key)
kwargs: dict[str, Any] = {
"config": base_repo,
"subfolder": "transformer",
"torch_dtype": torch_dtype,
"token": hf_token,
**LTX_2_3_TRANSFORMER_CONFIG_OVERRIDES,
}
if is_gguf:
kwargs["quantization_config"] = diffusers.GGUFQuantizationConfig(compute_dtype = torch_dtype)
return LTX2VideoTransformer3DModel.from_single_file(dit_state, **kwargs)
def load_ltx23_connectors(
connector_state: dict[str, Any], *, variant: str, torch_dtype: Any, hf_token: Optional[str]
) -> Any:
from diffusers.pipelines.ltx2.connectors import LTX2TextConnectors
# Transformer-only checkpoints carry the connector stacks but not the per-modality text
# projections; fetch those from the companion file.
if not any(k.startswith("text_embedding_projection") for k in connector_state):
connector_state = dict(connector_state)
connector_state.update(
_load_extras_file(_EXTRAS_TEXT_PROJ.format(variant = variant), hf_token)
)
return _build_from_config(
LTX2TextConnectors,
_CONNECTORS_CONFIG,
connector_state,
_CONNECTORS_RENAME,
torch_dtype,
)
def load_ltx23_vae(
vae_state: dict[str, Any], *, variant: str, torch_dtype: Any, hf_token: Optional[str]
) -> Any:
from diffusers import AutoencoderKLLTX2Video
if not vae_state:
vae_state = _load_extras_file(_EXTRAS_VIDEO_VAE.format(variant = variant), hf_token)
return _build_from_config(
AutoencoderKLLTX2Video,
_VIDEO_VAE_CONFIG,
vae_state,
_VIDEO_VAE_RENAME,
torch_dtype,
remove_suffixes = _VIDEO_VAE_REMOVE_SUFFIXES,
)
def load_ltx23_audio_vae_and_vocoder(
audio_vae_state: dict[str, Any],
vocoder_state: dict[str, Any],
*,
variant: str,
torch_dtype: Any,
hf_token: Optional[str],
) -> tuple[Any, Any]:
from diffusers import AutoencoderKLLTX2Audio
from diffusers.pipelines.ltx2.vocoder import LTX2VocoderWithBWE
if not audio_vae_state or not vocoder_state:
combined = _load_extras_file(_EXTRAS_AUDIO_VAE.format(variant = variant), hf_token)
audio_vae_state = {
k[len("audio_vae.") :]: v for k, v in combined.items() if k.startswith("audio_vae.")
}
vocoder_state = {
k[len("vocoder.") :]: v for k, v in combined.items() if k.startswith("vocoder.")
}
audio_vae = _build_from_config(
AutoencoderKLLTX2Audio,
_AUDIO_VAE_CONFIG,
audio_vae_state,
_AUDIO_VAE_RENAME,
torch_dtype,
)
# The 2.3 vocoder is a composite (base + bandwidth-extension stack + mel STFT buffers); keys line
# up module-for-module after the renames.
vocoder_state = _apply_rename(_to_plain_dtype(vocoder_state, torch_dtype), _VOCODER_RENAME)
for key in [k for k in vocoder_state if ".ups." in k]:
vocoder_state[key.replace(".ups.", ".upsamplers.")] = vocoder_state.pop(key)
from accelerate import init_empty_weights
with init_empty_weights():
vocoder = LTX2VocoderWithBWE.from_config(_VOCODER_CONFIG)
vocoder.load_state_dict(vocoder_state, strict = True, assign = True)
return audio_vae, vocoder.to(torch_dtype)
# ── pipeline assembly ────────────────────────────────────────────────────────
def load_ltx23_pipeline(
checkpoint_path: Path | str,
*,
base_repo: str,
torch_dtype: Any,
is_gguf: bool,
hf_token: Optional[str] = None,
text_encoder: Optional[Any] = None,
) -> Any:
"""Full LTX-2.3 pipeline from a single-file/GGUF checkpoint. Assembled per-component
(constructor, not from_pretrained) because the base model_index pins LTX2Vocoder while 2.3
needs LTX2VocoderWithBWE, which the type gate would reject.
``text_encoder`` supplies an already-built encoder (the caller's pre-cast fp8 Gemma3);
None builds it dense from the base repo. Because the assembly bypasses
``from_pretrained``, this is the only way an fp8 request reaches the 2.3 path."""
import transformers
from diffusers import LTX2Pipeline
from diffusers.loaders.single_file_utils import load_single_file_checkpoint
variant = checkpoint_variant(checkpoint_path)
logger.info(
"video.ltx23_assembly: variant=%s gguf=%s extras=%s",
variant,
is_gguf,
LTX23_EXTRAS_REPO,
)
state = load_single_file_checkpoint(str(checkpoint_path))
groups = _split_checkpoint(state)
del state
# The Lightricks fp8 single files store SCALED float8 weights (.weight_scale/.input_scale
# companions). Casting without the scales corrupts every quantized layer, so refuse loudly; use
# the GGUF quants (Q8_0 for highest fidelity) instead.
if any(k.endswith((".weight_scale", ".input_scale")) for k in groups["dit"]):
raise ValueError(
"This LTX checkpoint stores scaled fp8 weights, which this loader does "
"not dequantize yet. Use the GGUF quants from unsloth/LTX-2.3-GGUF "
"instead (Q8_0 for the highest fidelity) or the official bf16 checkpoint."
)
transformer = load_ltx23_transformer(
groups["dit"],
base_repo = base_repo,
torch_dtype = torch_dtype,
is_gguf = is_gguf,
hf_token = hf_token,
)
connectors = load_ltx23_connectors(
groups["connectors"],
variant = variant,
torch_dtype = torch_dtype,
hf_token = hf_token,
)
vae = load_ltx23_vae(groups["vae"], variant = variant, torch_dtype = torch_dtype, hf_token = hf_token)
audio_vae, vocoder = load_ltx23_audio_vae_and_vocoder(
groups["audio_vae"],
groups["vocoder"],
variant = variant,
torch_dtype = torch_dtype,
hf_token = hf_token,
)
# Shared 2.0/2.3 components from the base repo, via model_index so upstream class renames break
# loudly here rather than drift silently.
index = LTX2Pipeline.load_config(base_repo, token = hf_token)
def _sub(name: str, **extra: Any) -> Any:
library, class_name = index[name]
module = transformers if library == "transformers" else __import__("diffusers")
return getattr(module, class_name).from_pretrained(
base_repo, subfolder = name, token = hf_token, **extra
)
scheduler = _sub("scheduler")
tokenizer = _sub("tokenizer")
if text_encoder is None:
text_encoder = _sub("text_encoder", torch_dtype = torch_dtype)
return LTX2Pipeline(
scheduler = scheduler,
text_encoder = text_encoder,
tokenizer = tokenizer,
connectors = connectors,
transformer = transformer,
vae = vae,
audio_vae = audio_vae,
vocoder = vocoder,
)