درس 5 — Contrôler et publier

Contrôle qualité automatique

50 min

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

Un lundi, une story part avec « LESSlVE » écrit sur le flacon, un L minuscule à la place du I, dessiné par le modèle d'image et passé inaperçu à la relecture sur téléphone. Trois commentaires moqueurs plus tard, le post est retiré. Le relecteur humain reste indispensable, mais il ne doit voir que des visuels qui ont déjà passé les contrôles qu'une machine fait mieux que lui : mesurer un contraste, comparer des couleurs, repérer une lettre dans un coin.

Le mécanisme

Trois contrôles par visuel, chacun avec un seuil et une valeur enregistrée :

ContrôleMéthodeSeuilCoût
Contraste du texteWCAG 2.x, sur le rendu final, au percentile (leçon précédente)≥ 4,5:10
Respect de la palette6 couleurs dominantes (quantification), distance CIE76 à la couleur de palette la plus proche, pondérée par la surface≤ 280
Texte parasitele modèle de texte regarde le fond (sans calques) et répond selon un schéma {contient_texte, elements}aucun texte≈ 0,0004 $
flowchart TD
  F["Fond généré"] --> P["distance_palette()"]
  F --> X["texte_parasite() : gemini-3.5-flash-lite + schéma"]
  R["Rendu avec calques"] --> C["contraste mesuré à la mise en page"]
  P --> A{"les 3 contrôles passent ?"}
  X --> A
  C --> A
  A -- "texte détecté" --> E["édition : « retire tout texte (éléments) »<br/>max 2 tours"]
  E --> F
  A -- "oui" --> G["à valider en galerie"]
  A -- "non, après 2 tours" --> H["à valider, contrôle en rouge :<br/>décision humaine"]

Le détecteur regarde le fond avant les calques : sinon il signalerait le titre que le code vient de poser. Et la correction ne se fait pas en régénérant : elle se fait en éditant, dans la conversation du format, avec la liste des éléments que le détecteur a vus.

La mise en œuvre

Le contrôle de texte parasite, dans qc.py :

files/studio/qc.py
class TexteParasite(BaseModel):
    contient_texte: bool = Field(description="vrai si l'image contient des lettres, chiffres, mots, logos ou étiquettes lisibles")
    elements: list[str] = Field(description="liste courte de ce qui est lisible et où, vide sinon")

PROMPT_TEXTE = ("Examine cette image publicitaire. Contient-elle du texte généré DANS l'image : lettres, mots, chiffres, "
                "pseudo-écriture, étiquette ou logo dessinés ? Ignore les formes abstraites. Réponds selon le schéma.")

La boucle de correction, dans pipeline.py :

files/studio/pipeline.py
for _ in range(self.s.max_tours_qc):
    a = self.manifest["assets"][nom]
    if a["qc"]["texte_parasite"]["ok"]:
        return
    elements = "; ".join(a["qc"]["texte_parasite"]["elements"]) or "texte détecté"
    consigne = f"Retire tout texte, lettre, chiffre, étiquette ou logo visible ({elements}). Ne change rien d'autre ..."
    fond = self.m.editer(self._session(nom), consigne, image=..., aspect=FORMATS[nom].aspect)
    self._nouvelle_version(nom, fond, consigne="QC auto : " + consigne)

Un détecteur qui ne détecte jamais rien ressemble exactement à un détecteur qui fonctionne. Sur les quatre visuels de la campagne, aucun texte n'est apparu : la boucle n'a donc pas tourné. Pour prouver qu'elle tourne, le studio a une sonde : il imprime lui-même une fausse étiquette « LESSIVE / 40 lavages » sur un fond propre, demande au modèle s'il voit du texte, demande l'effacement, et recontrôle.

make test                                   # test_campagne_complete_avec_correction_auto_du_texte_parasite (faux modèle)
.venv/bin/python -m studio sonde-qc         # la sonde, avec les vrais modèles (≈ 0,07 $)
Sonde réelle : étiquette imprimée par le code, détectée, puis effacée en un tour d'édition

Résultat obtenu

Trois exécutions réelles de la sonde le 2026-10-15 (Gemini API deux fois, Vertex AI une fois), même résultat :

sonde QC : étiquette piégée détectée=True ['LESSIVE', '40 lavages']  fond propre détecté=False  après édition détecté=False

Et les contrôles des quatre visuels du second test de bout en bout :

linkedin              1200x627  v1  contraste  8.68:1  palette   9.8  texte parasite non  QC OK
instagram_portrait   1080x1350  v1  contraste 13.34:1  palette  15.5  texte parasite non  QC OK
story                1080x1920  v1  contraste 12.28:1  palette  15.4  texte parasite non  QC OK
banniere              1584x396  v1  contraste  8.72:1  palette  10.3  texte parasite non  QC OK

Trois sondes concluantes ne mesurent pas un taux de détection. Elles prouvent que la chaîne est branchée. Pour mesurer le rappel, il faut un jeu de 50 à 100 fonds annotés à la main, dont une partie avec du texte réel du modèle.

Ce qui casse en production

SymptômeCauseParade
Une pseudo-écriture minuscule sur un bouchon passe le contrôlel'image est envoyée entière ; quelques pixels de texte ne pèsent rienpour les formats verticaux, envoyer aussi deux moitiés recadrées ; le prompt mentionne explicitement « pseudo-écriture »
Le détecteur signale le titre comme texte parasitecontrôle fait sur le rendu au lieu du fondcontrôler fond, jamais rendu
Écart à la palette de 22,3 sur la bannière du premier essai, contre 10 ailleursfond rosé, plus saturé que la palette crèmele seuil (28) laisse passer ; la valeur reste visible en galerie pour l'humain. Le seuil est empirique : recalibrez-le sur 20 visuels jugés par la marque
Boucle d'édition qui ne converge pasle modèle redessine du texte à chaque tourMAX_TOURS_QC=2, puis contrôle en rouge et décision humaine ; jamais de boucle sans borne

La règle de décision

Automatisez les contrôles mesurables et bornez les corrections automatiques. Ne transformez jamais un contrôle en blocage silencieux : un visuel qui échoue arrive en galerie avec son contrôle en rouge et la raison, et c'est un humain qui tranche. Surveillez le taux de visuels qui passent du premier coup ; s'il baisse après une mise à jour de modèle, la sonde dira si le détecteur ou le générateur a changé.

Pratique

Après une mise à jour du modèle de texte, 100 % des visuels passent le contrôle texte parasite pendant une semaine, contre 85 % avant. Que faites-vous ?

Le contrôle palette échoue (distance 41) sur un visuel que la responsable de marque trouve parfait. Quelle décision ?

Exercice. Ajoutez un quatrième contrôle : la netteté du fond (un fond flou trahit souvent un agrandissement). Mesurez la variance du laplacien sur l'image en niveaux de gris et refusez sous un seuil.

Voir la solution
# qc.py
from PIL import ImageFilter

def nettete(img: Image.Image) -> float:
    g = img.convert("L").resize((512, int(512 * img.height / img.width)))
    lap = np.asarray(g.filter(ImageFilter.Kernel((3, 3), [0, 1, 0, 1, -4, 1, 0, 1, 0], scale=1, offset=128)), dtype=float)
    return round(float(lap.var()), 1)

# dans controler()
n = nettete(img)
res["nettete"] = {"valeur": n, "ok": bool(n >= SEUIL_NETTETE)}

Calibrez SEUIL_NETTETE sur les fonds réels de vos campagnes (mesurez-en une vingtaine, nets et flous), puis ajoutez une ligne dans la carte de la galerie. Test : une image effect_noise doit passer, la même image floutée par GaussianBlur(8) doit échouer.

  • Je sais pourquoi on contrôle le fond et non le rendu pour le texte parasite
  • Je sais mesurer l'écart d'une image à une palette
  • Je sais borner une boucle de correction automatique
  • Je sais prouver qu'un détecteur fonctionne avec une sonde
التالي