درس 5 — Construire le générateur

Formats, post-production, planche contact

9 دقائق قراءة

هذا المحتوى غير مترجم بعد. تُعرض النسخة الأصلية.

Formats, post-production, planche contact

Le visuel validé en 16:9 part sur Instagram en 4:5 : la plateforme coupe le produit à moitié. Le flyer imprimé sort flou parce qu'une image de 1 376 pixels a été étirée sur un A5 à 300 dpi. Le titre, écrit par le modèle, comporte une faute que personne n'a vue avant l'impression. Aucun de ces défauts ne vient de la génération : ils viennent de tout ce qui se passe après. Un studio les traite par un outil, pas par l'attention de quelqu'un un vendredi soir.

Le mécanisme

Chaque canal impose des dimensions de livraison. Le modèle, lui, ne produit qu'une liste fermée de ratios. Le pipeline choisit donc le ratio supporté le plus proche du premier canal du visuel, génère, puis recadre pour chaque canal autour d'un point focal et redimensionne aux dimensions exactes.

Canal (canaux.py)LivraisonRatio demandé au modèleTaille minimale sans agrandir
linkedin_post1200 × 62716:91K
linkedin_carre1200 × 12001:12K
instagram_feed1080 × 13504:52K
instagram_story1080 × 19209:162K
site_hero1920 × 108016:92K
site_vignette800 × 6004:31K
print_a51748 × 2480 (300 dpi)2:34K

Vérifiez ces dimensions dans les guides des plateformes avant une campagne payante : elles changent. Pour l'impression, ajoutez le fond perdu demandé par l'imprimeur (souvent 3 mm par côté).

flowchart LR
  G["Image générée<br/>ratio du 1er canal"] --> A{"Agrandissement<br/>> seuil ?"}
  A -->|"oui"| W["Alerte : générer en taille supérieure"]
  A --> C["Recadrage au ratio du canal<br/>autour du point focal"]
  C --> Z["Redimensionnement exact"]
  Z --> T["Titre + logo composés<br/>(le titre ne chevauche jamais le logo)"]
  T --> J["JPEG qualité 80 (web) ou 92 (print)<br/>dpi + XMP IPTC"]
  J --> H["planche.html + manifest.json"]

La mise en œuvre

Ratio, agrandissement, recadrage

files/campagne/canaux.py (extrait)
def ratio_generation(canal: str) -> str:
    """Ratio supporté le plus proche du canal (écart logarithmique, symétrique portrait/paysage)."""
    cible = CANAUX[canal].ratio
    return min(RATIOS_1K, key=lambda r: abs(math.log((RATIOS_1K[r][0] / RATIOS_1K[r][1]) / cible)))


def agrandissement(source: tuple[int, int], canal: str) -> float:
    """Facteur d'agrandissement imposé par le recadrage : > 1 signifie des pixels inventés."""
    c = CANAUX[canal]
    l, h = source
    if l / h > c.ratio:
        return c.hauteur / h
    return c.largeur / l

L'écart logarithmique traite de la même façon un ratio deux fois trop large et deux fois trop haut ; une simple différence favoriserait les formats paysage. Le pipeline alerte au-delà de 1,0 pour l'impression (aucun pixel inventé toléré) et de 1,5 pour le web.

Le visuel orbe-salon illustre la limite : généré en 4:5 1K (928 × 1152), il livre le feed Instagram avec un agrandissement de 1,17, et la story 9:16 avec 1,67, au-delà du seuil. L'alerte de la planche propose la parade : générer en 2K.

Livrable instagram_feed, 1080 × 1350 (agrandissement 1,17)
Livrable instagram_story, 1080 × 1920, tiré de la même image (agrandissement 1,67 : alerte)

Titre et logo hors du modèle

files/campagne/postprod.py (extrait)
if texte:
    dispo = l - 2 * marge - largeur_logo
    taille = round(bande * 0.34)
    police = _police(taille)
    while police.getlength(texte) > dispo and taille > 10:   # le titre ne chevauche jamais le logo
        taille -= 2
        police = _police(taille)

Cette boucle existe parce que la première version a échoué sur le flyer : en format portrait, le titre « Orbe. Salon du design, Lyon » passait sous le logo. Le test test_titre_long_ne_chevauche_pas_le_logo compare désormais la zone du logo avec et sans titre ; elle doit être identique au pixel près.

Flyer A5 livré : 1748 × 2480 à 300 dpi, titre réduit pour laisser la place au logo. La boîte à lumière en haut est un défaut de prompt, corrigé à la leçon 4

Les métadonnées

Chaque JPEG livré porte un paquet XMP avec le champ IPTC DigitalSourceType : trainedAlgorithmicMedia pour une image purement générée, compositeWithTrainedAlgorithmicMedia quand un titre ou un logo a été ajouté. Ce vocabulaire est celui que lisent les banques d'images et plusieurs réseaux sociaux pour afficher une mention « IA ».

files/campagne/postprod.py (extrait)
image.convert("RGB").save(chemin, "JPEG", quality=c.qualite_jpeg, optimize=True,
                          dpi=(c.dpi, c.dpi), xmp=xmp(description, compose))

En inspectant les fichiers bruts renvoyés par l'API le 2026-10-15, on trouve davantage : chaque image porte un manifeste C2PA (Content Credentials) signé par Google, et les PNG renvoyés par Vertex AI contiennent déjà le champ IPTC. Tout ré-encodage (recadrage, titre) supprime ce manifeste : on ne peut pas le re-signer sans certificat. Le pipeline le détecte et l'écrit dans les alertes ; les fichiers bruts etape-N restent dans sorties/ comme preuve d'origine. Le filigrane invisible SynthID, lui, est inscrit dans les pixels et conçu par Google DeepMind pour résister au recadrage et à la compression.

La planche contact

planche.html est un fichier autonome (aucune dépendance) : vignette du premier canal, modèle, taille, durée, coût, alertes, liens vers chaque livrable, prompt complet et étapes de retouche dans un volet, grille de validation à cocher. manifest.json contient les mêmes données pour un traitement automatique.

Fiche d'un visuel ouverte : prompt, étapes de retouche avec leur coût, contrôles de validation
La planche en mode sombre, sur mobile

Résultat obtenu

make e2e, 2026-10-15 (clé d
20:59:19 INFO gemini-3.1-flash-image : image en 5.7 s, 0.0460 USD
20:59:26 INFO gemini-3.1-flash-image : image en 6.3 s, 0.0458 USD
{
 "modele": "gemini-3.1-flash-image",
 "etapes": [
  {"consigne": "(génération initiale)", "secondes": 5.7, "usd": 0.04599},
  {"consigne": "Allume la lampe : halo ambré chaud. Ne modifie rien d'autre.", "secondes": 6.3, "usd": 0.04583}
 ],
 "livrables": [
  {"canal": "linkedin_post", "fichier": "orbe-test-linkedin_post.jpg", "agrandissement": 1.74},
  {"canal": "instagram_feed", "fichier": "orbe-test-instagram_feed.jpg", "agrandissement": 3.52}
 ],
 "alertes": [
  "livrables sans manifeste C2PA (ré-encodés) : archivez le brut comme preuve d'origine",
  "linkedin_post : agrandi x1.74, générez en taille supérieure (1K)",
  "instagram_feed : agrandi x3.52, générez en taille supérieure (2K)"
 ],
 "cout_usd": 0.0918,
 "duree_totale_s": 12.4
}
E2E OK

Le même test sur Vertex AI (GOOGLE_GENAI_USE_VERTEXAI=TRUE, location global) a coûté 0,091 $ ; la retouche y a pris 86,3 s au lieu de 6 s, d'où le délai HTTP explicite. Les alertes sont voulues : le test génère en 512 px pour coûter peu, et le brief de campagne, lui, n'en déclenche que deux (story Instagram x1,67 et flyer A5 x1,03).

Ce qui casse en production

SymptômeCauseParade
Produit coupé sur la story Instagramun seul ratio généré pour des canaux opposés (4:5 et 9:16)point focal dans le brief ; pour deux formats très différents, deux visuels
Impression floueimage 1K ou 2K étirée à 300 dpialerte d'agrandissement ; générer le print en 4K (0,151 enFlash,0,24en Flash, 0,24 en Pro)
Titre sous le logo, ou coupélongueur de titre non contrôléeréduction de corps automatique, testée ; alerte au-delà de 6 mots
La plateforme n'affiche pas la mention IAmétadonnées perdues au ré-encodageXMP IPTC réécrit à l'export ; mention visible dans la publication de toute façon
400 API key not valid alors que la clé est exportée dans le terminalmake charge .env (valeur factice copiée par make setup) et l'exporte par-dessus la variable du shellrenseigner la vraie clé dans .env, ou lancer .venv/bin/python tests/e2e_reel.py directement
Fichiers de 2 à 6 Mo envoyés au sitebrut publié tel quel (PNG chez Vertex AI)livrables JPEG qualité 80 (92 pour le print) : 30 à 390 ko mesurés ici

La règle de décision

Générez au ratio du canal le plus exigeant et déclinez par recadrage tant que l'agrandissement reste sous 1,5 (web) ou 1,0 (print). Au-delà, générez un visuel de plus, au bon ratio : 7 centimes coûtent moins qu'un flyer réimprimé. Gardez les bruts : ce sont eux qui portent la preuve d'origine.

Pratique

Un visuel doit sortir en site_hero (16:9) et en instagram_story (9:16). Le pipeline génère en 16:9, 2K. Quel est le problème, et que faites-vous ?

Votre juriste demande comment prouver qu'un visuel publié est une image générée par Gemini. Que lui transmettez-vous ?

Exercice. Ajoutez un canal linkedin_banniere (bannière de page entreprise, 1128 × 191 dans cet exercice). Quel ratio le pipeline choisira-t-il, quelle taille minimale, et quel test écrivez-vous ?

Voir la solution
"linkedin_banniere": Canal(1128, 191, "bannière de page entreprise"),

Le ratio 5,9:1 est le plus proche de 21:9 parmi les ratios communs aux deux modèles (le Flash accepte aussi 4:1 et 8:1, mais pas le Pro, d'où leur absence de RATIOS_1K). En 21:9 1K (1584 × 672), le recadrage garde toute la largeur : agrandissement 1128 / 1584 = 0,71, donc 1K suffit.

def test_banniere():
    assert canaux.ratio_generation("linkedin_banniere") == "21:9"
    assert canaux.taille_minimale("linkedin_banniere") == "1K"

La composition, elle, doit l'anticiper : bande horizontale très fine, sujet au centre ou en bord, sans élément important en haut ni en bas.

  • Je sais quel ratio demander pour chaque canal et quand générer deux visuels
  • Je lis l'alerte d'agrandissement et je choisis la taille en conséquence
  • Je compose titre et logo hors du modèle
  • J'archive les bruts (C2PA) et j'écris le XMP IPTC sur les livrables
  • make e2e est vert chez moi
التالي