LTX-2 - Generation Audiovisuelle Conjointe (Video + Audio)

# Parameters
BATCH_MODE = "true"

Module : 02-Video-Advanced Niveau : Avance Technologies : LTX-2 (Lightricks), ltx-core + ltx-pipelines, Gemma 3, torch, bitsandbytes Duree estimee : 60 minutes VRAM : ~14-24 GB (quantization obligatoire : fp8-cast natif ou GGUF Q4 ; le modèle 22B ne tient pas en FP16 sur 24 GB)

Objectifs d’Apprentissage

Prerequis

  • GPU avec 20+ GB VRAM (RTX 3090 24GB minimum ; GGUF Q4 ~14 GB recommande, fp8-cast ~22 GB borderline)
  • Notebook 02-2 (LTX-Video 0.9) complete pour comparaison
  • Checkpoint LTX-2.3 telecharge (~44 GB) + spatial upsampler
  • Gemma 3 12B telecharge (text encoder, necessite acceptation licence sur HuggingFace)
  • Packages : torch, bitsandbytes, imageio, imageio-ffmpeg, av

Navigation : << 02-4 SVD | Index | Comparaison >>

# Parametres Papermill - JAMAIS modifier ce commentaire

# Configuration notebook
notebook_mode = "interactive"        # "interactive" ou "batch"
skip_widgets = False               # True pour mode batch MCP
debug_level = "INFO"

# --- Choix du chemin d'execution (decision pedagogique importante) ---
# ltx-pipelines 1.0.0 ne supporte QUE fp8-cast / fp8-scaled-mm comme policy de quantization.
# Pour un 22B, fp8-cast = ~22 GB de poids => BORDERLINE sur RTX 3090 (24 GB, ~2 GB de marge
# pour les activations sur 97 frames => OOM probable). Le chemin valide et VRAM-safe sur 3090
# est le path COMMUNAUTE ComfyUI + GGUF Q4 (~14 GB, ~10 GB de marge), documente dans la
# Section "ComfyUI + GGUF" ci-dessous. On garde ltx-pipelines comme REFERENCE (API officielle
# Lightricks), desactivee par defaut pour eviter une tentative OOM.
runtime_path = "comfyui_gguf"        # "comfyui_gguf" (recommande 3090) ou "ltx_pipelines" (reference)

# --- Modele LTX-2.3 (Lightricks, HF: Lightricks/LTX-2.3) - path ltx_pipelines (reference) ---
model_repo = "Lightricks/LTX-2.3"
model_file = "ltx-2.3-22b-dev.safetensors"        # Checkpoint principal 22B (~44 GB FP16)
spatial_upscaler_file = "ltx-2.3-spatial-upscaler-x1.5-1.0.safetensors"
distilled_lora_file = "ltx-2.3-22b-distilled-lora-384.safetensors"  # OBLIGATOIRE pour ti2vid_two_stages
gemma_repo = "google/gemma-3-12b-it-qat-q4_0-unquantized"  # Text encoder (gated HF)
device = "cuda"

# Quantization (policy REELLE ltx-pipelines 1.0.0) :
#   "fp8-cast"        -> cast FP8 avec upcast a l'inference (~22 GB pour le 22B)
#   "fp8-scaled-mm"   -> FP8 scaled matmul (optionnellement fichier amax de calibration)
# NB : "int4_nf4" / "int8" / "nf4" N'EXISTENT PAS dans ltx-pipelines (erreur frequente).
#      bitsandbytes INT4/NF4 n'est PAS branche par ltx-pipelines. Pour de la quantization
#      plus agressive (Q4 ~14 GB) => chemin ComfyUI + GGUF (cf Section dediee).
quantization = "fp8-cast"

# --- Parametres generation (communs aux deux chemins) ---
num_frames = 97                    # LTX-2 : num_frames = 8*k + 1 (97 = 8*12 + 1, ~4s a 24 fps)
num_inference_steps = 30           # Etapes de debruitage (stage 1 ; stage 2 utilise un schedule distille)
guidance_scale = 3.0               # CFG scale video (LTX-2 prefere un CFG bas)
height = 512                       # Hauteur video
width = 768                        # Largeur video
fps_output = 24                    # FPS de sortie
audio = True                       # Generation audio conjoint (feature cle de LTX-2)

# --- Endpoint ComfyUI (path comfyui_gguf) ---
comfyui_host = "http://127.0.0.1:8189"   # comfyui-video (GPU 3090), derriere ComfyUI-Login

# Configuration
run_generation = False             # Safety : passer a True apres avoir (a) telecharge les modeles GGUF
                                   # (cf tableau Section 2b) et (b) valide le wiring du workflow contre
                                   # /object_info. L'auth ComfyUI-Login FONCTIONNE (COMFYUI_VIDEO_PASSWORD +
                                   # restart) ; le verrou est la presence des modeles, PAS l'auth.
save_as_mp4 = True
save_results = True

print(f"LTX-2 Audiovisual | runtime={runtime_path} | quant={quantization}")
print(f"  {num_frames}f @ {width}x{height} | audio={audio} | run_generation={run_generation}")
LTX-2 Audiovisual | runtime=comfyui_gguf | quant=fp8-cast
  97f @ 768x512 | audio=True | run_generation=True
# Parameters
BATCH_MODE = True
# Setup environnement et imports
import os
import sys
import json
import time
import warnings
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Any, Optional
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
import logging

warnings.filterwarnings('ignore', category=DeprecationWarning)
warnings.filterwarnings('ignore', category=FutureWarning)

# Chargement robuste de la configuration .env (recherche recursive des parents,
# necessaire car Papermill change le cwd).
from dotenv import load_dotenv
current_path = Path.cwd()
GENAI_ROOT = current_path
env_loaded = False
for _ in range(10):
    env_path = current_path / ".env"
    if env_path.exists():
        load_dotenv(env_path)
        print(f".env charge depuis: {env_path.name}")
        env_loaded = True
        GENAI_ROOT = current_path
        break
    if current_path.name == "GenAI" or len(current_path.parts) <= 1:
        break
    current_path = current_path.parent
if not env_loaded:
    print("WARNING: .env non trouve, utilisation variables environnement")
    GENAI_ROOT = GENAI_ROOT.parent

# Helpers GenAI (pattern canonique, cf 02-2).
HELPERS_PATH = GENAI_ROOT / 'shared' / 'helpers'
if HELPERS_PATH.exists():
    sys.path.insert(0, str(HELPERS_PATH.parent))
    try:
        from helpers.genai_helpers import setup_genai_logging
        print("Helpers GenAI importes")
    except ImportError:
        print("Helpers GenAI non disponibles - mode autonome")

OUTPUT_DIR = GENAI_ROOT / 'outputs' / 'ltx2'
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

logging.basicConfig(level=getattr(logging, debug_level))
logger = logging.getLogger('ltx2')

print(f"LTX-2 - Generation Audiovisuelle Conjointe")
print(f"Date : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Mode : {notebook_mode}")
print(f"Modele : {model_repo} ({model_file})")
print(f"Quantization : {quantization} | Frames : {num_frames}, Steps : {num_inference_steps}, CFG : {guidance_scale}")
.env charge depuis: D:\Dev\CoursIA-10985-rot\MyIA.AI.Notebooks\GenAI\.env
Helpers GenAI importes
LTX-2 - Generation Audiovisuelle Conjointe
Date : 2026-08-20 22:43:57
Mode : interactive
Modele : Lightricks/LTX-2.3 (ltx-2.3-22b-dev.safetensors)
Quantization : fp8-cast | Frames : 97, Steps : 30, CFG : 3.0

Licence : LTX-2 Community License (important)

LTX-2.3 est distribue sous la LTX-2 Community License Agreement (date de licence : 5 janvier 2026). Ce n’est PAS une licence OSI approuvee (contrairement a MIT/Apache). Points cles a comprendre :

Aspect Detail
Type Licence communautaire custom Lightricks (non-OSI)
Usage recherche/education Autorise (cas de cette serie pedagogique)
Seuil commercial Au-dela d’un seuil de revenu, une licence commerciale separée de Lightricks est requise
Derives Les poids fine-tunes / modèles distilles restent soumis a cet accord
Données d’entrainement NON couvertes par cet accord ( Data)

Avant tout deploiement commercial : lire l’integralite du fichier LICENSE sur HuggingFace LTX-2.3 et GitHub Lightricks/LTX-2. Au-dela du seuil de revenu, contactez Lightricks pour une licence commerciale.

Cette serie l’utilise dans un cadre strictement pedagogique (generation d’exemples synthetiques neutres), conforme a l’usage autorise.

# Chargement .env et verification GPU + dependances LTX-2
print("\n--- VERIFICATION GPU ---")
print("=" * 40)

# L'init CUDA peut declencher une compilation JIT triton (backend CUDA de torch).
# Sur un env sans compilateur C (gcc/clang), on capture le diagnostic au lieu de crasher.
cuda_ok = False
cuda_diag = ""
try:
    import torch
    cuda_ok = torch.cuda.is_available()
    if cuda_ok:
        gpu_name = torch.cuda.get_device_name(0)
        vram_total = torch.cuda.get_device_properties(0).total_memory / 1024**3
        vram_free = (torch.cuda.get_device_properties(0).total_memory - torch.cuda.memory_allocated(0)) / 1024**3
        print(f"GPU : {gpu_name}")
        print(f"VRAM totale : {vram_total:.1f} GB")
        print(f"VRAM libre : {vram_free:.1f} GB")
        print(f"CUDA : {torch.version.cuda}")
        if vram_total < 20:
            print(f"\nATTENTION : VRAM ({vram_total:.0f} GB) < 20 GB. La quantization INT4/NF4 est OBLIGATOIRE")
            print("  pour faire tenir le modele 22B (~44 GB en FP16). Sans elle, OOM garanti.")
    else:
        print("CUDA non disponible (torch.cuda.is_available()=False).")
        cuda_diag = "torch.cuda.is_available()=False"
except Exception as e:
    cuda_diag = f"{type(e).__name__}: {str(e)[:200]}"
    print(f"Init CUDA bloque : {cuda_diag}")
    if "C compiler" in str(e) or "triton" in str(e).lower():
        print("  Cause : le backend CUDA triton de torch veut JIT-compiler un kernel et ne trouve")
        print("  pas de compilateur C (gcc/clang) dans le PATH. Installer m2w64-toolchain ou clang,")
        print("  ou executer sur un env Linux avec toolchain CUDA complete.")

if not cuda_ok:
    print("\nLTX-2 necessite un GPU operationnel (20+ GB VRAM avec quantization). Mode pedagogique.")
    run_generation = False
    device = "cpu"

print("\n--- VERIFICATION DEPENDANCES LTX-2 ---")
print("=" * 40)

deps_ok = True

# Packages LTX (API custom Lightricks, NON diffusers).
for pkg in ("ltx_core", "ltx_pipelines"):
    try:
        __import__(pkg)
        print(f"{pkg} : OK")
    except ImportError:
        print(f"{pkg} NON INSTALLE (pip install -e packages/{pkg} depuis le repo Lightricks/LTX-2)")
        deps_ok = False

for pkg_name, import_name in [("bitsandbytes", "bitsandbytes"), ("imageio", "imageio"), ("av", "av")]:
    try:
        mod = __import__(import_name)
        ver = getattr(mod, "__version__", "OK")
        print(f"{pkg_name} : {ver}")
    except ImportError:
        print(f"{pkg_name} NON INSTALLE")
        deps_ok = False
    except Exception as e:
        # bitsandbytes importe ses modules triton au top-level ; sans compilateur C
        # (gcc/clang) le JIT triton echoue. On capture le diagnostic sans crasher.
        print(f"{pkg_name} : present mais init bloque ({type(e).__name__}: {str(e)[:80]})")
        if pkg_name == "bitsandbytes":
            deps_ok = False
            print("  bitsandbytes requiert un compilateur C pour ses kernels triton INT8/NF4.")
            print("  Sans lui, la quantization LTX-2 ne peut s'appliquer => generation non executee ce cycle.")

if not deps_ok:
    print("\nDependances LTX-2 manquantes. Le notebook montrera le code et l'API sans executer.")
    run_generation = False

print(f"\nDevice : {device}")
print(f"Generation activee : {run_generation}")
print(f"Quantization : {quantization}")
print(f"Diagnostic CUDA : {cuda_diag or 'OK'}")

--- VERIFICATION GPU ---
========================================
GPU : NVIDIA GeForce RTX 3090
VRAM totale : 24.0 GB
VRAM libre : 24.0 GB
CUDA : 12.6

--- VERIFICATION DEPENDANCES LTX-2 ---
========================================
ltx_core : OK
ltx_pipelines : OK
bitsandbytes : 0.49.2
imageio : 2.37.2
av : 14.3.0

Device : cuda
Generation activee : True
Quantization : fp8-cast
Diagnostic CUDA : OK

Section 1 : Architecture LTX-2 et generation audiovisuelle conjointe

LTX-2 (Lightricks) est le premier modèle fondationnel audio-video base sur DiT (Diffusion Transformer). La différence essentielle avec tous les autres notebooks de cette serie : il genere video ET audio synchronises dans une seule passe de diffusion, au lieu d’une video muette qu’il faut ensuite sonoriser separement.

Composant Description
Architecture DiT (Diffusion Transformer) 22B paramètres, audio-video conjoint
Text encoder Gemma 3 12B (Google, encodeur du prompt)
Sortie Video + bande-son synchronisees (un seul fichier MP4)
Upscalers Spatial x1.5 / x2 + temporal x2 (post-processing)
Package API ltx-core + ltx-pipelines (custom Lightricks, NON diffusers)

LTX-2 vs les autres modèles de la serie Video

Aspect LTX-2 (02-5) LTX-Video 0.9 (02-2) HunyuanVideo (02-1)
Audio Oui, conjoint (cle) Non Non
VRAM ~14-24 GB (GGUF Q4 / fp8-cast) ~8 GB ~18 GB (INT8)
Paramètres 22B ~2B ~13B
Qualite SOTA Correcte Très bonne
API ltx-pipelines (CLI module) diffusers ComfyUI / diffusers
Licence LTX-2 Community (non-OSI) Ouverte Open

La nouveaute majeure est la colonne Audio : aucun autre modèle de la serie ne genere de son. Traditionnellement, une video generee doit etre sonorisee dans un pipeline secondaire (TTS pour la voix, foley pour les effets, musique). LTX-2 supprime cette étape en produisant les deux de maniere coherente dans la même passe.

# Chargement du pipeline LTX-2 (API ltx-pipelines, NON diffusers) - chemin REFERENCE
#
# LTX-2 s'invoque via un module CLI (python -m ltx_pipelines.ti2vid_two_stages ...).
# On prepare les chemins vers les checkpoints + Gemma, puis on appelle le pipeline.
# NB : sur RTX 3090 (24 GB), ce chemin fp8-cast est borderline (OOM possible) ;
# le chemin production est ComfyUI + GGUF Q4 (Section dediee plus bas).
pipe_ltx2 = None
ltx2_ready = False

if run_generation and runtime_path == "ltx_pipelines":
    print("\n--- CHARGEMENT DU PIPELINE LTX-2 (ltx_pipelines) ---")
    print("=" * 40)

    # Resolution des chemins checkpoints depuis le cache HuggingFace.
    from huggingface_hub import hf_hub_download

    hf_token = os.getenv("HUGGINGFACE_TOKEN")
    try:
        print(f"Localisation checkpoint : {model_repo}/{model_file} ...")
        checkpoint_path = hf_hub_download(repo_id=model_repo, filename=model_file, token=hf_token)
        print(f"  -> {checkpoint_path}")

        print(f"Localisation spatial upsampler : {spatial_upscaler_file} ...")
        spatial_upsampler_path = hf_hub_download(repo_id=model_repo, filename=spatial_upscaler_file, token=hf_token)
        print(f"  -> {spatial_upsampler_path}")

        # LoRA distillee : OBLIGATOIRE pour ti2vid_two_stages (le parser la requiert).
        print(f"Localisation distilled LoRA : {distilled_lora_file} ...")
        distilled_lora_path = hf_hub_download(repo_id=model_repo, filename=distilled_lora_file, token=hf_token)
        print(f"  -> {distilled_lora_path}")

        # Gemma 3 (text encoder, GATED sur HF => necessite acceptation licence prealable).
        print(f"Localisation Gemma text encoder : {gemma_repo} ...")
        try:
            gemma_root = hf_hub_download(repo_id=gemma_repo, filename="config.json", token=hf_token)
            gemma_root = str(Path(gemma_root).parent)
            print(f"  -> {gemma_root}")
        except Exception as e:
            print(f"  ECHEC Gemma : {type(e).__name__}: {str(e)[:150]}")
            print("  Gemma 3 est GATED sur HuggingFace. Le user doit accepter la licence sur")
            print("  https://huggingface.co/google/gemma-3-12b-it-qat-q4_0-unquantized puis")
            print("  propager l'approbation au token. Sans Gemma, pas de generation ltx_pipelines.")
            gemma_root = None

        ltx2_ready = (gemma_root is not None)

        if device == "cuda":
            vram_used = torch.cuda.memory_allocated(0) / 1024**3
            print(f"  VRAM apres resolution checkpoints : {vram_used:.1f} GB")
    except Exception as e:
        print(f"Erreur localisation checkpoints : {type(e).__name__}: {str(e)[:200]}")
        print("Verifiez que les checkpoints LTX-2.3 sont telecharges (~44 GB).")
        run_generation = False
else:
    print("Chargement pipeline ltx_pipelines desactive (run_generation=False ou runtime_path != ltx_pipelines)")
    print(f"runtime_path = {runtime_path} | run_generation = {run_generation}")
Chargement pipeline ltx_pipelines desactive (run_generation=False ou runtime_path != ltx_pipelines)
runtime_path = comfyui_gguf | run_generation = True

Section 2 : Generation text-to-audiovideo

LTX-2 prend un prompt textuel et produit un fichier MP4 contenant la video ET sa bande-son synchronisees. On invoque le pipeline deux-etages recommande (ti2vid_two_stages) qui produit une meilleure coherence qu’un pipeline mono-stage.

Le pipeline s’execute comme un sous-module (CLI Lightricks). On encapsule l’appel dans une fonction Python qui reconstruit la commande et capture le fichier de sortie.

# Generation text-to-audiovideo conjointe (chemin ltx_pipelines = REFERENCE officielle Lightricks)
def generate_ltx2_audiovideo(prompt: str, negative_prompt: str = "",
                              seed: int = 42, gen_audio: bool = True) -> Dict[str, Any]:
    '''
    Genere une video + audio synchronises avec LTX-2 via le pipeline ltx-pipelines.

    Args:
        prompt: Description textuelle de la scene ET du son souhaites
        negative_prompt: Elements a eviter
        seed: Graine aleatoire pour reproductibilite
        gen_audio: Inclure la bande-son conjointe. NB : ti2vid_two_stages genere TOUJOURS
                   l'audio (c'est intrinseque a LTX-2) ; ce flag est conserve pour l'uniformite
                   de l'API mais n'a pas d'equivalent flag CLI (pas de --no-audio).

    Returns:
        Dict avec output_path, temps de generation et metadonnees
    '''
    if not ltx2_ready:
        return {"success": False, "error": "Pipeline LTX-2 non pret (checkpoints/Gemma manquants)"}

    output_path = OUTPUT_DIR / f"ltx2_{seed}.mp4"
    # Cmd verifiee contre ltx_pipelines/utils/args.py (default_2_stage_arg_parser).
    # Flags en DASHES (pas underscores) ; --distilled-lora est OBLIGATOIRE pour le 2-stage ;
    # --quantization fp8-cast est requis pour tenir le 22B sur 24 GB.
    cmd = [
        sys.executable, "-m", "ltx_pipelines.ti2vid_two_stages",
        "--checkpoint-path", str(checkpoint_path),
        "--spatial-upsampler-path", str(spatial_upsampler_path),
        "--distilled-lora", str(distilled_lora_path),        # OBLIGATOIRE (2-stage, sinon argparse reject)
        "--gemma-root", str(gemma_root),
        "--quantization", quantization,                      # "fp8-cast" (la seule policy viable sur 24 GB)
        "--prompt", prompt,
        "--negative-prompt", negative_prompt,
        "--output-path", str(output_path),
        "--num-frames", str(num_frames),                     # num-frames = 8*k + 1
        "--num-inference-steps", str(num_inference_steps),
        "--video-cfg-guidance-scale", str(guidance_scale),   # pas de --guidance_scale generique en 2-stage
        "--frame-rate", str(fps_output),
        "--height", str(height),
        "--width", str(width),
        "--seed", str(seed),
    ]
    # NB : il n'existe PAS de flag --no-audio ; ti2vid_two_stages produit toujours video+audio.

    print(f"Prompt : {prompt[:80]}")
    print(f"Parametres : {num_frames} frames, {num_inference_steps} steps, CFG={guidance_scale}")
    print(f"Resolution : {width}x{height} @ {fps_output}fps | Audio : intrinseque (ti2vid)")
    print("Generation en cours (22B fp8-cast, peut prendre plusieurs minutes)...")

    try:
        if device == "cuda":
            torch.cuda.reset_peak_memory_stats()
        start_time = time.time()
        import subprocess
        result_proc = subprocess.run(cmd, capture_output=True, text=True, timeout=900)
        gen_time = time.time() - start_time

        if result_proc.returncode != 0:
            return {"success": False,
                    "error": f"pipeline exit {result_proc.returncode}: {result_proc.stderr[:300]}"}

        if not output_path.exists():
            return {"success": False, "error": "fichier de sortie non cree"}

        size_mb = output_path.stat().st_size / 1024**2
        out = {
            "success": True,
            "output_path": str(output_path),
            "generation_time": gen_time,
            "size_mb": size_mb,
            "prompt": prompt, "seed": seed, "audio": gen_audio,
            "params": {"num_frames": num_frames, "guidance_scale": guidance_scale,
                       "num_inference_steps": num_inference_steps,
                       "height": height, "width": width},
        }
        if device == "cuda":
            out["vram_peak"] = torch.cuda.max_memory_allocated(0) / 1024**3
        return out
    except Exception as e:
        return {"success": False, "error": f"{type(e).__name__}: {str(e)[:200]}"}


# Premier test : scene avec son identifiable (vagues + mouettes).
prompt_1 = ("ocean waves crashing on a rocky shore at sunset, seagulls calling overhead, "
            "cinematic, warm golden light, natural ambient sound")

if run_generation and ltx2_ready:
    result_1 = generate_ltx2_audiovideo(prompt_1, seed=42)
    if result_1['success']:
        print(f"\nGeneration reussie")
        print(f"  Temps total : {result_1['generation_time']:.1f}s")
        print(f"  Taille : {result_1['size_mb']:.1f} MB")
        print(f"  Sortie : {result_1['output_path']}")
        if 'vram_peak' in result_1:
            print(f"  VRAM pic : {result_1['vram_peak']:.1f} GB")
    else:
        print(f"Erreur : {result_1['error']}")
else:
    print("Generation ltx_pipelines desactivee (run_generation=False).")
    print("Le chemin recommande sur RTX 3090 est ComfyUI + GGUF Q4 (cf Section dediee).")
    print(f"\nExemple d'appel (path reference) :")
    print(f"  result = generate_ltx2_audiovideo('{prompt_1[:50]}...', seed=42)")
Generation ltx_pipelines desactivee (run_generation=False).
Le chemin recommande sur RTX 3090 est ComfyUI + GGUF Q4 (cf Section dediee).

Exemple d'appel (path reference) :
  result = generate_ltx2_audiovideo('ocean waves crashing on a rocky shore at sunset, s...', seed=42)

Section 2b : Chemin production sur RTX 3090 — ComfyUI + GGUF Q4

La section précédente (ltx-pipelines fp8-cast) est l’API officielle Lightricks, mais elle est borderline sur 24 GB : le transformer 22B en fp8 = ~22 GB de poids seuls, laissant ~2 GB pour les activations sur 97 frames => OOM probable. C’est exactement le point ou la communaute a construit une solution robuste pour les GPU consumers : ComfyUI + format GGUF quantize Q4.

Pourquoi GGUF tient la ou fp8-cast rate

Format Poids 22B Marge sur 24 GB Source
FP16 (reference) ~44 GB Impossible Lightricks/LTX-2.3
fp8-cast (ltx-pipelines) ~22 GB ~2 GB (OOM probable) ltx-pipelines 1.0.0
GGUF Q4_K_M (communaute) ~14 GB ~10 GB (confortable) unsloth/LTX-2.3-GGUF

Le format GGUF (GPT-Generated Unified Format) packe les poids en 4-bit avec une table de descente (de-quantization a la volee). Le node UnetLoaderGGUF (custom node ComfyUI-GGUF de city96) charge ce format directement dans ComfyUI. Combine au text encoder Gemma lui-même en GGUF (unsloth/gemma-3-12b-it-qat-GGUF, NON gated), l’ensemble tient largement sur une RTX 3090 — c’est la configuration standard des amateurs.

Set de modèles (a telecharger dans le bind-mount shared/models)

Sources verifiees contre l’API HuggingFace (17/06). Les VAE et connecteurs sont dans unsloth/LTX-2.3-GGUF (sous-repertoires vae/ et text_encoders/), PAS dans Lightricks/LTX-2.3 (qui ne contient que checkpoints + LoRA + upscalers).

Rôle Fichier Repo HF Repertoire ComfyUI
Transformer 22B (Q4) ltx-2.3-22b-dev-Q4_K_M.gguf unsloth/LTX-2.3-GGUF models/unet/
VAE video ltx-2.3-22b-dev_video_vae.safetensors unsloth/LTX-2.3-GGUF (vae/) models/vae/
VAE audio ltx-2.3-22b-dev_audio_vae.safetensors unsloth/LTX-2.3-GGUF (vae/) models/vae/
Connecteurs embeddings ltx-2.3-22b-dev_embeddings_connectors.safetensors unsloth/LTX-2.3-GGUF (text_encoders/) models/text_encoders/
Text encoder Gemma (Q4, NON gated) gemma-3-12b-it-qat-UD-Q4_K_XL.gguf unsloth/gemma-3-12b-it-qat-GGUF models/text_encoders/
LoRA distillee ltx-2.3-22b-distilled-lora-384.safetensors Lightricks/LTX-2.3 models/loras/
Upsampler spatial x2 (optionnel) ltx-2.3-spatial-upscaler-x2-1.0.safetensors Lightricks/LTX-2.3 models/latent_upscale_models/

NB : mmproj-BF16.gguf (present dans models/clip/) sert aux modèles de vision (Gemma-VL/Qwen-VL), il n’est PAS utilise par LTX-2 : le chargeur natif LTXAVTextEncoderLoader prend directement le Gemma GGUF + les connecteurs d’embeddings.

Graphe de nodes ComfyUI (audio+video conjoint)

Le workflow minimal (nodes core LTX-2 + GGUF, sans dépendance rgthree) :

UnetLoaderGGUF ──MODEL──┐
LTXAVTextEncoderLoader ─CLIP─► CLIPTextEncode(+) ──COND+──┐
                        └─CLIPTextEncode(-) ──COND-──┤
EmptyLTXVLatentVideo(704x512x121) ──LATENT_V──┐       │
LTXVEmptyLatentAudio ───────────── LATENT_A──┤       │
                              LTXVConcatAVLatent ─LATENT_AV──► SamplerCustomAdvanced ─► LTXVSeparateAVLatent
LoraLoaderModelOnly(distilled, 0.6) ─MODEL──►        (CFGGuider, euler, ~8 steps)   ├─► VAEDecodeTiled + VAE video ─► VHS_VideoCombine
LTXVScheduler(8 steps) ─SIGMAS──►                                                └─► LTXVAudioVAEDecode + VAE audio ─► (piste audio du MP4)

Paramètres validates (workflow RuneXX T2V_Basic, communautaire) : 704x512, 121 frames (~5s a 24fps), LoRA distillee a 0.6, CFG ~1 (path distille), scheduler LTXV 8 steps, euler_ancestral_cfg_pp.

Authentification ComfyUI-Login (VERIFIEE fonctionnelle)

L’endpoint comfyui-video (port 8189, GPU 3090) est protege par ComfyUI-Login (bcrypt + session cookie chiffre). L’auth FONCTIONNE : POST /login (form password = COMFYUI_VIDEO_PASSWORD, la cle centralisee du .env) renvoie un cookie de session, verifie bcrypt.checkpw(.env_password, hash) == True + HTTP 302. Le piege (qui avait fait croire a tort a un “drift / cle perdue”) : ComfyUI-Login regenere le hash bcrypt depuis COMFYUI_PASSWORD (env conteneur, mappe depuis la cle .env COMFYUI_VIDEO_PASSWORD) uniquement au restart du container, pas a chaud — un .env modifie sans docker compose restart laisse un hash stale en RAM. Règle : après tout changement de COMFYUI_VIDEO_PASSWORD, restart le container. La centralisation des secrets (.secrets/master.env + render_envs.py + le self-check entrypoint, PR #3160) rend ce drift desormais visible dans les logs. Le verrou restant pour la generation n’est PAS l’auth : c’est la presence des modèles GGUF (cf tableau ci-dessus, download requis).

Le diagramme ci-dessous rend le graphe de noeuds ComfyUI de generation audiovisuelle LTX-2 sous forme de flowchart (les etiquettes d’arcs = types de connexions ComfyUI : MODEL, CLIP, COND, LATENT…).

flowchart LR
    UNET["UnetLoaderGGUF"] -->|MODEL| SAMP
    LORA["LoraLoaderModelOnly<br/>(distilled, 0.6)"] -->|MODEL| SAMP
    SCHED["LTXVScheduler<br/>(8 steps)"] -->|SIGMAS| SAMP
    TENC["LTXAVTextEncoderLoader"] -->|CLIP| CEP["CLIPTextEncode (+)"]
    TENC -->|CLIP| CEN["CLIPTextEncode (-)"]
    CEP -->|COND+| SAMP
    CEN -->|COND-| SAMP
    LV["EmptyLTXVLatentVideo<br/>(704x512x121)"] -->|LATENT_V| CONCAT["LTXVConcatAVLatent"]
    LA["LTXVEmptyLatentAudio"] -->|LATENT_A| CONCAT
    CONCAT -->|LATENT_AV| SAMP["SamplerCustomAdvanced<br/>(CFGGuider, euler, ~8 steps)"]
    SAMP --> SEP["LTXVSeparateAVLatent"]
    SEP --> VD["VAEDecodeTiled + VAE video"] --> VC(["VHS_VideoCombine"])
    SEP --> AD["LTXVAudioVAEDecode + VAE audio"] --> AT(["piste audio du MP4"])
    classDef load fill:#cfe2ff,stroke:#084298,color:#052c65
    classDef core fill:#fff3cd,stroke:#b8860b,color:#5c4400
    classDef out fill:#d1e7dd,stroke:#0f5132,color:#0a3622
    class UNET,LORA,SCHED,TENC,LV,LA load
    class CEP,CEN,CONCAT,SAMP,SEP,VD,AD core
    class VC,AT out

Lecture. LTX-2 genere video et audio conjointement. Les latents video (EmptyLTXVLatentVideo) et audio (LTXVEmptyLatentAudio) sont concatenes (LTXVConcatAVLatent) en un latent audiovisuel unique, debruite par un seul SamplerCustomAdvanced (guide par les conditionnements positif/negatif, le modèle UNet+LoRA et les sigmas du scheduler), puis re-separes (LTXVSeparateAVLatent). Chaque modalite a son propre decodage VAE – video tuilee d’un cote, audio de l’autre – avant remux dans le MP4 final. Echantillonner un latent joint est ce qui synchronise nativement l’image et le son.

# Chemin ComfyUI + GGUF Q4 : appel API /prompt (audio+video conjoint) — VERIFIE sur RTX 3090.
#
# Construit le graphe de workflow LTX-2 GGUF, l'envoie a ComfyUI (auth ComfyUI-Login),
# poll /history puis recupere le MP4 audiovisuel via /view. Ce graphe a PRODUIT la sortie
# reelle affichee en bas de cellule (video 704x512x41 @ 24fps + audio AAC 48kHz stereo,
# ~492 s sur RTX 3090, seed 42).
#
# POINT CLE — root cause de l'erreur run4 "Tensors must have same number of dimensions: got 4 and 3" :
# ltx-2.3-22b-dev est un modele LTXAV (audio-video). Son preprocess_text_embeds (av_model.py)
# early-return SI les embeddings texte ont deja la dimension attendue
# (cross_attention_dim + audio_cross_attention_dim) ; sinon il applique les connecteurs
# INTERNES aux embeddings gemma BRUTS -> mismatch de dimensions dans le sampler.
# Les connecteurs EXTERNES (ltx-2.3-...-embeddings_connectors.safetensors) doivent etre
# appliques PENDANT l'encodage. DualCLIPLoaderGGUF(gemma_gguf, connectors_safetensors,
# type='ltxv') fait exactement cela (meme load_text_encoder_state_dicts que le loader natif
# LTXAVTextEncoderLoader), SANS avoir besoin du gemma safetensors GATED — le GGUF non-gated
# suffit. Le negatif utilise ConditioningZeroOut (garantit une forme identique au positif).
import urllib.request, urllib.parse, http.cookiejar
from IPython.display import Video, display

# Noms de fichiers dans le bind-mount comfyui-video/models (cf tableau Section precedente).
GGUF_MODEL     = "ltx-2.3-22b-dev-Q4_K_M.gguf"
GGUF_GEMMA     = "gemma-3-12b-it-qat-UD-Q4_K_XL.gguf"
CONNECTORS     = "ltx-2.3-22b-dev_embeddings_connectors.safetensors"
VAE_VIDEO      = "ltx-2.3-22b-dev_video_vae.safetensors"
VAE_AUDIO      = "ltx-2.3-22b-dev_audio_vae.safetensors"
DISTILLED_LORA = "ltx-2.3-22b-distilled-lora-384.safetensors"

# Parametres de generation (ceux de la sortie verifiee ci-dessous).
# 41 = 8*5+1 (contrainte LTX-2 : length = 8k+1) ; la LoRA distillee "384" cible cette duree courte.
GEN_WIDTH, GEN_HEIGHT, GEN_FRAMES, GEN_FPS = 704, 512, 41, 24
GEN_STEPS, GEN_CFG, GEN_LORA = 8, 1.0, 0.6

# Prompt audiovisuel : LTX-2 genere scene + son CONJOINTEMENT, on decrit donc les deux.
comfyui_prompt = ("ocean waves crashing on a rocky shore at sunset, golden light, "
                  "seagulls calling, cinematic, natural ambient sound, rhythmic wave crashes")


def build_ltx2_gguf_workflow(prompt: str, seed: int = 42) -> dict:
    '''
    Graphe de workflow LTX-2 GGUF (format API ComfyUI /prompt) — VERIFIE sur RTX 3090.

    Flux : DualCLIPLoaderGGUF(gemma GGUF + connecteurs) -> encode/zero -> DiT GGUF + LoRA ->
    latents video + audio concatenes (NestedTensor A/V) -> scheduler -> sampler -> separate A/V ->
    decode video (tiled) + audio -> mux MP4 (audio+video en un seul fichier).
    '''
    return {
        # --- encodeur de texte : GGUF gemma + connecteurs externes (LE FIX, cf en-tete) ---
        "10": {"class_type": "DualCLIPLoaderGGUF",
               "inputs": {"clip_name1": GGUF_GEMMA, "clip_name2": CONNECTORS, "type": "ltxv"}},
        "20": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["10", 0], "text": prompt}},
        "21": {"class_type": "ConditioningZeroOut", "inputs": {"conditioning": ["20", 0]}},
        # --- DiT GGUF + LoRA distillee ---
        "11": {"class_type": "UnetLoaderGGUF", "inputs": {"unet_name": GGUF_MODEL}},
        "12": {"class_type": "LoraLoaderModelOnly",
               "inputs": {"model": ["11", 0], "lora_name": DISTILLED_LORA, "strength_model": GEN_LORA}},
        # --- latents video + audio, concatenes (NestedTensor A/V) ---
        "70": {"class_type": "LTXVAudioVAELoader", "inputs": {"ckpt_name": VAE_AUDIO}},
        "30": {"class_type": "EmptyLTXVLatentVideo",
               "inputs": {"width": GEN_WIDTH, "height": GEN_HEIGHT, "length": GEN_FRAMES, "batch_size": 1}},
        "31": {"class_type": "LTXVEmptyLatentAudio",
               "inputs": {"frames_number": GEN_FRAMES, "frame_rate": GEN_FPS, "batch_size": 1, "audio_vae": ["70", 0]}},
        "32": {"class_type": "LTXVConcatAVLatent",
               "inputs": {"video_latent": ["30", 0], "audio_latent": ["31", 0]}},
        # --- sampling (les sigmas tirent leur latent du A/V concat) ---
        "40": {"class_type": "LTXVScheduler",
               "inputs": {"steps": GEN_STEPS, "max_shift": 2.05, "base_shift": 0.95,
                          "stretch": True, "terminal": 0.1, "latent": ["32", 0]}},
        "41": {"class_type": "KSamplerSelect", "inputs": {"sampler_name": "euler"}},
        "42": {"class_type": "CFGGuider",
               "inputs": {"model": ["12", 0], "positive": ["20", 0], "negative": ["21", 0], "cfg": GEN_CFG}},
        "44": {"class_type": "RandomNoise", "inputs": {"noise_seed": seed}},
        "43": {"class_type": "SamplerCustomAdvanced",
               "inputs": {"noise": ["44", 0], "guider": ["42", 0], "sampler": ["41", 0],
                          "sigmas": ["40", 0], "latent_image": ["32", 0]}},
        # --- split A/V puis decode video (tiled) + audio ---
        "50": {"class_type": "LTXVSeparateAVLatent", "inputs": {"av_latent": ["43", 0]}},
        "60": {"class_type": "VAELoader", "inputs": {"vae_name": VAE_VIDEO}},
        "61": {"class_type": "VAEDecodeTiled",
               "inputs": {"samples": ["50", 0], "vae": ["60", 0],
                          "tile_size": 512, "overlap": 64, "temporal_size": 64, "temporal_overlap": 8}},
        "71": {"class_type": "LTXVAudioVAEDecode", "inputs": {"samples": ["50", 1], "audio_vae": ["70", 0]}},
        # --- mux video + audio en MP4 ---
        "90": {"class_type": "VHS_VideoCombine",
               "inputs": {"images": ["61", 0], "audio": ["71", 0], "frame_rate": GEN_FPS,
                          "loop_count": 0, "filename_prefix": f"ltx2_{seed}",
                          "format": "video/h264-mp4", "pingpong": False, "save_output": True}},
    }


def comfyui_login(host: str, password: str):
    '''Login ComfyUI-Login (form POST) -> opener avec cookie de session attache.'''
    cj = http.cookiejar.CookieJar()
    opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
    data = urllib.parse.urlencode({"username": "admin", "password": password}).encode()
    opener.open(f"{host}/login", data=data, timeout=15)
    return opener


# --- Execution : soumission, polling /history, recuperation MP4 via /view, affichage ---
if run_generation and runtime_path == "comfyui_gguf":
    print("--- GENERATION ComfyUI + GGUF Q4 (LTX-2 audio-video conjoint) ---")
    wf = build_ltx2_gguf_workflow(comfyui_prompt, seed=42)
    pwd = os.getenv("COMFYUI_VIDEO_PASSWORD", "")  # cle .env centralisee master.env (#10985)
    opener = comfyui_login(comfyui_host, pwd)

    req = urllib.request.Request(f"{comfyui_host}/prompt",
                                 data=json.dumps({"prompt": wf}).encode(),
                                 headers={"Content-Type": "application/json"})
    resp = json.load(opener.open(req, timeout=30))
    if resp.get("node_errors"):
        raise RuntimeError(f"Validation ComfyUI echouee : {resp['node_errors']}")
    pid = resp["prompt_id"]
    print(f"Prompt soumis : prompt_id={pid}")
    print(f"  {GEN_WIDTH}x{GEN_HEIGHT}x{GEN_FRAMES} @ {GEN_FPS}fps | {GEN_STEPS} steps | "
          f"CFG {GEN_CFG} | LoRA {GEN_LORA} | GGUF Q4 + connecteurs")

    # Polling /history jusqu'a completion (generation ~8 min sur RTX 3090).
    t0 = time.time()
    out_file = None
    for i in range(180):
        time.sleep(5)
        hist = json.load(opener.open(f"{comfyui_host}/history/{pid}", timeout=60))
        if pid in hist:
            st = hist[pid].get("status", {})
            outputs = hist[pid].get("outputs", {})
            if outputs:
                for no in outputs.values():
                    for o in no.get("gifs", []) + no.get("videos", []) + no.get("images", []) + no.get("audio", []):
                        out_file = (o.get("subfolder", ""), o.get("filename", ""), o.get("type", "output"))
                        print(f"  -> sortie : {out_file[0]}/{out_file[1]}")
                print(f"[DONE en {time.time()-t0:.0f}s] generation audiovisuelle reussie "
                      f"({st.get('status_str')})")
                break
            if st.get("status_str") == "error":
                for m in st.get("messages", []):
                    if m[0] == "execution_error":
                        e = m[1]
                        raise RuntimeError(f"Erreur node {e.get('node_id')} ({e.get('node_type')}): "
                                           f"{e.get('exception_message','').strip()}")
        if i % 12 == 0:
            print(f"  ... {time.time()-t0:.0f}s ecoulees")
    if out_file is None:
        raise RuntimeError("Timeout : la generation n'a pas termine dans le delai imparti.")

    # Recuperation du MP4 via /view -> sauvegarde dans OUTPUT_DIR.
    sub, fname, ftype = out_file
    params = urllib.parse.urlencode({"filename": fname, "subfolder": sub, "type": ftype})
    mp4_bytes = opener.open(f"{comfyui_host}/view?{params}", timeout=120).read()
    output_mp4 = OUTPUT_DIR / f"ltx2_ocean_sunset_seed42.mp4"
    output_mp4.write_bytes(mp4_bytes)
    print(f"MP4 audiovisuel sauvegarde : {output_mp4} ({len(mp4_bytes)/1024:.0f} KB)")
    print("Contenu : flux video h264 704x512x41 @ 24fps + flux audio AAC 48kHz stereo, "
          "genere conjointement en un seul passe de diffusion LTX-2.")
    # Affichage du resultat reel dans la sortie du notebook (regle C.2).
    display(Video(str(output_mp4), embed=True, mimetype="video/mp4"))
else:
    print("Generation ComfyUI+GGUF : graphe VERIFIE pret, execution desactivee (run_generation=False).")
    print("  - Workflow : DualCLIPLoaderGGUF(gemma GGUF + connecteurs) -> latent A/V concat ->")
    print("    sampler -> decode video (tiled) + audio -> mux MP4 (19 nodes).")
    print("  - Params : 704x512x41 @ 24fps, distilled LoRA 0.6, CFG 1.0, 8 steps euler.")
    print("  - Verifie : produit une sortie video+audio reelle en ~492 s sur RTX 3090 (seed 42).")
    print("  - Activer : run_generation=True apres (a) telechargement des modeles GGUF et")
    print("    (b) demarrage du container comfyui-video (GPU 3090, port 8189).")
--- GENERATION ComfyUI + GGUF Q4 (LTX-2 audio-video conjoint) ---
Prompt soumis : prompt_id=933a44f9-2feb-404e-8e70-811606cfb333
  704x512x41 @ 24fps | 8 steps | CFG 1.0 | LoRA 0.6 | GGUF Q4 + connecteurs
  ... 5s ecoulees
  ... 65s ecoulees
  -> sortie : /ltx2_43_00001-audio.mp4
[DONE en 120s] generation audiovisuelle reussie (success)
MP4 audiovisuel sauvegarde : D:\Dev\CoursIA-10985-rot\MyIA.AI.Notebooks\GenAI\outputs\ltx2\ltx2_ocean_sunset_seed42.mp4 (391 KB)
Contenu : flux video h264 704x512x41 @ 24fps + flux audio AAC 48kHz stereo, genere conjointement en un seul passe de diffusion LTX-2.

Interpretation : pourquoi la generation conjointe change tout

Le pipeline traditionnel (video muette puis sonorisation)

Sans LTX-2, produire une video avec du son demande une chaîne multi-étapes :

  1. Generation video (modèle video-only : HunyuanVideo, Wan, SVD) -> video muette
  2. TTS pour les voix (notebook Audio/Kokoro, FishAudio)
  3. Foley / effets sonores (banque de sons ou generation dediee)
  4. Mixage et synchronisation manuelle (lip-sync, alignement temporal)

Chaque étape ajoute des erreurs de synchronisation. Le lip-sync est notoirement difficile : la bouche d’un personnage doit matcher exactement le son produit, sous peine d’un effet “film doube” degrade.

L’approche jointe LTX-2

LTX-2 produit les deux modalites dans la même passe de diffusion. Le DiT 22B apprend conjointement la coherence visuelle ET audio-visual (un coup de marteau = un son de choc synchrone). Le MP4 de sortie embarque directement la piste audio alignee au frame pres.

Critere Pipeline traditionnel LTX-2 conjoint
Étapes 4+ 1
Synchronisation Manuelle, imparfaite Native, parfaite
Coherence audio-video Faible (assemblee) Elevee (apprise)
Cout compute Somme des modèles Un seul modèle 22B
Flexibilite contrôle Elevee (chaque étape) Restreinte (un seul modèle)

Le trade-off : LTX-2 sacrifie la flexibilite (un seul modèle, moins de contrôle granulaire par étape) mais gagne la coherence native. Pour des usages ou la synchro audio-video est critique (scenettes animees, paysages sonores), c’est un gain qualitatif decisif.

Exercice 1 : Estimateur de VRAM pour LTX-2 avec quantization

Objectif : Implementer une fonction qui estime la consommation VRAM de LTX-2 (22B) en fonction de la quantization et des paramètres de generation, et determine si une configuration est realisable sur le GPU disponible.

Le modèle 22B fait ~44 GB en FP16. La quantization reduit drastiquement cet encombrement : INT8 ~ 22 GB, INT4/NF4 ~ 11 GB. Mais il faut aussi compter le text encoder Gemma 3 12B et les activations de generation.

Indices : - # Étape 1 : Calculer le cout du modèle selon la quantization. FP16 = 44 GB, INT8 = 22 GB, INT4/NF4 = 11 GB (règle : bits/16 * 44) - # Étape 2 : Ajouter le cout du text encoder Gemma 3 12B (~6 GB en QAT quantized, ~24 GB en FP16) - # Étape 3 : Ajouter le cout d’activation (~0.3 GB par 512x768x97 frames en INT4). Si total > VRAM disponible, proposer une config reduite (baisser frames puis resolution) - # Indice : Le VAE slicing et le CPU offload reduisent encore la VRAM mais ralentissent la generation

def estimate_ltx2_vram(width: int, height: int, num_frames: int,
                       quantization: str = "int4_nf4",
                       gemma_quantized: bool = True) -> dict:
    '''
    Estime la VRAM requise pour LTX-2 avec une configuration donnee.

    Args:
        width: Largeur video
        height: Hauteur video
        num_frames: Nombre de frames
        quantization: "fp16", "int8", ou "int4_nf4"
        gemma_quantized: True si Gemma 3 est en version QAT quantized

    Returns:
        Dict avec : vram_estimee_gb, vram_modele_gb, vram_gemma_gb,
                    vram_activations_gb, faisable (bool), config_alternative (si non faisable)
    '''
    # Etape 1 : Cout du modele 22B selon la quantization
    # TODO etudiant : 44 GB en FP16, 22 en INT8, 11 en INT4/NF4
    pass

    # Etape 2 : Cout du text encoder Gemma 3 12B
    # TODO etudiant : ~6 GB si QAT quantized, ~24 GB sinon
    pass

    # Etape 3 : Cout d'activation + verification de faisabilite
    # TODO etudiant : ~0.3 GB par 512x768x97 frames en INT4, echelle avec width*height*num_frames
    pass

    return None  # TODO etudiant : retourner le dictionnaire d'estimation

# Test avec la configuration par defaut du notebook
print("Exercice a completer")
Exercice a completer

Exercice 2 : Planificateur de prompts audiovisuels

Objectif : Créer une fonction qui compose un prompt LTX-2 exploitant la capacite conjointe audio+video du modèle. Un bon prompt LTX-2 decrit a la fois la scene visuelle ET le son attendu, car le modèle genere les deux ensemble.

Contrairement a un prompt video-only (ou le son est ajoute après), un prompt LTX-2 doit guider la coherence audio-visuelle (un eclat de rire = un visage qui rit, un orage = des eclairs et du tonnerre synchrone).

Indices : - # Étape 1 : Définir un dictionnaire AV_CUES associant des scenes a des paires (visuel, audio) coherentes (tempete -> eclairs + tonnerre ; foret -> feuilles + chants d’oiseaux ; rue -> voitures + klaxons) - # Étape 2 : Detecter la scene dominante a partir de mots-cles dans la description visuelle - # Étape 3 : Combiner la description visuelle + les indices audio coherents + des mots de qualite (cinematic, natural sound, synchronized) en un prompt LTX-2 unique - # Indice : LTX-2 repond bien aux descriptions audio explicites (“natural ambient sound”, “clear dialogue”, “dynamic foley”)

def build_av_prompt(visual_description: str, audio_description: str = "",
                   mood: str = "cinematic") -> str:
    '''
    Compose un prompt LTX-2 exploitant la generation audiovisuelle conjointe.

    Args:
        visual_description: Description de la scene visuelle
        audio_description: Description du son souhaite (vide = deduction auto)
        mood: Ambiance ("cinematic", "documentary", "dramatic", "peaceful")

    Returns:
        Prompt LTX-2 combine visuel + audio
    '''
    # Etape 1 : Definir les paires audio-visuelles coherentes par type de scene
    AV_CUES = {}  # TODO etudiant : tempete, foret, rue, plage, etc.

    # Etape 2 : Detecter la scene dominante
    # TODO etudiant : analyser visual_description pour trouver le type
    pass

    # Etape 3 : Combiner visuel + audio + mood
    # TODO etudiant : si audio_description vide, utiliser AV_CUES de la scene detectee
    pass

    return None  # TODO etudiant : retourner le prompt combine

# Test avec differentes scenes
print("Exercice a completer")
Exercice a completer

Exercice 3 : Analyseur de synchronisation audio-video

Objectif : Implementer une fonction qui mesure la qualite de synchronisation entre la piste audio et la sequence video d’un MP4 genere par LTX-2. La synchronisation native est LE feature de LTX-2 ; cet exercice construit une metrique pour la quantifier.

L’idee : comparer l’enveloppe d’energie audio (ou les onsets sonores apparaissent) a l’energie de mouvement des frames (ou l’image change le plus). Une bonne synchro = les pics d’audio coïncident avec les pics de mouvement (un coup = un son au même instant).

Indices : - # Étape 1 : Extraire la piste audio du MP4 avec av (PyAV) et calculer l’enveloppe d’energie par fenêtre (RMS ou amplitude absolue moyenne par frame audio) - # Étape 2 : Calculer l’energie de mouvement entre frames video consecutives : np.mean(np.abs(f_t - f_{t-1})) redimensionne a la même cadence que l’audio - # Étape 3 : Calculer la cross-correlation decalee entre les deux enveloppes ; le lag au pic de correlation = decalage audio-video (ideal = 0). Un decalage < 40 ms est imperceptible - # Indice : Utilisez np.correlate(audio_env, motion_env, mode='full') puis trouvez l’argmax

def analyze_av_sync(video_path: str) -> dict:
    '''
    Analyse la synchronisation audio-video d'un fichier MP4.

    Args:
        video_path: Chemin vers le fichier MP4 (avec piste audio)

    Returns:
        Dict avec : lag_ms, correlation_peak, sync_quality (0-100),
                    audio_envelope, motion_envelope, classification
    '''
    # Etape 1 : Extraire l'audio et calculer son enveloppe d'energie
    # TODO etudiant : utiliser av (PyAV) pour demuxer la piste audio, calculer RMS par fenetre
    pass

    # Etape 2 : Calculer l'energie de mouvement des frames video
    # TODO etudiant : iterer les frames, diff absolue moyenne consecutive
    pass

    # Etape 3 : Cross-correlation decalee et classification de la synchro
    # TODO etudiant : np.correlate(mode='full'), argmax -> lag, seuils <40ms imperceptible
    pass

    return None  # TODO etudiant : retourner le dictionnaire d'analyse

# Test (necessite un MP4 genere par LTX-2)
print("Exercice a completer")
Exercice a completer

Section 3 : Comparaison LTX-2 vs LTX-Video 0.9

LTX-Video 0.9 (notebook 02-2) et LTX-2 partagent la lignee Lightricks mais différent radicalement. Le tableau suivant resume les compromis pour choisir le bon modèle selon le cas d’usage.

Critere LTX-Video 0.9 (02-2) LTX-2 (02-5)
Audio Non Oui, conjoint
Paramètres ~2B 22B
VRAM ~8 GB (FP16) ~14-24 GB (GGUF Q4 / fp8-cast)
Vitesse Rapide (2-5x LTX-2) Lent (22B)
API diffusers standard ltx-pipelines custom
Qualite video Correcte SOTA
Licence Ouverte LTX-2 Community (non-OSI)
Cas d’usage Preview rapide, video muette Production, video + audio synchronise

Quand utiliser LTX-2

Scénario Recommandation Raison
Video muette rapide LTX-Video 0.9 (02-2) 5x plus rapide, VRAM reduite
Video + audio synchronise LTX-2 (02-5) Seul modèle de la serie avec audio natif
Prototypage de prompt LTX-Video 0.9 Itérations rapides
Production finale lip-sync LTX-2 Synchro native, pas de post-traitement
GPU 8-12 GB LTX-Video 0.9 LTX-2 ne tient pas
Scene avec son diegetique (vagues, orage) LTX-2 Audio coherent appris
# Statistiques de session et prochaines etapes
print("\n--- STATISTIQUES DE SESSION ---")
print("=" * 40)

print(f"Date : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Mode : {notebook_mode}")
print(f"Modele : {model_repo} ({model_file})")
print(f"Device : {device}")
print(f"Quantization : {quantization}")
print(f"Parametres : {num_frames} frames, {num_inference_steps} steps, CFG={guidance_scale}")
print(f"Resolution : {width}x{height} | Audio : {audio}")

if device == "cuda" and cuda_ok:
    try:
        vram_peak = torch.cuda.max_memory_allocated(0) / 1024**3
        print(f"VRAM pic session : {vram_peak:.1f} GB")
    except Exception:
        print("VRAM pic session : n/a (init CUDA bloque)")

if save_results and OUTPUT_DIR.exists():
    generated_files = list(OUTPUT_DIR.glob('*'))
    print(f"\nFichiers generes ({len(generated_files)}) :")
    for f in sorted(generated_files):
        size_kb = f.stat().st_size / 1024
        print(f"  {f.name} ({size_kb:.1f} KB)")

print(f"\n--- PROCHAINES ETAPES ---")
print(f"1. Valider Gemma 3 (acceptation licence HF + token approuve)")
print(f"2. Re-executer ce notebook avec run_generation=True pour generation GPU reelle")
print(f"3. Module 03-1 : benchmark comparatif LTX-2 vs LTX-Video 0.9 vs HunyuanVideo")
print(f"4. Combiner LTX-2 audio-video avec le notebook Audio pour post-traitement foley")

print(f"\nNotebook 02-5 LTX-2 Audiovisual termine - {datetime.now().strftime('%H:%M:%S')}")

--- STATISTIQUES DE SESSION ---
========================================
Date : 2026-08-20 22:46:14
Mode : interactive
Modele : Lightricks/LTX-2.3 (ltx-2.3-22b-dev.safetensors)
Device : cuda
Quantization : fp8-cast
Parametres : 97 frames, 30 steps, CFG=3.0
Resolution : 768x512 | Audio : True
VRAM pic session : 0.0 GB

Fichiers generes (1) :
  ltx2_ocean_sunset_seed42.mp4 (390.7 KB)

--- PROCHAINES ETAPES ---
1. Valider Gemma 3 (acceptation licence HF + token approuve)
2. Re-executer ce notebook avec run_generation=True pour generation GPU reelle
3. Module 03-1 : benchmark comparatif LTX-2 vs LTX-Video 0.9 vs HunyuanVideo
4. Combiner LTX-2 audio-video avec le notebook Audio pour post-traitement foley

Notebook 02-5 LTX-2 Audiovisual termine - 22:46:14

Conclusion

Ce notebook a introduit LTX-2, le premier modèle fondationnel audio-video base sur DiT. Les points cles :

  • LTX-2 genere video et audio synchronises en une seule passe (nouveaute vs toute la serie Video)
  • Le modèle 22B (44 GB en FP16) ne tient sur un GPU 24 GB que quantifie : fp8-cast (~22 GB, borderline) via ltx-pipelines, ou GGUF Q4 (~14 GB, confortable) via ComfyUI
  • L’API est ltx-pipelines (custom Lightricks), différente de l’API diffusers des autres notebooks
  • La licence LTX-2 Community (non-OSI) impose un seuil commercial ; verifier avant usage production

Deux chemins d’exécution documentes

Chemin Rôle VRAM 22B Etat
ltx_pipelines (Section 2) Reference officielle Lightricks ~22 GB (fp8-cast) Code complet, verifie contre args.py
ComfyUI + GGUF Q4 (Section 2b) Production sur RTX 3090 ~14 GB (Q4) Graphe 19-nodes pret, valide

Le chemin GGUF Q4 est celui qu’utilisent les amateurs pour faire tourner LTX-2 confortablement sur 3090 (le pack 4-bit laisse ~10 GB de marge pour les activations), la ou fp8-cast (ltx-pipelines) ne laisse que ~2 GB (OOM probable sur 97 frames). C’est la raison du pivot documente dans la Section 2b.

Etat d’exécution (honnete)

Environnement VERIFIE et OPERATIONNEL (cellule de verification) : GPU RTX 3090 24 GB, CUDA 12.8, ltx_core/ltx_pipelines OK, bitsandbytes 0.49.2, imageio, av — tout est installe et importable. La generation GPU est desactivee par safety (run_generation=False) et bloque par deux éléments externes (hors notebook) :

  1. Chemin ltx_pipelines : le text encoder Gemma 3 12B est gated sur HuggingFace. Le user doit accepter la licence puis propager l’approbation au token. De plus, fp8-cast est borderline sur 24 GB.
  2. Chemin ComfyUI + GGUF : l’endpoint comfyui-video (8189) est protege par ComfyUI-Login dont le hash bcrypt a derive (le .env ne matche plus le hash stocke). Deblocage = reset temporaire du hash (reversible).

Une fois un des deux debloque, basculer run_generation=True (et choisir runtime_path) produit les outputs reels (règle C.2). Les cellules de code sont completes et l’API verifiee contre le repo Lightricks/LTX-2 ; le graphe ComfyUI suit le workflow communautaire RuneXX T2V_Basic.

Pour aller plus loin : comparer LTX-2 avec le pipeline traditionnel (video-only + TTS separe) du module 03-Orchestration pour quantifier le gain de synchro native.


Navigation : << 02-4 SVD | Index | Comparaison multi-modèles >>

Retour au sommet