Lección 3 — Produire

Texte et logo en calques

50 min

Este contenido aún no está traducido. Se muestra la versión original.

Le service juridique d'Ondée demande, la veille du lancement, de remplacer « 0,19 € le lavage » par « 0,19 € le lavage, prix constaté en magasin ». Si le prix est dessiné dans l'image par le modèle, il faut régénérer les quatre visuels, revalider, et espérer que le modèle écrive juste. Si le texte est un calque posé par le code, la modification prend une seconde, ne coûte rien et ne peut pas introduire de faute d'orthographe.

Le mécanisme

Le modèle livre un fond. Le code fait le reste, dans cet ordre :

flowchart TD
  F["Fond généré (ratio approché)"] --> R["ImageOps.fit : recadrage au format exact"]
  R --> Z["Zone de texte : bas ou gauche, marges, zone sûre de l'application"]
  Z --> M["Mesure : contraste du texte contre le pire 5 % des pixels de la zone"]
  M -- "≥ 4,5:1" --> T["Texte en couleur de marque"]
  M -- "< 4,5:1" --> V["Voile dans la couleur opposée, nouvelle mesure"]
  V --> T
  T --> L["Logo en haut à droite"]
  L --> S["Rendu + mesure enregistrés dans le manifeste"]

Le point délicat est la mesure. Une photo n'a pas une couleur de fond : la zone sous le titre contient un mur clair, une ombre, un reflet. Le contraste calculé sur la moyenne de la zone est trompeur. Le studio prend le 95e centile de luminance si le texte est clair (le pire fond est le plus clair) et le 5e centile si le texte est sombre.

La mise en œuvre

La mesure, dans qc.py :

files/studio/qc.py
def contraste_zone(img, boite, couleur_texte) -> float:
    """Contraste du texte contre le pire 5 % des pixels de la zone (pas la moyenne)."""
    zone = np.asarray(img.convert("RGB").crop(boite), dtype=float).reshape(-1, 3)
    lum = luminance(zone)
    lt = float(luminance(couleur_texte))
    pire = np.percentile(lum, 95) if lt > float(np.median(lum)) else np.percentile(lum, 5)
    return round(float((max(lt, pire) + 0.05) / (min(lt, pire) + 0.05)), 2)

Le choix de couleur et le voile, dans layout.py :

files/studio/layout.py
# Uniquement « texte » ou « fond » de la palette : le kit garantit 4,5:1 entre elles.
couleur = max((kit.couleur("texte"), kit.couleur("fond")), key=lambda c: qc.contraste_zone(img, boite, c))
mesure = qc.contraste_zone(img, boite, couleur)
if mesure < qc.SEUIL_CONTRASTE:
    voile_c = max((kit.couleur("primaire"), kit.couleur("texte"), kit.couleur("fond")),
                  key=lambda c: qc.contraste(c, couleur))
    # rectangle arrondi, opacité 214/255, puis nouvelle mesure sur l'image composée

Le logo est un monogramme dessiné par le code (carré arrondi primaire, initiale en couleur de fond). Une vraie marque remplacera la fonction logo() par le chargement de son fichier PNG ou SVG rastérisé ; le reste ne change pas.

Changer une accroche ou la mise en page ne demande aucun appel au modèle :

make test         # test_format_exact_et_contraste_aa : 4 formats sur un bruit aléatoire, tous ≥ 4,5:1 avec voile
make recomposer   # repose texte et logo sur les fonds existants ; les visuels repassent « à valider »
À gauche le fond généré, à droite le rendu LinkedIn avec voile, titre et logo posés par le code

Résultat obtenu

make recomposer sur la campagne réelle du 2026-10-15 : 0,7 s, 0 appel, et les contrastes mesurés après passage de « noir générique » à « couleur texte de la marque » :

FormatAvant (texte #111111)Après (texte #2B1D16 de la marque)Voile
LinkedIn 1200x6274,91:112,56:1oui, ajouté
Instagram 1080x135013,20:111,38:1oui
Story 1080x192014,03:112,09:1oui
Bannière 1584x3966,01:15,16:1non

Le LinkedIn est l'enseignement : avec le noir générique, il passait de justesse sans voile (4,91:1). Avec la couleur de marque, légèrement plus claire, il serait tombé sous le seuil ; le code l'a détecté et a posé un voile. Personne n'a eu à regarder.

Ce qui casse en production

Quatre défauts observés sur le premier rendu réel, tous corrigés dans le code actuel :

Symptôme observéCauseParade
Le texte de la story est caché par le champ « Envoyer un message » d'Instagramle bloc était collé au bas de l'imagezone_sure=0.14 : 14 % de la hauteur réservés en haut et en bas, comme le recommande Meta pour les stories
« 0,19 » en fin de ligne, « € le lavage » à la ligne suivantecésure sur l'espace avant le symboleinsecables() : espaces insécables avant € : ; ! ? % et après un nombre ; découpe avec split(" "), car split() coupe aussi sur l'insécable
Sous-titre de la bannière à 18 px, illisiblecorps calculé sur la plus petite dimension (396 px)pour les ratios > 3:1, le corps se règle sur la hauteur (14 %)
Logo invisible sur la diapositive à fond primairemonogramme primaire sur aplat primairelogo inversé quand le contraste entre l'aplat et la primaire est < 3:1

La règle de décision

Tout ce qui doit être exact (prix, dates, mentions légales, nom de marque) est un calque. Le modèle d'image ne dessine que ce qui peut être approximatif. Mesurez le contraste sur l'image finale, au percentile, pour chaque rendu, et enregistrez la valeur : c'est la preuve à présenter en cas d'audit d'accessibilité.

Pratique

Un titre blanc est posé sur un ciel bleu moyen avec un nuage blanc dans le coin de la zone. Le contraste moyen de la zone vaut 5,2:1. Que conclut le studio ?

La marque impose son orange #E07A3F pour les titres. Sur le fond crème #F6F1E7, le contraste vaut environ 2,7:1. Que proposez-vous ?

Exercice. Ajoutez une mention légale obligatoire en petit corps (« Prix constaté en magasin, novembre 2026 ») en bas à droite de chaque visuel, avec le même contrôle de contraste.

Voir la solution

Dans composer(), après le logo :

f_m = police(kit, fonts_dir, False, max(14, int(min(W, H) * 0.022)))
mention = insecables("Prix constaté en magasin, novembre 2026")
lm = d.textlength(mention, font=f_m)
bm = (int(W - marge - lm), H - marge - sure - f_m.size, W - marge, H - marge - sure)
c_m = max((kit.couleur("texte"), kit.couleur("fond")), key=lambda c: qc.contraste_zone(img, bm, c))
if qc.contraste_zone(img, bm, c_m) < qc.SEUIL_CONTRASTE:
    ...  # même voile que pour le titre, sur bm
d.text((bm[0], bm[1]), mention, font=f_m, fill=c_m)

Enregistrez la mesure dans le dictionnaire renvoyé ("contraste_mention") et ajoutez-la au contrôle qualité : la mention légale est du texte, le critère 4,5:1 s'y applique aussi.

  • Je sais recadrer une image générée au format exact sans la déformer
  • Je sais mesurer un contraste au percentile sur une photo
  • Je sais réserver la zone sûre d'une story
  • Je sais modifier un texte publié sans régénérer d'image (make recomposer)
Siguiente