Leçon 2 — Produire

Des visuels cohérents avec une image de référence

1h

Quatre appels indépendants au modèle d'image avec le même prompt donnent quatre flacons différents : une forme d'épaule qui change, un verre plus ou moins foncé, une lumière d'après-midi puis de matin. Sur un fil LinkedIn, le lecteur voit quatre produits. La cohérence ne vient pas du prompt : elle vient d'une image de référence générée une fois et donnée en entrée à chaque déclinaison.

Le mécanisme

sequenceDiagram
  participant S as Studio
  participant P as gemini-3-pro-image
  participant F as gemini-3.1-flash-image
  S->>P: prompt (kit, direction photo), 4:5
  P-->>S: hero.jpg (référence)
  loop 4 formats, une conversation par format
    S->>F: [hero.jpg] + composition + ratio
    F-->>S: fond sans texte
    S->>S: calques + contrôle
    opt texte parasite détecté
      S->>F: « retire le texte » (même conversation)
      F-->>S: fond corrigé
    end
  end

Trois choix :

  • L'image de référence est faite par le modèle Pro (0,14 ),lesdeˊclinaisonsparFlash(0,068), les déclinaisons par Flash (0,068 ). La référence porte la qualité des matières ; les déclinaisons la recopient. La documentation indique jusqu'à 10 images d'objet en référence pour gemini-3.1-flash-image : une seule suffit ici.
  • Chaque format ouvre une conversation (client.chats.create). L'édition multi-tours est la méthode recommandée par la documentation pour itérer sur une image : le modèle garde le contexte du tour précédent au lieu de repartir de zéro.
  • Le prompt décrit où laisser de l'air, pas le texte. « Produit à droite, la moitié gauche calme et unie » prépare la place que le code remplira.

La mise en œuvre

Les formats et leur ratio de génération, dans layout.py :

files/studio/layout.py
FORMATS = {
    "linkedin": Format(1200, 627, "16:9", "gauche", "LinkedIn, image de post"),
    "instagram_portrait": Format(1080, 1350, "4:5", "bas", "Instagram, post portrait"),
    "story": Format(1080, 1920, "9:16", "bas", "Instagram et LinkedIn, story", zone_sure=0.14),
    # 4:1 renvoie souvent un triptyque répété : on génère en 21:9 et on recadre.
    "banniere": Format(1584, 396, "21:9", "gauche", "LinkedIn, bannière de page"),
}

La génération d'un format, dans pipeline.py :

files/studio/pipeline.py
fond = self.m.image(
    f"Même produit, même lumière et mêmes matières que l'image de référence. "
    f"Nouvelle composition pour {fmt.usage}, une seule scène continue (pas de panneaux répétés) : "
    f"produit {place} pour accueillir du texte ajouté plus tard. {SANS_TEXTE}",
    refs=[hero], aspect=fmt.aspect, session=self._session(nom),
)

Et l'édition, qui réutilise la conversation si elle est en mémoire, ou en ouvre une à partir de l'image courante (cas de la galerie après un redémarrage du serveur) :

files/studio/models.py
def editer(self, session, consigne, image=None, aspect="1:1") -> bytes:
    self._garde_budget()                      # MAX_IMAGES : plafond dur par processus
    if session not in self._chats:
        chat = self.client.chats.create(model=self.s.image_model, config=config)
        self._chats[session] = (chat, self.s.image_model)
        message = [types.Part.from_bytes(data=image, mime_type=_mime(image)), consigne]
    else:
        message = consigne                    # le modèle a déjà l'image du tour précédent
    chat, model = self._chats[session]
    r = _relancer(lambda: chat.send_message(message))

Le coût de chaque appel est calculé à partir de usage_metadata (tokens d'entrée, tokens image en sortie à 60 $ le million pour Flash) et non estimé :

make campagne     # ou make e2e ; le journal affiche le coût de chaque appel

Résultat obtenu

Exécution réelle du 2026-10-15 :

20:43:58 INFO studio.models image gemini-3-pro-image coût=0.1401 $
20:44:08 INFO studio.models image gemini-3.1-flash-image coût=0.0686 $
20:44:11 INFO studio.pipeline linkedin v1 : contraste 4.91, palette 10.5, texte parasite False
20:44:20 INFO studio.models image gemini-3.1-flash-image coût=0.0684 $
20:44:23 INFO studio.pipeline instagram_portrait v1 : contraste 13.20, palette 10.0, texte parasite False
20:44:31 INFO studio.models image gemini-3.1-flash-image coût=0.0684 $
20:44:35 INFO studio.pipeline story v1 : contraste 11.94, palette 9.4, texte parasite False
20:44:44 INFO studio.models image gemini-3.1-flash-image coût=0.0685 $
20:44:46 INFO studio.pipeline banniere v1 : contraste 6.01, palette 22.3, texte parasite False

Environ 9 s par image, même flacon, même pierre, même lumière sur les quatre formats (voir la planche en page d'accueil).

Deux éditions multi-tours réelles depuis la galerie sur la story : « ajoute une étiquette kraft avec le nom de la marque », puis « retire complètement l'étiquette ». Le modèle a ajouté une étiquette vierge : regenerer() termine toujours la consigne humaine par « Aucun texte, aucune lettre… », et cette contrainte l'a emporté sur « le nom de la marque en grandes lettres ». Au tour suivant, il a retiré l'étiquette sans toucher au cadrage. Coût de chaque tour : 0,068 $, 15 s.

Story v1, v2 avec étiquette vierge, v3 après retrait : même cadrage sur les trois tours

Ce qui casse en production

Le triptyque. Une bannière demandée directement en 4:1 (essai réel du 2026-10-15, 2064x512 pixels) revient avec le même flacon répété trois fois, comme trois photos collées. Le modèle compose mal un format aussi allongé.

Bannière générée en 4:1 : trois fois la même scène

Parade : générer dans le ratio disponible le plus proche qui compose encore une scène unique (21:9), écrire « une seule scène continue » dans le prompt, et laisser ImageOps.fit recadrer au format exact.

Autre symptômeCauseParade
RuntimeError: le modèle n'a pas renvoyé d'image (...), avec la raison de fin renvoyée par l'APIfiltre de sécurité, souvent sur des personnes ou des marques réellesle brief interdit les personnes reconnaissables ; l'erreur remonte en galerie avec sa raison
Facture qui double un vendredi soirune boucle de régénération sans finMAX_IMAGES (16 par défaut) lève BudgetDepasse ; MAX_TOURS_QC borne les corrections automatiques à 2
Après un redémarrage de la galerie, une régénération « oublie » les tours précédentsles conversations vivent en mémoire du processusediter() rouvre une conversation à partir de l'image courante ; pour un historique durable, stocker les tours avec le manifeste
429 RESOURCE_EXHAUSTEDquota par minute des modèles d'image_relancer attend 2, 4, 8 s puis abandonne

La règle de décision

Une image de référence par campagne, générée avec le meilleur modèle ; des déclinaisons avec le modèle rapide, en conversation. Ne demandez jamais au modèle un ratio extrême : demandez le ratio le plus proche qui reste une photo crédible, et recadrez. Surveillez le coût moyen par visuel publié (régénérations comprises), pas le coût par appel.

Pratique

La story de la campagne montre un flacon plus clair que sur les trois autres formats. Que modifiez-vous en premier ?

Un client demande une bannière 1584x396 et une couverture d'événement 1776x444. Combien d'images générez-vous au minimum ?

Exercice. Ajoutez un format « carré » 1080x1080 pour Instagram, texte en bas. Quelles lignes changent ? Quel est son coût marginal par campagne ?

Voir la solution

Une ligne dans FORMATS :

"carre": Format(1080, 1080, "1:1", "bas", "Instagram, post carré"),

Puis un champ carre: Accroche dans Accroches (textes.py), sinon getattr(self.textes.accroches, nom) échoue. Rien d'autre : la boucle de lancer(), la galerie et le contrôle qualité parcourent FORMATS. Coût marginal : une image Flash (≈ 0,068 )etuncontro^letexte(≈0,0004) et un contrôle texte (≈ 0,0004 ), plus 0,068 $ par tour d'édition éventuel.

  • Je sais utiliser une image de référence pour garder un produit cohérent entre formats
  • Je sais ouvrir une conversation d'image et la réutiliser pour des éditions
  • Je sais pourquoi on ne génère pas directement en 4:1
  • Je sais plafonner le nombre d'images d'un processus
Suivant