# Parameters
BATCH_MODE = "true"LTX-2 - Generation Audiovisuelle Conjointe (Video + Audio)
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.
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 seulSamplerCustomAdvanced(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 :
- Generation video (modèle video-only : HunyuanVideo, Wan, SVD) -> video muette
- TTS pour les voix (notebook Audio/Kokoro, FishAudio)
- Foley / effets sonores (banque de sons ou generation dediee)
- 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_pipelinesOK,bitsandbytes0.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) :
- 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.
- Chemin ComfyUI + GGUF : l’endpoint
comfyui-video(8189) est protege par ComfyUI-Login dont le hash bcrypt a derive (le.envne 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 >>