AnimateDiff - Introduction a la Generation Text-to-Video

Navigation : Index | << Précédent

Module : 01-Video-Foundation
Niveau : Intermediaire
Technologies : AnimateDiff, diffusers, Stable Diffusion v1.5
Duree estimee : 45 minutes
VRAM : ~12 GB

Objectifs d’Apprentissage

Prerequis

  • GPU avec 12+ GB VRAM (RTX 3060 12GB minimum)
  • Notebooks 01-1 a 01-4 completes
  • Packages : diffusers>=0.30, transformers, torch, accelerate, imageio, imageio-ffmpeg

Principe d’AnimateDiff

AnimateDiff ajoute un motion module (attention temporelle) a un modèle Stable Diffusion existant pour generer des sequences de frames coherentes temporellement.

Composant Rôle
Base model Stable Diffusion v1.5 (generation d’images)
Motion adapter Module d’attention temporelle (coherence entre frames)
Scheduler Contrôle du processus de debruitage (DDIM, Euler, etc.)
# 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"

# Parametres modele
base_model = "stable-diffusion-v1-5/stable-diffusion-v1-5"  # Modele de base SD
motion_adapter = "guoyww/animatediff-motion-adapter-v1-5-3"  # Motion module

# Parametres generation
num_frames = 16                    # Nombre de frames a generer
guidance_scale = 7.5               # CFG scale (adherence au prompt)
num_inference_steps = 25           # Nombre d'etapes de debruitage
height = 512                       # Hauteur video
width = 512                        # Largeur video
fps_output = 8                     # FPS de la video de sortie

# Configuration
run_generation = True              # Executer la generation
save_as_gif = True                 # Sauvegarder en GIF
save_as_mp4 = True                 # Sauvegarder en MP4
save_results = True
# Parameters
BATCH_MODE = True

Les paramètres AnimateDiff sont définis. La cellule suivante initialise l’environnement Python : imports, chargement du .env et configuration du repertoire de sortie pour les videos generees.

# Import guards - verification des dependances
try:
    import openai
    OPENAI_AVAILABLE = True
except ImportError:
    OPENAI_AVAILABLE = False

try:
    import anthropic
    ANTHROPIC_AVAILABLE = True
except ImportError:
    ANTHROPIC_AVAILABLE = False

try:
    import requests
    REQUESTS_AVAILABLE = True
except ImportError:
    REQUESTS_AVAILABLE = False

try:
    import matplotlib
    MATPLOTLIB_AVAILABLE = True
except ImportError:
    MATPLOTLIB_AVAILABLE = False

try:
    import numpy
    NUMPY_AVAILABLE = True
except ImportError:
    NUMPY_AVAILABLE = False

try:
    import pandas
    PANDAS_AVAILABLE = True
except ImportError:
    PANDAS_AVAILABLE = False

try:
    from PIL import Image
    PIL_AVAILABLE = True
except ImportError:
    PIL_AVAILABLE = False

try:
    import IPython
    IPYTHON_AVAILABLE = True
except ImportError:
    IPYTHON_AVAILABLE = False

try:
    from dotenv import load_dotenv
    DOTENV_AVAILABLE = True
except ImportError:
    DOTENV_AVAILABLE = False

try:
    import torch
    TORCH_AVAILABLE = True
except ImportError:
    TORCH_AVAILABLE = False

try:
    import transformers
    TRANSFORMERS_AVAILABLE = True
except ImportError:
    TRANSFORMERS_AVAILABLE = False

try:
    import cv2
    CV2_AVAILABLE = True
except ImportError:
    CV2_AVAILABLE = False

try:
    import pydantic
    PYDANTIC_AVAILABLE = True
except ImportError:
    PYDANTIC_AVAILABLE = False

# Resume des dependances disponibles
_DEPS = {
    'openai': OPENAI_AVAILABLE,
    'anthropic': ANTHROPIC_AVAILABLE,
    'requests': REQUESTS_AVAILABLE,
    'matplotlib': MATPLOTLIB_AVAILABLE,
    'numpy': NUMPY_AVAILABLE,
    'pandas': PANDAS_AVAILABLE,
    'PIL': PIL_AVAILABLE,
    'IPython': IPYTHON_AVAILABLE,
    'dotenv': DOTENV_AVAILABLE,
    'torch': TORCH_AVAILABLE,
    'transformers': TRANSFORMERS_AVAILABLE,
    'cv2': CV2_AVAILABLE,
    'pydantic': PYDANTIC_AVAILABLE,
}
available = [k for k, v in _DEPS.items() if v]
missing = [k for k, v in _DEPS.items() if not v]
print(f"Dependances: {len(available)}/{len(_DEPS)} disponibles"
      + (f" | Manquantes: {missing}" if missing else ""))

# 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 (Pattern A - Papermill-safe)
from dotenv import load_dotenv

current_path = Path.cwd()
genai_path = None

while len(current_path.parts) > 1:
    if current_path.name == "GenAI":
        genai_path = current_path
        break
    current_path = current_path.parent

if genai_path:
    env_path = genai_path / ".env"
    if env_path.exists():
        load_dotenv(env_path)
        print(f".env charge depuis: {env_path.name}")
    GENAI_ROOT = genai_path
else:
    print("WARNING: GenAI directory not found")
    GENAI_ROOT = Path.cwd()

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' / 'animatediff'
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

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

print(f"AnimateDiff - Generation Text-to-Video")
print(f"Date : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Mode : {notebook_mode}")
print(f"Frames : {num_frames}, Steps : {num_inference_steps}, CFG : {guidance_scale}")
Dependances: 13/13 disponibles
Helpers GenAI importes
AnimateDiff - Generation Text-to-Video
Date : 2026-09-03 00:56:54
Mode : interactive
Frames : 16, Steps : 25, CFG : 7.5

L’environnement est configure. La cellule suivante verifie le GPU disponible et les dependances critiques : huggingface-hub, diffusers et torch, en signalant les incompatibilites de version qui empecharaient le chargement d’AnimateDiff.

# Verification GPU et dependances
print("\n--- VERIFICATION GPU ---")
print("=" * 40)
import torch
import pandas as pd
device = "cuda" if torch.cuda.is_available() else "cpu"
if torch.cuda.is_available():
    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 < 12:
        print(f"\nAttention : VRAM ({vram_total:.0f} GB) < 12 GB recommandes")
        print("Reduction de la resolution et du nombre de frames")
        if vram_total < 8:
            height = 256
            width = 256
            num_frames = 8
            print(f"  Resolution reduite a {width}x{height}, {num_frames} frames")
else:
    print("CUDA non disponible.")
    print("AnimateDiff necessite un GPU. Le notebook montrera le code sans executer.")
    run_generation = False

# Verification des dependances
print("\n--- VERIFICATION DEPENDANCES ---")
print("=" * 40)
# Diagnostic empirique : on teste le VRAI import AnimateDiff plutot qu'une
# heuristique de version (hf-hub >= 1.0 etait incompatible avec d'anciennes
# versions de diffusers, mais les versions recentes des deux librairies
# fonctionnent ensemble). Le verdict de compatibilite vient de l'import reel.
animatediff_available = False
try:
    import huggingface_hub
    print(f"huggingface-hub : v{huggingface_hub.__version__}")
except ImportError:
    print("huggingface-hub NON INSTALLE")

try:
    import diffusers
    print(f"diffusers : v{diffusers.__version__}")
except ImportError:
    print("diffusers NON INSTALLE")

# Test empirique : l'import reel est la source de verite (pas la version)
try:
    from diffusers import AnimateDiffPipeline, MotionAdapter, DDIMScheduler
    from diffusers.utils import export_to_gif, export_to_video
    print("AnimateDiff pipeline : disponible (import reussi)")
    animatediff_available = True
except ImportError as e:
    error_str = str(e)
    if "huggingface-hub" in error_str:
        print(f"AnimateDiff pipeline : NON DISPONIBLE (conflit de version huggingface-hub)")
        print(f"  Solution: pip install --upgrade huggingface-hub diffusers")
    else:
        print(f"AnimateDiff pipeline : NON DISPONIBLE - {error_str[:100]}")
    animatediff_available = False
    run_generation = False

print(f"\nDevice : {device}")
print(f"Generation activee : {run_generation}")

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

--- VERIFICATION DEPENDANCES ---
========================================
huggingface-hub : v1.20.1
diffusers : v0.38.0
AnimateDiff pipeline : disponible (import reussi)

Device : cuda
Generation activee : True

Section 1 : Chargement du pipeline AnimateDiff

Le pipeline AnimateDiff combine : 1. Un motion adapter (module d’attention temporelle entraine separement) 2. Un modèle Stable Diffusion de base (genere les frames individuelles) 3. Un scheduler (contrôle le processus de debruitage)

Le motion adapter s’insere dans les couches d’attention du U-Net de Stable Diffusion pour ajouter une dimension temporelle au traitement.

# Chargement du pipeline AnimateDiff
import warnings

# Règle 6 secrets-hygiene : le warning de deprecation hf_hub inclut le chemin
# local du module dans son en-tete (site-packages\...\huggingface_hub\utils
# \_validators.py:205) — chemin machine interdit dans une sortie commitée.
# Cause filtering cible par message (non-actionnable : local_dir_use_symlinks
# est ignore depuis hf_hub_download) — jamais d'edition a la main de l'output.
warnings.filterwarnings(
    "ignore",
    message=".*local_dir_use_symlinks.*",
    category=UserWarning,
)

pipe = None

if run_generation:
    print("\n--- CHARGEMENT DU PIPELINE ---")
    print("=" * 40)
    
    try:
        # 1. Charger le motion adapter
        print(f"Chargement motion adapter : {motion_adapter}")
        adapter = MotionAdapter.from_pretrained(
            motion_adapter,
            torch_dtype=torch.float16
        )
        print(f"  Motion adapter charge")
        
        # 2. Charger le pipeline complet
        print(f"Chargement base model : {base_model}")
        start_load = time.time()
        
        pipe = AnimateDiffPipeline.from_pretrained(
            base_model,
            motion_adapter=adapter,
            torch_dtype=torch.float16
        )
        
        # 3. Configurer le scheduler
        pipe.scheduler = DDIMScheduler.from_pretrained(
            base_model,
            subfolder="scheduler",
            clip_sample=False,
            timestep_spacing="linspace",
            beta_schedule="linear",
            steps_offset=1
        )
        
        # 4. Transferer sur GPU
        pipe = pipe.to(device)
        
        # 5. Optimisations memoire
        pipe.enable_vae_slicing()  # Reduit la VRAM pour le decodage VAE
        try:
            pipe.enable_model_cpu_offload()  # Offload intelligent CPU<->GPU
        except Exception:
            pass  # Pas critique si indisponible
        
        load_time = time.time() - start_load
        
        if device == "cuda":
            vram_used = torch.cuda.memory_allocated(0) / 1024**3
            print(f"  VRAM utilisee : {vram_used:.1f} GB")
        
        print(f"Pipeline charge en {load_time:.1f}s")
        print(f"  Scheduler : DDIMScheduler")
        print(f"  VAE slicing : active")
        print(f"  Resolution : {width}x{height}")
        
    except Exception as e:
        print(f"Erreur chargement pipeline : {type(e).__name__}: {str(e)[:200]}")
        print("Le notebook continuera sans generation.")
        run_generation = False
        pipe = None
else:
    print("Chargement pipeline desactive")

--- CHARGEMENT DU PIPELINE ---
========================================
Chargement motion adapter : guoyww/animatediff-motion-adapter-v1-5-3
  Motion adapter charge
Chargement base model : stable-diffusion-v1-5/stable-diffusion-v1-5
  VRAM utilisee : 0.0 GB
Pipeline charge en 15.5s
  Scheduler : DDIMScheduler
  VAE slicing : active
  Resolution : 512x512

Section 2 : Generation text-to-video

Nous allons generer une première video a partir d’un prompt textuel. Les paramètres principaux sont :

Paramètre Impact Valeur typique
num_frames Duree de la video 8-24
guidance_scale Adherence au prompt (trop haut = artefacts) 5.0-10.0
num_inference_steps Qualite (plus = mieux mais plus lent) 20-50
height/width Resolution (carree recommandee) 256-512
# Generation text-to-video
print("\n--- GENERATION TEXT-TO-VIDEO ---")
print("=" * 40)

def generate_video(prompt: str, negative_prompt: str = "",
                   seed: int = 42) -> Dict[str, Any]:
    """
    Genere une video a partir d'un prompt textuel.
    
    Args:
        prompt: Description textuelle de la video
        negative_prompt: Elements a eviter
        seed: Graine aleatoire pour reproductibilite
    
    Returns:
        Dict avec frames, temps de generation et metadonnees
    """
    if pipe is None:
        return {"success": False, "error": "Pipeline non charge"}
    
    try:
        generator = torch.Generator(device=device).manual_seed(seed)
        
        if device == "cuda":
            torch.cuda.reset_peak_memory_stats()
            vram_before = torch.cuda.memory_allocated(0) / 1024**3
        
        start_time = time.time()
        
        output = pipe(
            prompt=prompt,
            negative_prompt=negative_prompt or "low quality, blurry, distorted",
            num_frames=num_frames,
            guidance_scale=guidance_scale,
            num_inference_steps=num_inference_steps,
            height=height,
            width=width,
            generator=generator
        )
        
        gen_time = time.time() - start_time
        frames = output.frames[0]  # Liste d'images PIL
        
        result = {
            "success": True,
            "frames": frames,
            "generation_time": gen_time,
            "time_per_frame": gen_time / num_frames,
            "prompt": prompt,
            "seed": seed,
            "params": {
                "num_frames": num_frames,
                "guidance_scale": guidance_scale,
                "num_inference_steps": num_inference_steps,
                "height": height,
                "width": width
            }
        }
        
        if device == "cuda":
            result["vram_peak"] = torch.cuda.max_memory_allocated(0) / 1024**3
        
        return result
        
    except Exception as e:
        return {"success": False, "error": f"{type(e).__name__}: {str(e)[:200]}"}


# Premier test : generation simple
prompt_1 = "a serene lake at sunset with mountains in the background, golden light reflecting on water, cinematic"

if run_generation:
    print(f"Prompt : {prompt_1}")
    print(f"Parametres : {num_frames} frames, {num_inference_steps} steps, CFG={guidance_scale}")
    print(f"Resolution : {width}x{height}")
    print(f"\nGeneration en cours...")
    
    result_1 = generate_video(prompt_1, seed=42)
    
    if result_1['success']:
        frames = result_1['frames']
        print(f"\nGeneration reussie")
        print(f"  Temps total : {result_1['generation_time']:.1f}s")
        print(f"  Temps/frame : {result_1['time_per_frame']:.1f}s")
        print(f"  Frames : {len(frames)}")
        if 'vram_peak' in result_1:
            print(f"  VRAM pic : {result_1['vram_peak']:.1f} GB")
        
        # Affichage en grille
        n_display = min(8, len(frames))
        indices = np.linspace(0, len(frames) - 1, n_display, dtype=int)
        fig, axes = plt.subplots(2, 4, figsize=(16, 8))
        axes_flat = axes.flatten()
        for i, idx in enumerate(indices):
            if i < len(axes_flat):
                axes_flat[i].imshow(frames[idx])
                axes_flat[i].set_title(f"Frame {idx + 1}/{len(frames)}", fontsize=9)
                axes_flat[i].axis('off')
        for i in range(len(indices), len(axes_flat)):
            axes_flat[i].axis('off')
        plt.suptitle(f"AnimateDiff : {prompt_1[:60]}...", fontsize=11, fontweight='bold')
        plt.tight_layout()
        plt.show()
        
        # Sauvegarde GIF
        if save_as_gif:
            gif_path = OUTPUT_DIR / "animatediff_demo.gif"
            export_to_gif(frames, str(gif_path))
            gif_size_kb = gif_path.stat().st_size / 1024
            print(f"  GIF sauvegarde : {gif_path.name} ({gif_size_kb:.1f} KB)")
        
        # Sauvegarde MP4
        if save_as_mp4:
            mp4_path = OUTPUT_DIR / "animatediff_demo.mp4"
            export_to_video(frames, str(mp4_path), fps=fps_output)
            mp4_size_kb = mp4_path.stat().st_size / 1024
            print(f"  MP4 sauvegarde : {mp4_path.name} ({mp4_size_kb:.1f} KB)")
    else:
        print(f"Erreur : {result_1['error']}")
else:
    # Mode pedagogique : afficher ce qui serait genere
    print("**MODE PEDAGOGIQUE**")
    print(f"\nSur un environnement GPU (RTX 3090, 24GB VRAM), ce code genererait :")
    print(f"")
    print(f"| Parametre | Valeur |")
    print(f"|-----------|--------|")
    print(f"| Prompt | {prompt_1[:50]}... |")
    print(f"| Frames | {num_frames} |")
    print(f"| Resolution | {width}x{height} |")
    print(f"| Steps | {num_inference_steps} |")
    print(f"| CFG Scale | {guidance_scale} |")
    print(f"| Temps estime | ~45s |")
    print(f"| VRAM | ~10 GB |")
    print(f"")
    print(f"**Resultat attendu :**")
    print(f"Une video de 2 secondes (16 frames @ 8 fps) montrant un lac paisible")
    print(f"au coucher du soleil avec des montagnes en arriere-plan et des")
    print(f"reflets dorees sur l'eau. Le mouvement sera subtil : ondulations de")
    print(f"l'eau, nuages se deplacant lentement, lumiere changeante.")
    print(f"")
    print(f"**Code pour reproduire :**")
    print(f"```python")
    print(f"result = generate_video('{prompt_1[:40]}...', seed=42)")
    print(f"```")

--- GENERATION TEXT-TO-VIDEO ---
========================================
Prompt : a serene lake at sunset with mountains in the background, golden light reflecting on water, cinematic
Parametres : 16 frames, 25 steps, CFG=7.5
Resolution : 512x512

Generation en cours...

Generation reussie
  Temps total : 26.3s
  Temps/frame : 1.6s
  Frames : 16
  VRAM pic : 4.5 GB

  GIF sauvegarde : animatediff_demo.gif (3123.8 KB)
  MP4 sauvegarde : animatediff_demo.mp4 (660.3 KB)

Exercice 1 : Engineering de negative prompts

La Section 2 genere des videos avec un negative prompt generique ("low quality, blurry, distorted"). L’objectif est d’implementer un système qui construit automatiquement un negative prompt adapte au type de contenu genere.

Contexte : Le negative prompt est aussi important que le prompt positif. Un bon negative prompt adapte au sujet (paysage vs personnage vs objet) ameliore significativement la qualite.

Étapes : 1. Categoriser le prompt positif (paysage, personnage, mouvement, abstrait) 2. Associer une liste de termes negatifs spécifiques a chaque catégorie 3. Ajouter des termes generiques de qualite 4. Tester et comparer avec le negative prompt generique

Indices : - Catégorie “paysage” : negatifs = “people, text, watermark, oversaturated” - Catégorie “personnage” : negatifs = “extra limbs, deformed hands, blurry face” - Catégorie “mouvement” : negatifs = “static, frozen, jittery” - Utilisez des mots-cles dans le prompt pour detecter la catégorie (ex: “lake” -> paysage)

# Exercice 1 : Engineering de negative prompts adaptes

def build_adaptive_negative_prompt(positive_prompt: str) -> str:
    """
    Construit un negative prompt adapte au contenu du prompt positif.
    
    Args:
        positive_prompt: Description textuelle de la video desiree
    
    Returns:
        Negative prompt adapte
    """
    # Etape 1 : Definir les categories et leurs mots-cles
    categories = {
        "paysage": {
            "keywords": ["lake", "mountain", "ocean", "sunset", "forest", "sky",
                         "mer", "montagne", "lac", "foret", "ciel"],
            "negatives": ["people", "text", "watermark", "oversaturated",
                          "buildings", "urban"]
        },
        "personnage": {
            "keywords": ["person", "man", "woman", "cat", "dog", "walking",
                         "personne", "chat", "marche"],
            "negatives": ["extra limbs", "deformed hands", "blurry face",
                          "mutated", "disfigured"]
        },
        "mouvement": {
            "keywords": ["running", "flying", "dancing", "crashing", "launching",
                         "courant", "volant", "lancement"],
            "negatives": ["static", "frozen", "jittery", "stuttering",
                          "repeating motion"]
        }
    }
    
    # Etape 2 : Detecter la categorie dominante
    # TODO etudiant : verifier la presence des mots-cles dans le prompt
    detected = "generic"  # TODO etudiant
    
    # Etape 3 : Construire le negative prompt
    # TODO etudiant : combiner les negatifs specifiques + negatifs generiques
    generic_negatives = ["low quality", "blurry", "distorted", "artifact",
                         "noise", "compression"]
    
    negative = ""  # TODO etudiant : concatener les termes
    
    return negative


# TODO etudiant : Tester avec differents prompts
# test_prompts = [
#     "a serene lake at sunset with mountains",
#     "a cat walking gracefully on a windowsill",
#     "a rocket launching into space"
# ]
# for p in test_prompts:
#     neg = build_adaptive_negative_prompt(p)
#     print(f"Prompt: {p}")
#     print(f"Negative: {neg}\n")
print("Exercice a completer")
Exercice a completer

Section 3 : Exploration des paramètres

Nous allons tester différentes combinaisons de paramètres pour comprendre leur impact sur la qualite et le temps de generation.

# Comparaison de prompts
if run_generation and pipe is not None:
    print("\n--- COMPARAISON DE PROMPTS ---")
    print("=" * 40)
    
    prompts = [
        {
            "text": "a cat walking gracefully on a windowsill, soft morning light, cozy atmosphere",
            "label": "Chat"
        },
        {
            "text": "ocean waves crashing on a rocky shore, dramatic clouds, powerful nature",
            "label": "Ocean"
        },
        {
            "text": "a rocket launching into space with fire and smoke, cinematic epic shot",
            "label": "Fusee"
        }
    ]
    
    comparison_results = []
    
    for p_idx, prompt_info in enumerate(prompts):
        print(f"\nGeneration {p_idx + 1}/{len(prompts)} : {prompt_info['label']}")
        print(f"  Prompt : {prompt_info['text'][:70]}...")
        
        result = generate_video(prompt_info['text'], seed=42 + p_idx)
        
        if result['success']:
            print(f"  Temps : {result['generation_time']:.1f}s")
            comparison_results.append({
                "label": prompt_info['label'],
                "prompt": prompt_info['text'],
                "frames": result['frames'],
                "time": result['generation_time']
            })
            
            # Sauvegarder chaque video
            if save_as_gif:
                gif_path = OUTPUT_DIR / f"comparison_{prompt_info['label'].lower()}.gif"
                export_to_gif(result['frames'], str(gif_path))
        else:
            print(f"  Erreur : {result['error']}")
    
    # Affichage comparatif
    if comparison_results:
        n_videos = len(comparison_results)
        n_preview = 4
        fig, axes = plt.subplots(n_videos, n_preview, figsize=(3.5 * n_preview, 3 * n_videos))
        if n_videos == 1:
            axes = [axes]
        
        for v_idx, cr in enumerate(comparison_results):
            frame_indices = np.linspace(0, len(cr['frames']) - 1, n_preview, dtype=int)
            for f_idx, fi in enumerate(frame_indices):
                axes[v_idx][f_idx].imshow(cr['frames'][fi])
                axes[v_idx][f_idx].axis('off')
                if f_idx == 0:
                    axes[v_idx][f_idx].set_ylabel(cr['label'], fontsize=11, fontweight='bold')
        
        plt.suptitle("Comparaison de prompts - AnimateDiff", fontsize=13, fontweight='bold')
        plt.tight_layout()
        plt.show()
        
        # Tableau recapitulatif
        print(f"\nRecapitulatif :")
        print(f"{'Label':<15} {'Temps (s)':<12} {'Prompt':<60}")
        print("-" * 85)
        for cr in comparison_results:
            prompt_short = cr['prompt'][:55] + '...' if len(cr['prompt']) > 55 else cr['prompt']
            print(f"  {cr['label']:<15} {cr['time']:<12.1f} {prompt_short:<60}")
else:
    print("Comparaison de prompts : generation desactivee")
    print("\nExemples de prompts efficaces pour AnimateDiff :")
    print("  - Mouvements naturels : eau, vent, nuages")
    print("  - Animaux en mouvement : chat marchant, oiseau volant")
    print("  - Paysages dynamiques : coucher de soleil, aurores boreales")
    print("  - Actions simples : fusee decollant, fleur eclosant")

--- COMPARAISON DE PROMPTS ---
========================================

Generation 1/3 : Chat
  Prompt : a cat walking gracefully on a windowsill, soft morning light, cozy atm...
  Temps : 26.6s

Generation 2/3 : Ocean
  Prompt : ocean waves crashing on a rocky shore, dramatic clouds, powerful natur...
  Temps : 25.0s

Generation 3/3 : Fusee
  Prompt : a rocket launching into space with fire and smoke, cinematic epic shot...
  Temps : 26.7s


Recapitulatif :
Label           Temps (s)    Prompt                                                      
-------------------------------------------------------------------------------------
  Chat            26.6         a cat walking gracefully on a windowsill, soft morning ...  
  Ocean           25.0         ocean waves crashing on a rocky shore, dramatic clouds,...  
  Fusee           26.7         a rocket launching into space with fire and smoke, cine...  

Exercice 2 : Analyse de coherence temporelle entre seeds

La Section 3 compare différents prompts avec le même seed (42). L’objectif est d’implementer une fonction qui mesure la coherence temporelle d’une video generee en analysant la variance entre frames consecutives.

Contexte : Une video generee par AnimateDiff peut presenter des sauts temporels (flickering) ou une evolution non fluide. Mesurer la différence entre frames consecutives permet de quantifier ce problème.

Étapes : 1. Pour une liste de frames, calculer la différence absolue moyenne entre chaque paire consecutive 2. Detecter les sauts importants (différence > 2x la mediane) 3. Calculer un score de fluidite : ratio de transitions fluides / total 4. Visualiser l’evolution des différences dans un graphique

Indices : - La différence entre deux frames consecutives : np.mean(np.abs(frame[i].astype(float) - frame[i+1].astype(float))) - Un “saut” est une différence significativement plus elevee que la moyenne - Un score de fluidite proche de 1.0 indique une video fluide sans artefacts temporels

# Exercice 2 : Analyse de coherence temporelle

def analyze_temporal_coherence(frames: List[np.ndarray],
                                jump_threshold: float = 2.0) -> Dict[str, Any]:
    """
    Analyse la coherence temporelle d'une sequence de frames.
    
    Args:
        frames: Liste de frames numpy (H, W, 3) uint8
        jump_threshold: Facteur multiplicateur de la mediane pour detecter les sauts
    
    Returns:
        Dict avec differences, n_jumps, fluidity_score, mean_diff, std_diff
    """
    if len(frames) < 2:
        return {'differences': [], 'n_jumps': 0, 'fluidity_score': 0.0,
                'mean_diff': 0.0, 'std_diff': 0.0}
    
    # Etape 1 : Calculer les differences entre frames consecutives
    differences = []  # TODO etudiant : boucler et calculer np.mean(np.abs(diff))
    
    # Etape 2 : Calculer la mediane et detecter les sauts
    median_diff = 0.0  # TODO etudiant
    threshold = median_diff * jump_threshold
    n_jumps = 0  # TODO etudiant : compter les differences > threshold
    
    # Etape 3 : Score de fluidite
    fluidity_score = 0.0  # TODO etudiant : 1 - (n_jumps / len(differences))
    
    # Etape 4 : Statistiques globales
    mean_diff = 0.0  # TODO etudiant
    std_diff = 0.0   # TODO etudiant
    
    return {
        'differences': differences,
        'n_jumps': n_jumps,
        'fluidity_score': fluidity_score,
        'mean_diff': mean_diff,
        'std_diff': std_diff
    }


def plot_temporal_coherence(differences: List[float], title: str = "") -> None:
    """
    Affiche l'evolution des differences entre frames consecutives.
    """
    # TODO etudiant : tracer les differences avec matplotlib
    # TODO etudiant : ajouter la ligne horizontale du seuil de saut
    pass


# TODO etudiant : Analyser une video generee
# if result_1.get('success') and result_1.get('frames'):
#     coherence = analyze_temporal_coherence(
#         [np.array(f) for f in result_1['frames']]
#     )
#     print(f"Fluidite : {coherence['fluidity_score']:.3f}")
#     print(f"Sauts detectes : {coherence['n_jumps']}")
#     plot_temporal_coherence(coherence['differences'], "Video 1")
print("Exercice a completer")
Exercice a completer

Exercice 3 : Impact des paramètres de generation sur la qualite video

Les paramètres guidance_scale, num_inference_steps et num_frames influencent fortement la qualite d’une video AnimateDiff. L’objectif est d’implementer un sweep de paramètres qui genere des videos avec différentes configurations et mesure leur impact.

Contexte : Le guidance_scale contrôle l’adherence au prompt (trop bas = flou, trop haut = artefacts). Le num_inference_steps affecte la qualite du debruitage. Le nombre de frames impacte la coherence temporelle.

Étapes : 1. Définir une grille de paramètres avec au moins 2 valeurs par paramètre 2. Pour chaque combinaison, simuler (ou executer si GPU disponible) une generation 3. Mesurer le temps de generation et la coherence temporelle (si frames disponibles) 4. Presenter les résultats dans un DataFrame et un graphique comparatif

Indices : - Grille suggeree : guidance_scale in [5.0, 7.5, 10.0], num_inference_steps in [15, 25] - Pour mesurer la coherence sans GPU, vous pouvez comparer les prompts et paramètres theoriquement - Le temps de generation est approximativement proportionnel a num_frames * num_inference_steps - Un DataFrame avec colonnes guidance, steps, frames, temps_estime, qualite_estimee

# Exercice 3 : Sweep de parametres AnimateDiff

def parameter_sweep(prompt: str,
                    guidance_values: List[float] = [5.0, 7.5, 10.0],
                    steps_values: List[int] = [15, 25],
                    num_frames_fixed: int = 16) -> pd.DataFrame:
    """
    Teste differentes combinaisons de parametres AnimateDiff.
    
    Args:
        prompt: Texte de prompt pour la generation
        guidance_values: Liste de guidance_scale a tester
        steps_values: Liste de num_inference_steps a tester
        num_frames_fixed: Nombre de frames fixe pour le sweep
    
    Returns:
        DataFrame avec resultats par combinaison
    """
    results = []
    
    for guidance in guidance_values:
        for steps in steps_values:
            # TODO etudiant : estimer le temps de generation
            # Le temps est approximativement : steps * num_frames * 0.1s (sur RTX 3090)
            estimated_time = 0.0  # TODO etudiant
            
            # TODO etudiant : estimer la qualite theorique
            # guidance trop haut (>10) = artefacts, trop bas (<5) = flou
            # steps trop bas (<15) = sous-debruite, trop haut (>50) = rendement decroissant
            estimated_quality = "medium"  # TODO etudiant : logique de classification
            
            # TODO etudiant : si pipe est disponible, lancer la vraie generation
            # result = generate_video(prompt, seed=42)
            # et mesurer le temps reel et la coherence temporelle
            
            results.append({
                'guidance_scale': guidance,
                'num_inference_steps': steps,
                'num_frames': num_frames_fixed,
                'temps_estime': estimated_time,
                'qualite_estimee': estimated_quality,
                'generation_reussie': False  # TODO etudiant
            })
    
    return pd.DataFrame(results)


# TODO etudiant : Executer le sweep et analyser les resultats
# df_sweep = parameter_sweep("a cat walking on a windowsill")
# print(df_sweep)
# 
# TODO etudiant : Visualiser avec un graphique
# fig, ax = plt.subplots(figsize=(10, 6))
# for guidance in [5.0, 7.5, 10.0]:
#     subset = df_sweep[df_sweep['guidance_scale'] == guidance]
#     ax.plot(subset['num_inference_steps'], subset['temps_estime'],
#             marker='o', label=f'CFG={guidance}')
# ax.set_xlabel('Steps')
# ax.set_ylabel('Temps estime (s)')
# ax.legend()
# ax.set_title('Impact des parametres sur le temps de generation')
# plt.show()
print("Exercice a completer")
Exercice a completer

Les différents prompts ont ete compares. La cellule suivante active le mode interactif pour generer une video a partir d’un prompt personnalise saisi par l’utilisateur.

# Mode interactif
if notebook_mode == "interactive" and not skip_widgets:
    print("\n--- MODE INTERACTIF ---")
    print("=" * 40)
    print("Entrez votre propre prompt pour generer une video.")
    print("(Laissez vide pour passer a la suite)")
    
    try:
        user_prompt = input("\nVotre prompt : ").strip()
        
        if user_prompt and run_generation and pipe is not None:
            print(f"\nGeneration en cours...")
            result_user = generate_video(user_prompt, seed=123)
            
            if result_user['success']:
                print(f"Generation reussie en {result_user['generation_time']:.1f}s")
                
                # Affichage
                n_display = min(8, len(result_user['frames']))
                fig, axes = plt.subplots(1, n_display, figsize=(2.5 * n_display, 3))
                if n_display == 1:
                    axes = [axes]
                indices = np.linspace(0, len(result_user['frames']) - 1, n_display, dtype=int)
                for ax, idx in zip(axes, indices):
                    ax.imshow(result_user['frames'][idx])
                    ax.set_title(f"Frame {idx+1}", fontsize=8)
                    ax.axis('off')
                plt.suptitle(f"Votre video : {user_prompt[:50]}...", fontweight='bold')
                plt.tight_layout()
                plt.show()
                
                if save_as_gif:
                    user_gif = OUTPUT_DIR / "user_generation.gif"
                    export_to_gif(result_user['frames'], str(user_gif))
                    print(f"GIF sauvegarde : {user_gif.name}")
            else:
                print(f"Erreur : {result_user['error']}")
        elif user_prompt:
            print("Generation non disponible (pipeline non charge)")
        else:
            print("Mode interactif ignore")
    
    except (KeyboardInterrupt, EOFError) as e:
        print(f"\nMode interactif interrompu ({type(e).__name__})")
    except Exception as e:
        error_type = type(e).__name__
        if "StdinNotImplemented" in error_type or "input" in str(e).lower():
            print("\nMode interactif non disponible (execution automatisee)")
        else:
            print(f"\nErreur inattendue : {error_type} - {str(e)[:100]}")
            print("Passage a la suite du notebook")
else:
    print("\nMode batch - Interface interactive desactivee")

--- MODE INTERACTIF ---
========================================
Entrez votre propre prompt pour generer une video.
(Laissez vide pour passer a la suite)

Mode interactif non disponible (execution automatisee)

Bonnes pratiques et optimisation

Conseils pour les prompts AnimateDiff

Bon prompt Mauvais prompt Raison
“a cat walking slowly” “a cat” Preciser le mouvement
“ocean waves, cinematic” “nice ocean” Termes de qualite
“single subject, simple motion” “crowd scene, complex action” Mouvement simple = meilleur

Paramètres optimaux

Paramètre Rapide Equilibre Haute qualite
num_frames 8 16 24
num_inference_steps 15 25 50
guidance_scale 5.0 7.5 7.5
height/width 256 512 512
Temps estime (RTX 3090) ~15s ~45s ~120s
VRAM estimee ~6 GB ~10 GB ~14 GB

Limitations d’AnimateDiff

  • Resolution maximale limitee (512x512 pour SD 1.5)
  • Mouvements complexes souvent mal geres
  • Coherence temporelle imparfaite sur les longues sequences
  • Base model SD 1.5 (qualite inferieure a SDXL ou Flux)

Alternatives (voir Module 02)

Modèle Avantage VRAM
HunyuanVideo Haute qualite, longues sequences ~18 GB
LTX-Video Rapide, leger ~8 GB
Wan 2.1/2.2 Prompts multilingues ~10 GB
SVD Image-to-video ~10 GB
# 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"Base model : {base_model}")
print(f"Motion adapter : {motion_adapter}")
print(f"Device : {device}")
print(f"Parametres : {num_frames} frames, {num_inference_steps} steps, CFG={guidance_scale}")
print(f"Resolution : {width}x{height}")

if device == "cuda" and torch.cuda.is_available():
    vram_peak = torch.cuda.max_memory_allocated(0) / 1024**3
    print(f"VRAM pic session : {vram_peak:.1f} GB")

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

# Liberation VRAM
if pipe is not None:
    del pipe
    if device == "cuda":
        torch.cuda.empty_cache()
        print(f"\nVRAM liberee")

print(f"\n--- PROCHAINES ETAPES ---")
print(f"1. Module 02-1 : HunyuanVideo (generation video haute qualite)")
print(f"2. Module 02-2 : LTX-Video (generation rapide et legere)")
print(f"3. Module 02-3 : Wan 2.1/2.2 (prompts multilingues)")
print(f"4. Module 02-4 : SVD - Stable Video Diffusion (image-to-video)")
print(f"5. Combiner 01-4 (upscaling) avec la generation pour des videos HD")

print(f"\nNotebook 01-5 AnimateDiff Introduction termine - {datetime.now().strftime('%H:%M:%S')}")

--- STATISTIQUES DE SESSION ---
========================================
Date : 2026-09-03 00:59:21
Mode : interactive
Base model : stable-diffusion-v1-5/stable-diffusion-v1-5
Motion adapter : guoyww/animatediff-motion-adapter-v1-5-3
Device : cuda
Parametres : 16 frames, 25 steps, CFG=7.5
Resolution : 512x512
VRAM pic session : 4.5 GB

Fichiers generes (5) :
  animatediff_demo.gif (3123.8 KB)
  animatediff_demo.mp4 (660.3 KB)
  comparison_chat.gif (2997.3 KB)
  comparison_fusee.gif (2570.6 KB)
  comparison_ocean.gif (4283.5 KB)

VRAM liberee

--- PROCHAINES ETAPES ---
1. Module 02-1 : HunyuanVideo (generation video haute qualite)
2. Module 02-2 : LTX-Video (generation rapide et legere)
3. Module 02-3 : Wan 2.1/2.2 (prompts multilingues)
4. Module 02-4 : SVD - Stable Video Diffusion (image-to-video)
5. Combiner 01-4 (upscaling) avec la generation pour des videos HD

Notebook 01-5 AnimateDiff Introduction termine - 00:59:21

Exemple guide : Exploration des Paramètres AnimateDiff

Duree estimee : 25-30 minutes

Objectif

Explorer systematiquement l’impact des paramètres AnimateDiff sur la qualite et le style des videos generees.

Instructions

  1. Grille de paramètres : Completez la fonction ci-dessous pour tester différentes combinaisons de guidance_scale, num_inference_steps et num_frames.

  2. Analyse qualitative : Pour chaque combinaison, enregistrez la video et notez la qualite (coherence temporelle, nettete, presence d’artefacts).

  3. Visualisation des résultats : Créez une grille visuelle comparant les résultats pour identifier les paramètres optimaux.

Indices

  • Indice 1 : Utilisez des seeds fixes pour assurer la reproductibilite entre les tests.
  • Indice 2 : Le guidance_scale trop eleve (>10) genere souvent des artefacts, trop bas (<3) donne des results flous.
  • Indice 3 : Le nombre de frames affecte la coherence temporelle (plus de frames = plus difficile a maintenir).
  • Indice 4 : num_inference_steps impacte la qualite mais a un rendement decroissant (>30 steps = peu d’amelioration).

Critères de succès

# Exercice: Exploration systematique des parametres AnimateDiff

def explore_params(grid: Dict[str, List[Any]],
                   prompt: str,
                   pipe) -> pd.DataFrame:
    """
    Explore une grille de parametres et enregistre les resultats.
    
    Args:
        grid: Dictionnaire {"param_name": [values]}
        prompt: Prompt texte pour la generation
        pipe: Pipeline AnimateDiff charge
    
    Returns:
        DataFrame avec resultats et metriques
    """
    results = []
    
    # Exercice: Generer toutes les combinaisons de parametres
    # Exercice: Pour chaque combinaison :
    #   - Generer la video
    #   - Sauvegarder le GIF
    #   - Noter la qualite (manuelle ou automatique)
    # Exercice: Retourner un DataFrame resume
    
    return pd.DataFrame(results)

# Exercice: Fonction d'evaluation automatique (optionnel)

def evaluate_video_quality(frames: List[np.ndarray]) -> Dict[str, float]:
    """
    Evalue la qualite d'une video generee.
    
    Args:
        frames: Liste des frames de la video
    
    Returns:
        Dict avec metriques (coherence, nettete, etc.)
    """
    # Exercice: Calculer la difference moyenne entre frames consecutives
    # Exercice: Mesurer la nettete (variance du Laplacian)
    # Exercice: Detecter les artefacts (pixels satures, etc.)
    pass

# Exercice: Creer la grille de parametres et tester
param_grid = {
    "guidance_scale": [5.0, 7.5, 10.0],
    "num_inference_steps": [15, 25, 35],
    "num_frames": [8, 16, 24]
}

# Exercice: Analyser les resultats et identifier les parametres optimaux
# df_results = explore_params(param_grid, "a cat walking", pipe)
Retour au sommet