Wan 2.1/2.2 - Generation Video Multilingue

Module : 02-Video-Advanced
Niveau : Intermediaire
Technologies : Wan 2.1/2.2 (Alibaba), ComfyUI API ou diffusers
Duree estimee : 45 minutes
VRAM : ~8-10 GB

Objectifs d’Apprentissage

Prerequis

Mode API ComfyUI (recommande pour production)

  • Service ComfyUI-Video demarre (docker-compose comfyui-video)
  • Pas de dependances Python lourdes cote client

Mode Local diffusers (pedagogique)

  • GPU avec 10+ GB VRAM
  • Packages : diffusers>=0.32, transformers, torch, accelerate, imageio

Navigation : << 02-2 | Index | Suivant >>

# Parametres Papermill - JAMAIS modifier ce commentaire
import os

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

# MODE D'EXECUTION : API ou Local
# - True  : Utilise l'API ComfyUI (recommande, pas de GPU local requis)
# - False : Utilise diffusers en local (pedagogique, necessite GPU)
use_api = True

# Parametres API ComfyUI (si use_api=True)
comfyui_url = os.getenv("COMFYUI_VIDEO_URL", "https://comfyui-video.myia.io")  # ComfyUI-Video service
comfyui_token = os.getenv("COMFYUI_VIDEO_TOKEN")  # Token Bearer (charge depuis .env)

# Parametres modele Wan (si use_api=False)
model_id = "Wan-AI/Wan2.1-T2V-14B"  # Modele Wan 2.1 Text-to-Video (HuggingFace)
quantize = True                      # Quantification INT8 (recommande)
device = "cuda"                     # Device de calcul

# Parametres generation (communs aux deux modes)
num_frames = 16                    # Nombre de frames a generer
guidance_scale = 6.0               # CFG scale pour Wan (6.0 recommande)
num_inference_steps = 20           # Nombre d'etapes de debruitage
height = 480                       # Hauteur video
width = 832                        # Largeur video (ratio 16:9)
fps_output = 16                    # FPS de la video de sortie

# Configuration
run_generation = True              # Executer la generation
save_results = True
# Parameters
BATCH_MODE = "true"

Les paramètres Papermill initialises, la cellule suivante installe les dependances requises et importe les bibliotheques necessaires : diffusers pour le pipeline Wan, torch pour l’acceleration GPU, et les utilitaires de gestion des fichiers video.

# 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
from dotenv import load_dotenv

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

# Initialisation des variables globales (important pour l'execution Papermill)
comfyui_client = None

# Import helpers GenAI
GENAI_ROOT = Path.cwd()
while GENAI_ROOT.name != 'GenAI' and len(GENAI_ROOT.parts) > 1:
    GENAI_ROOT = GENAI_ROOT.parent

HELPERS_PATH = GENAI_ROOT / 'shared' / 'helpers'
if HELPERS_PATH.exists():
    sys.path.insert(0, str(HELPERS_PATH.parent))
    try:
        from helpers import comfyui_client
        print("[OK] Helper comfyui_client importé")
    except ImportError as e:
        print(f"[WARN] Helper comfyui_client NON disponible: {e}")
        comfyui_client = None
else:
    print("[WARN] Répertoire helpers non trouvé, comfyui_client sera None")

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

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

# Affichage du mode d'execution
mode_str = "API ComfyUI" if use_api else "Local diffusers"
print(f"Wan 2.1/2.2 - Generation Video Multilingue")
print(f"Mode d'execution : {mode_str}")
print(f"Date : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Frames : {num_frames}, Steps : {num_inference_steps}, CFG : {guidance_scale}")
print(f"Resolution : {width}x{height}")
[OK] Helper comfyui_client importé
Wan 2.1/2.2 - Generation Video Multilingue
Mode d'execution : API ComfyUI
Date : 2026-08-20 14:08:05
Frames : 16, Steps : 20, CFG : 6.0
Resolution : 832x480

Les imports et dependances charges, la cellule suivante initialise la configuration depuis .env, verifie la disponibilite du GPU et determine le mode d’exécution : generation locale ou mode pedagogique si aucun GPU n’est detecte.

# Chargement .env et verification de l'environnement
# Chargement robuste de la configuration .env
from dotenv import load_dotenv
import os

# Recherche du .env dans tous les parents (pour Papermill qui change le cwd)
current_path = Path.cwd()
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
        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")

# Le token est lu dans la cellule parametres AVANT le load_dotenv : on le relit ici
if use_api:
    env_token = os.getenv("COMFYUI_VIDEO_TOKEN", "")
    if env_token:
        comfyui_token = env_token

# Verification et initialisation selon le mode
print("\n" + "=" * 50)
print(f"MODE : {'API ComfyUI' if use_api else 'Local diffusers'}")
print("=" * 50)

client = None
pipe = None
comfyui_available = False
local_available = False

if use_api:
    # === MODE API COMFYUI ===
    print("\n[API] Verification de l'API ComfyUI-Video...")
    
    if comfyui_client is not None:
        try:
            client = comfyui_client.ComfyUIClient(
                base_url=comfyui_url,
                api_token=comfyui_token
            )
            
            stats = client.get_system_stats()
            
            print(f"[OK] ComfyUI-Video accessible sur : {comfyui_url}")
            comfyui_available = True
            
        except Exception as e:
            print(f"[WARN] ComfyUI-Video non accessible: {type(e).__name__}: {str(e)[:100]}")
            print("\n💡 Pour démarrer ComfyUI-Video :")
            print("   docker-compose -f docker-configurations/services/comfyui-video/docker-compose.yml up -d")
            run_generation = False
    else:
        print("[WARN] Helper comfyui_client non disponible")
        run_generation = False
        
else:
    # === MODE LOCAL DIFFUSERS ===
    print("\n[LOCAL] Verification de l'environnement local...")
    
    # Verification GPU
    try:
        import torch
        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
            print(f"[OK] GPU : {gpu_name}")
            print(f"   VRAM totale : {vram_total:.1f} GB")
            
            if vram_total < 10:
                print(f"[WARN] VRAM faible (< 10 GB), activation de la quantification")
                quantize = True
                height = 320
                width = 576
                num_frames = 12
        else:
            print("[WARN] CUDA non disponible")
            run_generation = False
    except ImportError:
        print("[WARN] PyTorch non installé")
        run_generation = False
    
    # Verification des dependances
    deps_ok = True
    
    try:
        import diffusers
        print(f"[OK] diffusers : v{diffusers.__version__}")
    except ImportError:
        print("[WARN] diffusers NON INSTALLE (pip install diffusers>=0.32)")
        deps_ok = False
    
    try:
        import transformers
        print(f"[OK] transformers : v{transformers.__version__}")
    except ImportError:
        print("[WARN] transformers NON INSTALLE")
        deps_ok = False
    
    if quantize:
        try:
            import bitsandbytes as bnb
            print(f"[OK] bitsandbytes : v{bnb.__version__}")
        except ImportError:
            print("[WARN] bitsandbytes NON INSTALLE (pip install bitsandbytes)")
            quantize = False
    
    if deps_ok and run_generation:
        print("\n[PACKAGE] Chargement du pipeline Wan...")
        try:
            from diffusers import WanPipeline
            from diffusers.utils import export_to_video
            
            start_load = time.time()
            
            if quantize:
                from diffusers import BitsAndBytesConfig
                quant_config = BitsAndBytesConfig(load_in_8bit=True)
                pipe = WanPipeline.from_pretrained(
                    model_id,
                    quantization_config=quant_config,
                    torch_dtype=torch.float16
                )
            else:
                pipe = WanPipeline.from_pretrained(
                    model_id,
                    torch_dtype=torch.float16
                )
            
            pipe = pipe.to(device)
            pipe.enable_vae_slicing()
            pipe.enable_vae_tiling()
            
            load_time = time.time() - start_load
            print(f"[OK] Pipeline charge en {load_time:.1f}s")
            local_available = True
            
        except Exception as e:
            print(f"[WARN] Erreur chargement pipeline : {type(e).__name__}: {str(e)[:200]}")
            run_generation = False

# Flag unique : la generation est possible (mode + disponibilite backend)
generation_ready = run_generation and (comfyui_available if use_api else local_available)

print(f"\n{'='*50}")
print(f"Generation activee : {run_generation}")
print(f"Backend pret : {generation_ready}")
print(f"{'='*50}")
WARNING: .env non trouve, utilisation variables environnement

==================================================
MODE : API ComfyUI
==================================================

[API] Verification de l'API ComfyUI-Video...
[OK] ComfyUI-Video accessible sur : https://comfyui-video.myia.io

==================================================
Generation activee : True
Backend pret : True
==================================================

Section 1 : Architecture Wan 2.1/2.2

Wan est une famille de modèles de generation video developpes par Alibaba. La serie 2.1/2.2 se distingue par sa comprehension multilingue et son contrôle fin du mouvement.

Deux approches pour utiliser Wan

Aspect API ComfyUI Local diffusers
Cas d’usage Production, applications Pedagogie, recherche
GPU requis Non (cote serveur) Oui (10+ GB)
Installation Aucune (Docker) diffusers, transformers, torch
Flexibilite Moyenne Elevee
Performance Serveur optimise Depend du GPU local

Architecture de Wan

Composant Description
UNET wan2.1_t2v_1.3B_fp16.safetensors (~2.5GB)
Text Encoder umt5_xxl_fp8_e4m3fn_scaled.safetensors (~4GB)
VAE wan_2.1_vae.safetensors (~360MB)
Sampling ModelSamplingSD3 (shift=8), scheduler uni_pc

Comparaison Wan 2.1 vs 2.2

Aspect Wan 2.1 Wan 2.2
Qualite Bonne Amelioree
Coherence temporelle Bonne Très bonne
Motion control Basique Camera paths avancees
Resolutions 480p-720p 480p-1080p
VRAM (1.3B) ~8 GB ~8 GB
# Fonction de generation unifiee (API ou Local)
import imageio


def generate_wan_video(prompt: str, negative_prompt: str = "", seed: int = 42, save_prefix: str = "wan") -> Dict[str, Any]:
    """
    Genere une video avec Wan (API ComfyUI ou local diffusers).
    
    Cette fonction s'adapte automatiquement au mode d'execution choisi.
    
    Args:
        prompt: Description textuelle (FR, EN ou CN)
        negative_prompt: Elements a eviter
        seed: Graine aleatoire pour reproductibilite
    
    Returns:
        Dict avec frames, temps de generation et metadonnees
    """
    if use_api:
        # === MODE API COMFYUI ===
        if not comfyui_available:
            return {"success": False, "error": "API ComfyUI non disponible"}
        
        try:
            start_time = time.time()
            
            result = client.generate_text2video_wan(
                prompt=prompt,
                width=width,
                height=height,
                num_frames=num_frames,
                steps=num_inference_steps,
                seed=seed,
                cfg=guidance_scale,
                save_prefix=save_prefix,
                timeout=300
            )
            
            gen_time = time.time() - start_time

            # Telechargement de la video cote notebook (preuve d execution reelle,
            # meme pattern que generate_hunyuan_video de 02-1)
            outputs = result.get("outputs", {})
            videos = (
                outputs.get("50", {}).get("videos", [])
                or outputs.get("50", {}).get("gifs", [])
                or outputs.get("50", {}).get("images", [])
            )
            if not videos:
                return {"success": False, "error": "ComfyUI n a retourne aucun fichier video (noeud 50)"}
            meta = videos[0]
            mp4_data = client.get_output_file(
                meta["filename"], meta.get("subfolder", ""), meta.get("type", "output")
            )
            mp4_path = OUTPUT_DIR / f"{save_prefix}_{seed}.mp4"
            mp4_path.write_bytes(mp4_data)
            frames = imageio.v2.mimread(str(mp4_path))

            return {
                "success": True,
                "frames": frames,
                "video_path": str(mp4_path),
                "generation_time": gen_time,
                "time_per_frame": gen_time / max(len(frames), 1),
                "mode": "API ComfyUI",
                "seed": seed,
                "vram_peak": 0.0
            }
            
        except Exception as e:
            return {"success": False, "error": f"{type(e).__name__}: {str(e)[:200]}"}
    
    else:
        # === MODE LOCAL DIFFUSERS ===
        if not local_available:
            return {"success": False, "error": "Pipeline local non disponible"}
        
        try:
            import torch
            from diffusers.utils import export_to_video
            
            generator = torch.Generator(device=device).manual_seed(seed)
            
            if device == "cuda":
                torch.cuda.reset_peak_memory_stats()
            
            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]
            
            # Sauvegarder en MP4
            mp4_path = OUTPUT_DIR / f"wan_local_{seed}.mp4"
            export_to_video(frames, str(mp4_path), fps=fps_output)
            
            result_dict = {
                "success": True,
                "frames": frames,
                "generation_time": gen_time,
                "time_per_frame": gen_time / num_frames,
                "prompt": prompt,
                "seed": seed,
                "mode": "Local diffusers",
                "video_path": str(mp4_path),
                "mp4_path": str(mp4_path)
            }
            
            if device == "cuda":
                result_dict["vram_peak"] = torch.cuda.max_memory_allocated(0) / 1024**3
            
            return result_dict
            
        except Exception as e:
            return {"success": False, "error": f"{type(e).__name__}: {str(e)[:200]}"}

print("[OK] Fonction de generation unifiee chargee")
[OK] Fonction de generation unifiee chargee

Section 2 : Generation avec prompts multilingues

L’un des avantages distinctifs de Wan est sa comprehension multilingue. Nous allons tester des prompts en francais et en anglais pour comparer les résultats.

# Generation avec prompts multilingues
print("\n--- PROMPTS MULTILINGUES ---")
print("=" * 40)

bilingual_prompts = [
    {
        "text": "Un chat roux dort paisiblement sur un rebord de fenetre ensoleille, lumiere douce, atmosphere calme",
        "lang": "FR",
        "label": "Chat (FR)"
    },
    {
        "text": "A ginger cat sleeping peacefully on a sunny windowsill, soft light, calm atmosphere",
        "lang": "EN",
        "label": "Cat (EN)"
    }
]

bilingual_results = []

if run_generation:
    for p_idx, prompt_info in enumerate(bilingual_prompts):
        print(f"\nGeneration {p_idx + 1}/{len(bilingual_prompts)} : {prompt_info['label']}")
        print(f"  Prompt ({prompt_info['lang']}) : {prompt_info['text'][:60]}...")

        result = generate_wan_video(prompt_info['text'], seed=42, save_prefix=f"wan_{prompt_info['lang'].lower()}")

        if result['success']:
            print(f"  [OK] Generation terminee en {result['generation_time']:.1f}s ({result['mode']})")
            if result.get('video_path'):
                mp4 = Path(result['video_path'])
                print(f"     MP4 sauvegarde : {mp4.name} ({mp4.stat().st_size / 1024:.1f} KB)")
            if 'vram_peak' in result:
                print(f"     VRAM pic : {result['vram_peak']:.1f} GB")
            bilingual_results.append({**result, **prompt_info})

            # Affichage planche frames (embed image/png dans l'output cellule).
            # Meme pattern que generate_hunyuan_video de 02-1 (#11026) : plt.subplots
            # sur 4 frames equidistantes + plt.show() commit un image/png directement
            # dans la sortie de la cellule, qui survit au commit (les MP4 sont dans
            # outputs/ gitignore, les frames embed sont la preuve visible de l'exec).
            frames = result.get('frames')
            if frames is not None and len(frames) > 0:
                n_display = min(4, len(frames))
                indices = np.linspace(0, len(frames) - 1, n_display, dtype=int)
                fig, axes = plt.subplots(1, n_display, figsize=(4 * n_display, 4))
                if n_display == 1:
                    axes = [axes]
                for j, idx in enumerate(indices):
                    axes[j].imshow(frames[idx])
                    axes[j].set_title(f"{prompt_info['label']} - Frame {idx+1}/{len(frames)}", fontsize=9)
                    axes[j].axis('off')
                plt.tight_layout()
                plt.show()
        else:
            print(f"  [ERROR] Erreur : {result['error']}")

    # Resume des generations
    if bilingual_results:
        print(f"\n{'Langue':<15} {'Mode':<20} {'Temps (s)':<12}")
        print("-" * 47)
        for br in bilingual_results:
            print(f"  {br['label']:<15} {br['mode']:<20} {br['generation_time']:<12.1f}")
else:
    print("Generation desactivee")
    print("\n💡 Pour activer la generation, set run_generation=True")

--- PROMPTS MULTILINGUES ---
========================================

Generation 1/2 : Chat (FR)
  Prompt (FR) : Un chat roux dort paisiblement sur un rebord de fenetre enso...
  [OK] Generation terminee en 130.1s (API ComfyUI)
     MP4 sauvegarde : wan_fr_42.mp4 (77.3 KB)
     VRAM pic : 0.0 GB


Generation 2/2 : Cat (EN)
  Prompt (EN) : A ginger cat sleeping peacefully on a sunny windowsill, soft...
  [OK] Generation terminee en 28.6s (API ComfyUI)
     MP4 sauvegarde : wan_en_42.mp4 (107.4 KB)
     VRAM pic : 0.0 GB


Langue          Mode                 Temps (s)   
-----------------------------------------------
  Chat (FR)       API ComfyUI          130.1       
  Cat (EN)        API ComfyUI          28.6        

Exercice 1 : Analyseur de structure de prompt multilingue

Objectif : Implementer une fonction qui analyse un prompt video, identifie ses composants (sujet, action, style, eclairage, camera) et le enrichit automatiquement pour Wan.

Un prompt efficace pour Wan contient généralement 5 éléments : sujet, action, type de camera, eclairage et style. Cet exercice consiste a parser et structurer un prompt brut.

Indices : - # Étape 1 : Définir des listes de mots-cles pour chaque catégorie : CAMERA_WORDS (pan, zoom, tracking, drone, close-up), LIGHTING_WORDS (golden hour, dramatic, soft, backlit), STYLE_WORDS (cinematic, 4K, slow motion, timelapse) - # Étape 2 : Implementer parse_prompt_components() qui detecte la presence de chaque catégorie dans le texte - # Étape 3 : Implementer enrich_prompt() qui ajoute les composants manquants avec des valeurs par defaut pertinentes - # Indice : Wan comprend le francais, mais les mots-cles techniques en anglais (cinematic, golden hour) sont souvent plus efficaces

def parse_prompt_components(prompt: str) -> dict:
    """
    Analyse un prompt video et identifie ses composants structurels.
    
    Args:
        prompt: Texte du prompt (FR ou EN)
    
    Returns:
        Dict avec les composants detectes : sujet, action, camera, eclairage, style, composants_manquants
    """
    # Etape 1 : Definir les vocabulaires par categorie
    # TODO etudiant : completer CAMERA_WORDS, LIGHTING_WORDS, STYLE_WORDS
    CAMERA_WORDS = []    # TODO etudiant
    LIGHTING_WORDS = []  # TODO etudiant
    STYLE_WORDS = []     # TODO etudiant
    
    # Etape 2 : Analyser le prompt pour detecter chaque categorie
    # TODO etudiant : verifier la presence des mots-cles dans le prompt (insensible a la casse)
    
    # Etape 3 : Identifier les composants manquants
    # TODO etudiant : lister les categories non trouvees
    pass
    
    return None  # TODO etudiant : retourner le dictionnaire d'analyse


def enrich_prompt(prompt: str) -> str:
    """
    Enrichit un prompt en ajoutant les composants manquants.
    
    Args:
        prompt: Prompt original
    
    Returns:
        Prompt enrichi avec les composants manquants
    """
    # TODO etudiant : appeler parse_prompt_components() puis ajouter les elements manquants
    pass
    
    return prompt  # TODO etudiant : retourner le prompt enrichi

# Test avec un prompt minimal
test_prompt = "a cat on a sofa"
print(f"Prompt original : {test_prompt}")
print("Exercice a completer")
Prompt original : a cat on a sofa
Exercice a completer

Interpretation : Prompts multilingues

Wan 2.1 utilise le text encoder UMT5-XXL (une variante de T5) qui a ete entraine sur de multiples langues. Cela permet de generer des videos a partir de prompts en francais, anglais, ou chinois avec des résultats comparables.

Langue Prompt Résultat attendu
Francais “Un chat roux dort paisiblement sur un rebord de fenêtre ensoleille, lumiere douce, atmosphere calme” Chat bien positionné, lumière chaude, atmosphère reposante
Anglais “A ginger cat sleeping peacefully on a sunny windowsill, soft light, calm atmosphere” Composition similaire, détails légèrement plus précis

Points cles : - Les concepts principaux sont bien captés dans les deux langues - L’anglais peut capturer plus de nuances subtiles - Le francais est parfaitement utilisable pour des prompts courants

Comparaison des modes : - API ComfyUI : Plus rapide (serveur optimise), pas de gestion GPU - Local diffusers : Plus de contrôle (debug, personnalisation), necessite GPU

Section 3 : Contrôle de mouvement et camera

Wan permet de contrôler le mouvement de la camera a travers des mots-cles spécifiques dans le prompt.

# Controle de mouvement et camera
if run_generation:
    print("\n--- CONTROLE DE MOUVEMENT ---")
    print("=" * 40)

    motion_prompts = [
        {
            "text": "a slow pan across a beautiful Japanese garden with cherry blossoms, smooth camera movement, serene",
            "label": "Pan lateral",
            "motion": "pan"
        },
        {
            "text": "camera slowly zooming into a detailed oil painting of a medieval castle, revealing intricate details",
            "label": "Zoom avant",
            "motion": "zoom"
        },
        {
            "text": "aerial drone shot flying over a coastal city at golden hour, birds eye view, cinematic",
            "label": "Vol aerien",
            "motion": "drone"
        }
    ]

    motion_results = []

    for p_idx, prompt_info in enumerate(motion_prompts):
        print(f"\nGeneration {p_idx + 1}/{len(motion_prompts)} : {prompt_info['label']}")
        print(f"  Type de mouvement : {prompt_info['motion']}")

        result = generate_wan_video(prompt_info['text'], seed=42 + p_idx, save_prefix=f"wan_{prompt_info['motion']}")

        if result['success']:
            print(f"  [OK] Generation terminee en {result['generation_time']:.1f}s ({result['mode']})")
            if result.get('video_path'):
                mp4 = Path(result['video_path'])
                print(f"     MP4 sauvegarde : {mp4.name} ({mp4.stat().st_size / 1024:.1f} KB)")
            motion_results.append({**result, **prompt_info})

            # Affichage planche frames (embed image/png dans l'output cellule).
            # Meme pattern que generate_hunyuan_video de 02-1 (#11026) : 4 frames
            # equidistantes pour visualiser la trajectoire du mouvement.
            frames = result.get('frames')
            if frames is not None and len(frames) > 0:
                n_display = min(4, len(frames))
                indices = np.linspace(0, len(frames) - 1, n_display, dtype=int)
                fig, axes = plt.subplots(1, n_display, figsize=(4 * n_display, 4))
                if n_display == 1:
                    axes = [axes]
                for j, idx in enumerate(indices):
                    axes[j].imshow(frames[idx])
                    axes[j].set_title(f"{prompt_info['label']} - Frame {idx+1}/{len(frames)}", fontsize=9)
                    axes[j].axis('off')
                plt.tight_layout()
                plt.show()
        else:
            print(f"  [ERROR] Erreur : {result['error']}")

    # Resume des generations
    if motion_results:
        print(f"\n{'Mouvement':<15} {'Mode':<20} {'Temps (s)':<12}")
        print("-" * 47)
        for mr in motion_results:
            print(f"  {mr['label']:<15} {mr['mode']:<20} {mr['generation_time']:<12.1f}")
else:
    print("Controle de mouvement : generation desactivee")
    print("\nMots-cles de mouvement pour Wan :")
    print("  - 'slow pan' : panoramique lateral")
    print("  - 'zoom in/out' : rapprochement/eloignement")
    print("  - 'aerial drone shot' : vue aerienne")
    print("  - 'tracking shot' : suivi de sujet")

--- CONTROLE DE MOUVEMENT ---
========================================

Generation 1/3 : Pan lateral
  Type de mouvement : pan
  [OK] Generation terminee en 28.5s (API ComfyUI)
     MP4 sauvegarde : wan_pan_42.mp4 (836.5 KB)


Generation 2/3 : Zoom avant
  Type de mouvement : zoom
  [OK] Generation terminee en 28.4s (API ComfyUI)
     MP4 sauvegarde : wan_zoom_43.mp4 (326.8 KB)


Generation 3/3 : Vol aerien
  Type de mouvement : drone
  [OK] Generation terminee en 28.5s (API ComfyUI)
     MP4 sauvegarde : wan_drone_44.mp4 (281.7 KB)


Mouvement       Mode                 Temps (s)   
-----------------------------------------------
  Pan lateral     API ComfyUI          28.5        
  Zoom avant      API ComfyUI          28.4        
  Vol aerien      API ComfyUI          28.5        

Exercice 2 : Analyseur de mouvements de camera

Objectif : Implementer une fonction qui genere un catalogue systématique de mouvements de camera et analyse l’efficacite de chaque mot-cle de mouvement dans un prompt.

Wan repond differemment aux mots-cles de mouvement (pan, zoom, drone, tracking). Cet exercice vise a quantifier ces différences.

Indices : - # Étape 1 : Définir le dictionnaire MOTION_KEYWORDS qui mappe chaque type de mouvement a un template de prompt - # Étape 2 : Pour chaque mouvement, construire le prompt complet en combinant le template avec la scene decrite - # Étape 3 : Mesurer la variabilite inter-frames comme indicateur de la qualite du mouvement (différence moyenne entre frames consecutives) - # Indice : utiliser np.mean(np.abs(frame1.astype(float) - frame2.astype(float))) pour mesurer la différence entre deux frames

def analyze_motion_keywords(scene_description: str) -> dict:
    """
    Analyse l'efficacite des mots-cles de mouvement sur une scene donnee.
    
    Pour chaque type de mouvement, genere le prompt complet et calcule
    un score de variabilite entre frames consecutives.
    
    Args:
        scene_description: Description de la scene de base (ex: "a mountain lake at sunrise")
    
    Returns:
        Dict avec pour chaque mouvement : prompt, variabilite_moyenne, score_mouvement
    """
    # Etape 1 : Definir les templates de mouvement
    MOTION_KEYWORDS = {}  # TODO etudiant : completer avec pan, zoom, drone, tracking
    
    # Etape 2 : Construire les prompts et (si generation disponible) calculer les metriques
    results = {}
    for motion_type, template in MOTION_KEYWORDS.items():
        # TODO etudiant : construire le prompt complet
        pass
    
    # Etape 3 : Analyser et classer les resultats
    # TODO etudiant : trier par variabilite et retourner
    pass
    
    return None  # TODO etudiant : retourner les resultats

# Test avec une scene
print("Exercice a completer")
Exercice a completer

Interpretation : Contrôle de mouvement

Les mots-cles de mouvement dans le prompt influencent la trajectoire de la camera virtuelle :

Mot-cle prompt Mouvement obtenu Efficacité
“slow pan” Panoramique lateral fluide de gauche à droite Elevee
“zoom into” Rapprochement progressif sur le sujet Bonne
“aerial drone” Vue plongeante qui survole la scene Bonne

Description visuelle : - Pan lateral: La camera se deplace lentement de gauche à droite, decouvrant progressivement le jardin japonais avec ses cerisiers en fleurs - Zoom avant: La camera s’approche du tableau, revelant les details de la peinture du chateau medieval - Vol aerien: Vue plongeante qui survole la ville cotiere, montrant l’etendue du paysage

Section 4 : Ratios d’aspect

Wan supporte différents ratios d’aspect pour s’adapter aux différentes plateformes.

# Test de differents ratios d'aspect
if run_generation:
    print("\n--- RATIOS D'ASPECT ---")
    print("=" * 40)

    aspect_prompt = "a majestic lighthouse standing on a cliff, waves crashing, dramatic sky, golden hour"

    aspect_configs = [
        {"w": 480, "h": 480, "label": "1:1 (480x480)", "name": "square"},
        {"w": 832, "h": 480, "label": "16:9 (832x480)", "name": "landscape"},
        {"w": 480, "h": 832, "label": "9:16 (480x832)", "name": "portrait"},
    ]

    aspect_results = []

    for cfg in aspect_configs:
        print(f"\nTest : {cfg['label']}")

        # Pour le mode local, on doit modifier width/height globaux
        old_width, old_height = width, height
        globals()['width'], globals()['height'] = cfg['w'], cfg['h']

        result = generate_wan_video(aspect_prompt, seed=42, save_prefix=f"wan_{cfg['name']}")

        # Restaurer les valeurs originales
        globals()['width'], globals()['height'] = old_width, old_height

        if result['success']:
            print(f"  [OK] Temps : {result['generation_time']:.1f}s ({result['mode']})")
            if result.get('video_path'):
                mp4 = Path(result['video_path'])
                print(f"     MP4 sauvegarde : {mp4.name} ({mp4.stat().st_size / 1024:.1f} KB)")
            aspect_results.append({**result, **cfg})

            # Affichage planche frames (embed image/png dans l'output cellule).
            # Meme pattern que generate_hunyuan_video de 02-1 (#11026) : 4 frames
            # equidistantes pour visualiser la composition par ratio d'aspect.
            frames = result.get('frames')
            if frames is not None and len(frames) > 0:
                n_display = min(4, len(frames))
                indices = np.linspace(0, len(frames) - 1, n_display, dtype=int)
                fig, axes = plt.subplots(1, n_display, figsize=(4 * n_display, 4))
                if n_display == 1:
                    axes = [axes]
                for j, idx in enumerate(indices):
                    axes[j].imshow(frames[idx])
                    axes[j].set_title(f"{cfg['label']} - Frame {idx+1}/{len(frames)}", fontsize=9)
                    axes[j].axis('off')
                plt.tight_layout()
                plt.show()
        else:
            print(f"  [ERROR] Erreur : {result['error']}")

    # Resume des generations
    if aspect_results:
        print(f"\n{'Ratio':<20} {'Mode':<20} {'Temps (s)':<12}")
        print("-" * 52)
        for ar in aspect_results:
            print(f"  {ar['label']:<20} {ar['mode']:<20} {ar['generation_time']:<12.1f}")
else:
    print("Test ratio d'aspect : generation desactivee")
    print("\nRatios supportes par Wan :")
    print("  1:1  (480x480) : Reseaux sociaux carre")
    print("  16:9 (832x480) : YouTube, cinema")
    print("  9:16 (480x832) : TikTok, Reels, Stories")

--- RATIOS D'ASPECT ---
========================================

Test : 1:1 (480x480)
  [OK] Temps : 14.3s (API ComfyUI)
     MP4 sauvegarde : wan_square_42.mp4 (175.4 KB)


Test : 16:9 (832x480)
  [OK] Temps : 28.5s (API ComfyUI)
     MP4 sauvegarde : wan_landscape_42.mp4 (367.1 KB)


Test : 9:16 (480x832)
  [OK] Temps : 28.6s (API ComfyUI)
     MP4 sauvegarde : wan_portrait_42.mp4 (328.1 KB)


Ratio                Mode                 Temps (s)   
----------------------------------------------------
  1:1 (480x480)        API ComfyUI          14.3        
  16:9 (832x480)       API ComfyUI          28.5        
  9:16 (480x832)       API ComfyUI          28.6        

Exercice 3 : Selecteur de configuration par plateforme

Objectif : Créer une fonction qui recommande automatiquement les paramètres optimaux (resolution, ratio, fps, duree) en fonction de la plateforme cible (YouTube, TikTok, Instagram, cinema).

Chaque plateforme impose des contraintes spécifiques que le générateur video doit respecter pour un résultat optimal.

Indices : - # Étape 1 : Définir un dictionnaire PLATFORM_SPECS avec les specs de chaque plateforme (resolution max, ratio, duree max, fps) - # Étape 2 : Implementer la logique de filtrage : si la duree demandee depasse la duree max, la reduire - # Étape 3 : Ajuster la resolution pour respecter le ratio impose par la plateforme - # Indice : YouTube = 16:9, 1080p, 60fps max ; TikTok = 9:16, 1080p, 30fps ; Instagram = 1:1 ou 9:16, 1080p, 30fps

def recommend_platform_config(platform: str, desired_duration: float = 3.0) -> dict:
    """
    Recommande les parametres optimaux pour une plateforme donnee.
    
    Args:
        platform: Nom de la plateforme ("youtube", "tiktok", "instagram", "cinema")
        desired_duration: Duree souhaitee en secondes
    
    Returns:
        Dict avec width, height, fps, max_duration, ratio, num_frames
    """
    # Etape 1 : Definir les specifications par plateforme
    # TODO etudiant : completer le dictionnaire PLATFORM_SPECS
    PLATFORM_SPECS = {}  # TODO etudiant
    
    # Etape 2 : Valider et ajuster la duree
    # TODO etudiant : reduire desired_duration si elle depasse max_duration
    
    # Etape 3 : Calculer la resolution adaptee au ratio
    # TODO etudiant : ajuster width/height selon le ratio impose
    
    return None  # TODO etudiant : retourner le dictionnaire de configuration

# Test de la fonction
print("Exercice a completer")
Exercice a completer

Interpretation : Ratios d’aspect

Ratio Usage Particularite
1:1 (carre) Instagram, prototypage Composition centree
16:9 (paysage) YouTube, cinema Composition large, horizon
9:16 (portrait) TikTok, Stories Composition verticale, sujet central

Points cles : 1. Le ratio d’aspect influence la composition automatique de la scene 2. Les paysages fonctionnent mieux en 16:9, les portraits en 9:16 3. Le temps de generation est proportionnel au nombre total de pixels (W x H)

Bonnes pratiques et prompt engineering

Structure d’un bon prompt Wan

Élément Exemple Importance
Sujet “a majestic eagle” Essentiel
Action “soaring through clouds” Essentiel
Camera “aerial tracking shot” Recommande
Eclairage “golden hour, dramatic light” Recommande
Style “cinematic, 4K, high quality” Optionnel

Prompts en francais vs anglais

Approche Avantage Inconvenient
Tout FR Naturel pour le redacteur Moins de nuances captees
Tout EN Meilleure comprehension Moins intuitif
Mixte Compromis optimal Necessite expertise bilingue

Exemple guide : Création de Vidéo Multilingue avec Wan

Durée estimée : 20-25 minutes

Objectif

Maîtriser la génération de vidéos avec Wan en utilisant des prompts dans différentes langues et en contrôlant le mouvement de caméra.

Instructions

  1. Créez un comparateur multilingue : Implémentez une fonction qui génère la même vidéo à partir de prompts en français, anglais et une autre langue (espagnol, allemand, etc.).

  2. Analysez les différences de qualité : Comparez les résultats visuels et documentez les nuances de chaque langue.

  3. Expérimentez avec les contrôles de mouvement : Testez différents types de mouvement de caméra (pan, zoom, drone, tracking).

Indices :

  • Wan comprend mieux les prompts en anglais, mais le français fonctionne bien
  • Les mots-clés de mouvement : “slow pan”, “zoom in/out”, “aerial drone shot”, “tracking shot”
  • Utilisez le même seed pour comparer équitablement les langues
  • Le ratio d’aspect influence la composition (16:9 paysage, 9:16 portrait)

Critères de succès


# Test multilingue
if run_generation:
    # Exercice: Tester avec 3 langues minimum
    base_concept = "a cat sleeping on a sofa"
    # lang_results = generate_multilingual_comparison(base_concept, ["fr", "en", "es"])
    
    # Exercice: Afficher les comparaisons côte à côte
    # Comparer visuellement les frames de chaque langue
    
    # Exercice: Tester les mouvements
    scene = "a beautiful japanese garden with cherry blossoms"
    movements = ["pan", "zoom", "drone"]
    # motion_results = test_camera_movements(scene, movements)
    
    # Exercice: Documenter les observations
    # Quelle langue donne les meilleurs résultats ?
    # Quel mouvement est le plus naturel ?
else:
    print("Test multilingue désactivé (génération non disponible)")

Ce test de generation multilingue exploite la capacite de Wan a comprendre des prompts en plusieurs langues. La cellule suivante implemente le test systématique des mots-cles de mouvement de camera pour explorer les capacites de contrôle cinematographique du modèle.

# Exercice: Fonction de test de mouvement
def test_camera_movements(base_scene: str, movements: List[str]) -> Dict[str, Any]:
    """
    Teste différents mouvements de caméra sur la même scène.
    
    Args:
        base_scene: Description de la scène (ex: "a mountain landscape")
        movements: Liste de mouvements ["pan", "zoom", "drone"]
    
    Returns:
        Dict avec les résultats pour chaque mouvement
    """
    # Templates de prompts avec mouvement
    motion_templates = {
        "pan": f"a slow pan across {base_scene}, smooth camera movement",
        "zoom": f"camera slowly zooming into {base_scene}, revealing details",
        "drone": f"aerial drone shot of {base_scene}, birds eye view, cinematic",
        "tracking": f"tracking shot following subject through {base_scene}"
    }
    
    results = {}
    
    for movement in movements:
        prompt = motion_templates.get(movement, base_scene)
        
        # Exercice: Générer avec le mouvement
        pass
        
        results[movement] = {
            "prompt": prompt,
            "frames": frames
        }
    
    return results

Le test multilingue valide la comprehension des différentes langues par le modèle. La cellule suivante implemente la fonction de contrôle de mouvement de camera qui utilisera des mots-cles spécifiques dans le prompt.

# Exercice: Fonction de génération multilingue
def generate_multilingual_comparison(base_prompt: str, languages: List[str]) -> Dict[str, Any]:
    """
    Génère la même vidéo dans plusieurs langues.
    
    Args:
        base_prompt: Concept de base (ex: "a cat sleeping")
        languages: Liste de codes langue ["fr", "en", "es", "de"]
    
    Returns:
        Dict avec les résultats pour chaque langue
    """
    # Traductions manuelles du concept
    translations = {
        "fr": "un chat dormant sur un canapé",
        "en": "a cat sleeping on a sofa",
        "es": "un gato durmiendo en un sofá",
        "de": "eine Katze schlafft auf einem Sofa"
    }
    
    results = {}
    
    for lang in languages:
        prompt = translations.get(lang, base_prompt)
        
        # Exercice: Générer avec generate_wan_video()
        # Utiliser le même seed pour toutes les langues
        pass
        
        results[lang] = {
            "prompt": prompt,
            "frames": frames,
            "time": gen_time
        }
    
    return results

Les fonctions multilingues et de mouvement implementees, la cellule suivante consolide les statistiques de session et presente un bilan des generations effectuees avec les temps de traitement observes.

# Statistiques de session et prochaines etapes
print("\n--- STATISTIQUES DE SESSION ---")
print("=" * 50)

print(f"Date : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Mode d'execution : {mode_str}")
print(f"Modele : {model_id if not use_api else 'wan2.1_t2v_1.3B (ComfyUI)'}")
print(f"Parametres : {num_frames} frames, {num_inference_steps} steps, CFG={guidance_scale}")
print(f"Resolution : {width}x{height}")

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)[:10]:
        size_kb = f.stat().st_size / 1024
        print(f"  {f.name} ({size_kb:.1f} KB)")
    if len(generated_files) > 10:
        print(f"  ... et {len(generated_files) - 10} autres fichiers")

print(f"\n--- PROCHAINES ETAPES ---")
print(f"1. Notebook 02-4 : SVD - Stable Video Diffusion (animation d'images statiques)")
print(f"2. Module 03-1 : Comparaison benchmark de tous les modeles")
print(f"3. Module 03-2 : Orchestration de pipelines text -> image -> video")
print(f"4. Tester Wan 2.2 quand disponible pour les ameliorations de qualite")

print(f"\nNotebook 02-3 Wan Video Generation termine - {datetime.now().strftime('%H:%M:%S')}")

--- STATISTIQUES DE SESSION ---
==================================================
Date : 2026-08-20 14:13:27
Mode d'execution : API ComfyUI
Modele : wan2.1_t2v_1.3B (ComfyUI)
Parametres : 16 frames, 20 steps, CFG=6.0
Resolution : 832x480

Fichiers generes (8) :
  wan_drone_44.mp4 (281.7 KB)
  wan_en_42.mp4 (107.4 KB)
  wan_fr_42.mp4 (77.3 KB)
  wan_landscape_42.mp4 (367.1 KB)
  wan_pan_42.mp4 (836.5 KB)
  wan_portrait_42.mp4 (328.1 KB)
  wan_square_42.mp4 (175.4 KB)
  wan_zoom_43.mp4 (326.8 KB)

--- PROCHAINES ETAPES ---
1. Notebook 02-4 : SVD - Stable Video Diffusion (animation d'images statiques)
2. Module 03-1 : Comparaison benchmark de tous les modeles
3. Module 03-2 : Orchestration de pipelines text -> image -> video
4. Tester Wan 2.2 quand disponible pour les ameliorations de qualite

Notebook 02-3 Wan Video Generation termine - 14:13:27

Conclusion

Ce notebook a permis d explorer les aspects essentiels de 02 3 wan video generation. Les points cles :

  • Les concepts fondamentaux ont ete presentes et illustres
  • Les exercices proposent une mise en pratique progressive
  • Les résultats obtenus permettent de valider la comprehension

Pour aller plus loin : approfondir les aspects avances du sujet et explorer les liens avec d autres domaines.

Retour au sommet