LTX-2.3 Video, Image & Audio on Apple Silicon (MLX) — Claude Code Blueprint
What this is: A reproducible blueprint for Claude Code. Give this file to Claude Code and say:
"Here's a blueprint for running LTX-2.3 locally on Mac with MLX. Build me this setup."
What it builds: A fully local creative studio — video, image, and audio generation — using Lightricks' LTX-2.3 (22B) on Apple Silicon via MLX, with Z-Image-Turbo for images, all routed through a FastAPI proxy compatible with the official LTX Desktop Electron app.
Requirements: Mac with Apple Silicon (M1 Pro/Max or better, 64GB+ RAM recommended), Python 3.11+, uv package manager
Architecture Overview
LTX Desktop (Electron App) Your own PySide6 App
├── Video Gen ──┐ |
├── Image Gen ──┤ |
└── Audio Gen ──┤ |
v v
FastAPI Proxy Server (port 8080)
mimics api.ltx.video + fal.run
|
┌─────────┴──────────┐
v v
mlx-video Engine zimg Server (port 8001)
LTX-2.3 pipeline Z-Image-Turbo (PyTorch MPS)
Video + Audio Images
| |
v v
Apple Silicon GPU (Metal / MPS)
Port Layout:
Port
Service
8000
LTX Desktop Backend (Electron app's own Python backend)
1# Option A: HuggingFace CLI2huggingface-cli download Lightricks/LTX-2.3
34# Option B: Manual download to HF cache5mkdir -p ~/.cache/huggingface/hub/models--Lightricks--LTX-2.3/snapshots/manual/
6# Download these files into that directory:7# - ltx-2.3-22b-distilled.safetensors (43GB)8# - ltx-2.3-spatial-upscaler-x2-1.0.safetensors (950MB)
1.3 Critical fixes for LTX-2.3 on MLX
The base mlx-video repo (as of March 2026) has several bugs when running LTX-2.3. These are the fixes that took us from "pure noise output" to working video generation.
Fix 1: Transformer AdaLN Parameter Index Swap (CRITICAL)
File:mlx_video/models/ltx/transformer.pyProblem: The AdaLN (Adaptive Layer Normalization) parameter indices for cross-attention and MLP are swapped.
The LTX-2.3 scale_shift_table has 9 parameters per block. The correct ordering (from the PyTorch reference) is:
[0:3] — Self-attention (shift, scale, gate)
[3:6] — MLP (shift, scale, gate)
[6:9] — Cross-attention (shift, scale, gate)
The bug: Cross-attention reads from slice(3, 6) (MLP's params) and MLP reads from slice(6, 9) (cross-attention's params). This causes complete feature corruption — the model outputs noise.
Fix: In BasicAVTransformerBlock.__call__():
Cross-attention section (~line 304): Change slice(3, 6) to slice(6, 9)
python
1# BEFORE (wrong):2vshift_ca, vscale_ca, vgate_ca = self.get_ada_values(3 self.scale_shift_table, vx.shape[0], video.timesteps,slice(3,6)4)5# AFTER (correct):6vshift_ca, vscale_ca, vgate_ca = self.get_ada_values(7 self.scale_shift_table, vx.shape[0], video.timesteps,slice(6,9)8)
MLP section (~line 423): Change conditional mlp_start = 6 if cross_attention_adaln else 3 to always use slice(3, 6)
Apply the same fix to the audio path — same indices for audio_scale_shift_table.
Fix 2: Text Encoder V2 Auto-Detection (CRITICAL)
File:mlx_video/models/ltx/text_encoder.pyProblem: The text encoder initializes with LTX-2.0 dimensions (hidden_dim=3840, 2 connector layers) but LTX-2.3 uses different dimensions (hidden_dim=4096, 8 connector layers). The weight loading glob also doesn't match LTX-2.3 filenames.
Fix: Add V2 auto-detection in LTX2TextEncoder:
Expand the weight file glob to match ltx-2*.safetensors (not just ltx-2-19*.safetensors)
Add _detect_and_reinit_for_v2() method that:
Detects V2 by checking for text_embedding_projection.video_aggregate_embed.weight key
Reads hidden_dim from weight shape (4096 for V2)
Counts connector layers from weight keys (8 for V2)
Reinitializes all components with correct dimensions
Handle V2 weight key differences:
V2 uses video_aggregate_embed (with bias) vs V1's aggregate_embed (without bias)
V2 has separate audio_aggregate_embed and audio_feature_extractor
File:mlx_video/models/ltx/text_encoder.pyProblem: The ConnectorAttention class is missing to_gate_logits — a per-head gating mechanism used in LTX-2.3. This means 32 weight tensors (2 per connector block × 16 blocks) are silently dropped during weight loading.
Fix: Add gated attention to ConnectorAttention:
python
1classConnectorAttention(nn.Module):2def__init__(self, dim, num_heads, head_dim, apply_gated_attention=False):3# ... existing init ...4if apply_gated_attention:5 self.to_gate_logits = nn.Linear(dim, num_heads, bias=True)67def__call__(self, x,...):8# ... existing attention computation ...9 out = self.to_out(out)1011if self.apply_gated_attention:12 gate =2.0* mx.sigmoid(self.to_gate_logits(x))13 gate = mx.expand_dims(gate, axis=-1)# (B, seq, heads, 1)14 out = mx.reshape(out,(batch_size, seq_len, self.num_heads, self.head_dim))15 out = out * gate
16 out = mx.reshape(out,(batch_size, seq_len,-1))17return out
Pass apply_gated_attention=True through ConnectorTransformerBlock → Embeddings1DConnector → _init_components() when V2 is detected.
Fix 4: Resolution Divisibility (MINOR)
Problem: Some resolution presets (e.g., 960x544) have dimensions not divisible by 64, which MLX requires.
Fix: Add a _round_to_64() helper and apply it to all resolutions:
Create a proxy server that mimics api.ltx.video endpoints so the LTX Desktop Electron app works with local generation.
2.1 Required endpoints
GET /health → {"status": "ok", "mode": "local_mlx", "model": "Lightricks/LTX-2.3"}
POST /v1/text-to-video → prompt, resolution, num_frames, fps, seed, generate_audio → MP4 bytes
POST /v1/image-to-video → image + prompt → MP4 bytes
POST /v1/audio-to-video → audio file + image + prompt → generates video, muxes with uploaded audio
POST /v1/upload → image/audio file upload → returns URL for I2V/A2V
POST /fal-ai/z-image/turbo → FAL-compatible image generation → proxied to zimg:8001
2.2 Key proxy logic
python
1# Resolution mapping (API string → pixel dimensions)2RESOLUTION_MAP ={3"960x544":(960,576),# 544→576 (divisible by 64)4"1280x720":(1280,704),# 720→7045"1920x1080":(1920,1088),# 1080→10886"512x512":(512,512),7}89# The proxy converts API resolution strings like "540p" + "16:9" → "960x576"10# Then calls mlx_video.generate.generate_video() directly
2.3 Environment variables
bash
1LTX_MODEL_REPO=Lightricks/LTX-2.3 # Which model to load2LTX_PROXY_HOST=127.0.0.1 # Listen address3LTX_PROXY_PORT=8080# Listen port4ZIMG_BASE_URL=http://localhost:8001 # zimg server for image generation
Chunked Conv3d: MLX conv3d can fail with large tensors (~33+ frames at 192x192), use temporal chunking
Part 5: Image Generation (Z-Image-Turbo)
LTX Desktop uses FAL's Z-Image API for image generation. We intercept those calls and route them to a local zimg server running Z-Image-Turbo on PyTorch MPS.
5.1 Install zimg
uv tool install zimg
5.2 How the image proxy works
The proxy intercepts FAL API calls from LTX Desktop and translates them:
LTX Desktop → POST /fal-ai/z-image/turbo (FAL format)
→ Proxy translates to zimg format
→ POST http://localhost:8001/generate
→ zimg generates image via PyTorch MPS
→ Proxy translates response back to FAL format
→ Returns to LTX Desktop
Key translation details:
FAL sends image_size: {"width": W, "height": H} → zimg expects width and height as top-level params
Models swap sequentially, not loaded simultaneously
Part 6: Audio Generation (LTX-2.3 Native)
LTX-2.3 has complete audio generation built into the model weights — 4,123 audio-specific parameters including transformer audio attention, audio VAE decoder, and BigVGAN vocoder. Audio is generated in a single pass alongside video.
Fix 5: Audio Connector Head Configuration (CRITICAL)
File:mlx_video/models/ltx/text_encoder.pyProblem: Audio connector initializes with 16 heads × 128 dim (same as video), but LTX-2.3 audio uses 32 heads × 64 dim. Gate logits weight has shape (32,), causing broadcast failures.
Fix:
python
1# When gated attention is enabled (V2/LTX-2.3), audio uses different head config2audio_head_dim =64if apply_gated_attention else1283audio_heads = audio_dim // audio_head_dim # 2048/64 = 32 heads
Fix 6: BigVGAN Vocoder (CRITICAL)
File:mlx_video/models/ltx/audio_vae/bigvgan.py (NEW FILE)
Problem: LTX-2.3 uses BigVGAN vocoder (not HiFi-GAN). The existing Vocoder class has 5 upsample layers with upsample_initial_channel=1024, but BigVGAN needs 6 layers with upsample_initial_channel=1536.
Key BigVGAN components:
SnakeBeta activation:f(x) = x + (1/beta) * sin²(alpha * x) with learnable per-channel alpha and beta
AMPBlock1: Residual blocks with SnakeBeta activations (replacing LeakyReLU)
Anti-aliased resampling: UpSample1d/DownSample1d with FIR lowpass filters (weights loaded for compatibility)
Activation1d: Wraps SnakeBeta; applies activation directly for length preservation