درس 1 — Cadrer

Du brief au kit de marque

50 min

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

La responsable marketing d'Ondée envoie un brief de neuf lignes : le produit, la cible, une promesse chiffrée, la date de lancement, et six expressions interdites par la loi AGEC (« écologique », « biodégradable »…). Si chaque visuel part de ce brief brut, chaque appel au modèle réinvente une palette, une police et un ton. Le premier livrable du studio n'est donc pas une image : c'est un kit de marque, un objet structuré que tout le reste du code lit.

Le mécanisme

Le kit est un modèle Pydantic. Le SDK google-genai le transmet comme response_schema : le modèle ne peut répondre qu'avec ces champs, et response.parsed renvoie directement l'objet typé. Deux décisions rendent ce kit exploitable par le code.

  1. Les polices sont une énumération fermée. Le modèle choisit parmi les fichiers présents dans fonts/ et dont la licence (SIL OFL 1.1) autorise l'incrustation dans une image commerciale. Une police que vous n'avez pas ne peut pas sortir du schéma.
  2. Le code a le dernier mot sur le contraste. Le modèle propose une couleur de texte et une couleur de fond ; normaliser() mesure leur contraste WCAG et remplace la couleur de texte si elle passe sous 4,5:1.
flowchart LR
  B["briefs/ondee.json"] --> P["Prompt + schéma BrandKit"]
  P --> M["gemini-3.5-flash-lite"]
  M --> K["BrandKit typé"]
  K --> N["normaliser()"]
  N -- "rôle manquant, hex invalide,<br/>contraste < 4,5" --> C["corrections listées"]
  N --> S["kit utilisable par le code"]

La mise en œuvre

Le schéma, extrait de kit.py :

files/studio/kit.py
POLICES = {
    "DM Serif Display": {"regular": "DMSerifDisplay-Regular.ttf", "bold": "DMSerifDisplay-Regular.ttf"},
    "Inter": {"regular": "Inter-Regular.ttf", "bold": "Inter-Bold.ttf"},
}
PoliceTitre = Literal["DM Serif Display", "Inter"]
PoliceTexte = Literal["Inter"]

class Couleur(BaseModel):
    nom: str
    hex: str = Field(description="couleur au format #RRGGBB")
    role: Literal["primaire", "secondaire", "accent", "fond", "texte"]

class BrandKit(BaseModel):
    marque: str
    palette: list[Couleur] = Field(description="exactement 5 couleurs, une par rôle : ...")
    police_titre: PoliceTitre
    police_texte: PoliceTexte
    ton: list[str]
    regles: list[str]
    mots_interdits: list[str]
    direction_photo: str

L'appel passe par une seule méthode de models.py, réutilisée pour tous les objets structurés du projet :

files/studio/models.py
r = _relancer(lambda: self.client.models.generate_content(
    model=self.s.text_model, contents=contenu,
    config=types.GenerateContentConfig(
        response_mime_type="application/json", response_schema=schema, temperature=0.4),
))
self._noter(nature, self.s.text_model, r.usage_metadata)   # coût réel, depuis usage_metadata
if r.parsed is None:
    raise ValueError(f"réponse non conforme au schéma {schema.__name__}")
return r.parsed

Puis le code reprend la main. Les mots interdits du brief sont fusionnés d'office (le modèle peut en oublier), et le contraste est vérifié :

files/studio/kit.py
fond, texte = par_role["fond"].hex, par_role["texte"].hex
if qc.contraste(fond, texte) < 4.5:
    meilleur = max(("#111111", "#FFFFFF"), key=lambda h: qc.contraste(fond, h))
    corrections.append(f"texte {texte} sur fond {fond} : ... remplacé par {meilleur}")
    par_role["texte"].hex = meilleur

Les corrections ne sont pas silencieuses : elles sont enregistrées dans le manifeste et affichées dans la galerie, sous « Corrigé par le code ».

Vérifiez le comportement sans clé :

make setup
make test        # dont test_normaliser_corrige_un_texte_illisible et test_police_hors_catalogue_refusee

Résultat obtenu

Exécution réelle du 2026-10-15 (make e2e), kit produit pour Ondée en 1,6 s pour 0,0016 $ :

kit prêt : primaire #8C4A2F « Ambre Chaud », secondaire #D9CDBF, accent #B85D3B,
           fond #F7F5F0 « Blanc Cassé », texte #2B1D16 « Brun Profond », 0 correction(s)
police_titre : DM Serif Display    police_texte : Inter
mots_interdits : biodégradable, green, magique, miracle, neutre en carbone,
                 respectueux de l'environnement, révolutionnaire, zéro déchet, écologique

Le contraste texte sur fond vaut 14,9:1. Le modèle a ajouté trois mots interdits de lui-même (« magique », « miracle », « révolutionnaire ») : utile, et inoffensif puisque la liste ne fait que s'allonger.

Ce qui casse en production

SymptômeCauseParade
La galerie affiche une règle « le bloc marque occupe 15 % de la largeur, aligné à gauche » alors que le logo est en haut à droiteobservé au premier essai : le modèle invente des règles de mise en page que le code n'applique pasle prompt limite les règles à la photo et à la rédaction ; la mise en page appartient au code. Au second essai, les règles sont devenues « un seul flacon par visuel », « aucune personne reconnaissable »
Un titre en Helvetica dans un visuelpolice choisie en texte libre, remplacée en silence par une police systèmeénumération fermée dans le schéma ; Pydantic refuse le reste
Texte beige sur fond lin, illisiblele modèle juge la couleur, pas le contrastenormaliser() mesure et remplace
ValueError: réponse non conforme au schémaréponse tronquée (limite de tokens) ou refusrelance unique, puis échec explicite : pas de kit par défaut inventé

La règle de décision

Faites produire au modèle ce qui relève du goût (nuances, ton, direction photo) et gardez dans le code ce qui se mesure ou se licencie (contraste, polices, mots interdits). Surveillez le nombre de corrections par kit : plus d'une correction par campagne signale un prompt à revoir.

Pratique

Le kit d'une nouvelle marque arrive avec texte #6E6E6E sur fond #FFFFFF (4,7:1). Le directeur artistique veut un gris plus clair, #8A8A8A (3,4:1), « plus élégant ». Que fait le studio ?

Vous voulez proposer au modèle la police « Futura PT » que l'agence utilise. Quelle est la bonne démarche ?

Exercice. Ajoutez une règle : la couleur d'accent ne doit pas être trop proche de la primaire (distance CIE76 inférieure à 15), sinon elle est remplacée par la secondaire et la correction est enregistrée. Écrivez le test d'abord.

Voir la solution
# kit.py, dans normaliser(), après la boucle des rôles
from .qc import _lab, _rgb
import numpy as np

p, a = par_role["primaire"].hex, par_role["accent"].hex
if np.linalg.norm(_lab(_rgb(p)) - _lab(_rgb(a))) < 15:
    corrections.append(f"accent {a} trop proche de la primaire {p}, remplacé par la secondaire")
    par_role["accent"].hex = par_role["secondaire"].hex

# tests/test_kit_et_textes.py
def test_accent_trop_proche_de_la_primaire():
    k = kit_exemple()
    k.palette[2].hex = "#9B4B14"  # quasi identique à #9A4A12
    kit, corrections = normaliser(k)
    assert kit.couleur("accent") == kit.couleur("secondaire")
    assert any("trop proche" in c for c in corrections)
  • Je sais contraindre une sortie de modèle avec un schéma Pydantic et une énumération fermée
  • Je sais séparer ce que le modèle propose de ce que le code impose
  • Je sais calculer un contraste WCAG et corriger une palette
  • make test est vert sur ma machine
التالي