Optimiser les coûts
Mis à jour le 29 juillet 2026
Optimiser les coûts
En production, la génération d’images cesse d’être une curiosité technique pour devenir une ligne budgétaire. Une équipe qui produit quelques centaines de visuels par mois s’en aperçoit vite, et la première réaction — brider les usages — est aussi la moins intelligente. Cette leçon présente cinq stratégies qui réduisent la dépense sans toucher à la qualité de ce que vous livrez, parce qu’elles agissent sur ce que vous jetez plutôt que sur ce que vous gardez.
Comprendre la structure des coûts
Le coût d’une génération dépend de trois facteurs.
| Facteur | Impact sur le coût |
|---|---|
Qualité (low, medium, high) | Multiplicateur direct |
| Résolution (1024, 1536) | Plus grand = plus cher |
Nombre d’images (n) | Coût linéaire (n x prix unitaire) |
Ces trois facteurs se multiplient entre eux, ce qui explique pourquoi une exploration menée sans discipline coûte un ordre de grandeur de plus que la même exploration menée avec. Le petit script ci-dessous liste les cinq combinaisons que vous rencontrerez le plus souvent en production ; les montants exacts se lisent sur votre tableau de bord OpenAI, mais l’ordre des lignes vous donne déjà l’échelle des arbitrages.
from openai import OpenAI
client = OpenAI()
# Comparer les coûts : même prompt, paramètres différents
configs = [
{"quality": "low", "size": "1024x1024", "label": "Low / 1024"},
{"quality": "medium", "size": "1024x1024", "label": "Medium / 1024"},
{"quality": "high", "size": "1024x1024", "label": "High / 1024"},
{"quality": "medium", "size": "1536x1024", "label": "Medium / 1536"},
{"quality": "high", "size": "1536x1024", "label": "High / 1536"},
]
print("Configuration | Usage (estimé)")
print("-" * 50)
for config in configs:
print(f"{config['label']:22s} | Vérifiez votre dashboard OpenAI")
Prototyper en low, livrer en high
C’est de loin la stratégie la plus rentable, et la plus simple à adopter. Pendant la phase d’exploration, vous ne jugez pas la finesse d’un dégradé : vous jugez une composition, un cadrage, une ambiance générale, et tout cela se voit parfaitement en quality: low. Le passage en high n’a de sens que pour l’image que vous allez réellement publier. Le workflow ci-dessous formalise cette séquence en produisant cinq brouillons économiques puis un unique rendu final, avec en prime le basculement en format paysage 1536×1024 pour la version définitive. Sur cinq essais et une livraison, l’économie annoncée en fin de script tourne autour de 60 % par rapport à une exploration entièrement menée en high.
from openai import OpenAI
import base64
client = OpenAI()
def workflow_economique(prompt: str, output: str, n_drafts: int = 5):
"""
Workflow en deux phases :
1. Brouillons rapides en low quality
2. Rendu final en high quality
"""
# Phase 1 : exploration rapide
print("Phase 1 : brouillons rapides (low quality)")
drafts = []
for i in range(n_drafts):
response = client.images.generate(
model="gpt-image-1",
prompt=prompt,
size="1024x1024",
quality="low",
response_format="b64_json"
)
path = f"draft_{i+1}.png"
data = base64.b64decode(response.data[0].b64_json)
with open(path, "wb") as f:
f.write(data)
drafts.append(path)
print(f" Brouillon {i+1} : {path}")
# Phase 2 : rendu final (après sélection humaine du meilleur brouillon)
print("\nPhase 2 : rendu final (high quality)")
response = client.images.generate(
model="gpt-image-1",
prompt=prompt,
size="1536x1024",
quality="high",
response_format="b64_json"
)
data = base64.b64decode(response.data[0].b64_json)
with open(output, "wb") as f:
f.write(data)
print(f" Rendu final : {output}")
# Bilan : 5 low + 1 high au lieu de 5 high
print(f"\nBilan : {n_drafts} brouillons low + 1 final high")
print("Économie estimée : ~60% par rapport à tout en high")
workflow_economique(
"Illustration d'un campus technologique vert avec panneaux solaires, style architectural",
"campus_final.png"
)
Ne jamais payer deux fois la même image
Dans une application réelle, le même prompt revient bien plus souvent qu’on ne l’imagine : un utilisateur qui recharge la page, un script de build qui rejoue une génération, une icône réutilisée sur trois pages. Chacun de ces cas est une facture inutile. Le cache ci-dessous les élimine en calculant une empreinte SHA-256 sur le triplet prompt, taille et qualité — les trois éléments qui déterminent le résultat facturé — et en tronquant cette empreinte à seize caractères pour en faire un nom de fichier maniable. Notez la vérification os.path.exists : l’index peut connaître une entrée dont le fichier a été supprimé du disque, et le code régénère alors plutôt que de retourner un chemin mort.
import hashlib
import json
import os
from openai import OpenAI
import base64
client = OpenAI()
CACHE_DIR = "image_cache"
CACHE_INDEX = os.path.join(CACHE_DIR, "index.json")
def charger_cache() -> dict:
"""Charge l'index du cache."""
if os.path.exists(CACHE_INDEX):
with open(CACHE_INDEX, "r") as f:
return json.load(f)
return {}
def sauver_cache(cache: dict):
"""Sauvegarde l'index du cache."""
os.makedirs(CACHE_DIR, exist_ok=True)
with open(CACHE_INDEX, "w") as f:
json.dump(cache, f, indent=2, ensure_ascii=False)
def generer_avec_cache(
prompt: str,
size: str = "1024x1024",
quality: str = "medium"
) -> str:
"""Génère une image ou retourne le résultat en cache."""
# Créer une clé unique basée sur les paramètres
key_data = f"{prompt}|{size}|{quality}"
cache_key = hashlib.sha256(key_data.encode()).hexdigest()[:16]
cache = charger_cache()
if cache_key in cache:
cached_path = cache[cache_key]["path"]
if os.path.exists(cached_path):
print(f"Cache hit : {cached_path}")
return cached_path
else:
print("Cache périmé, regénération...")
# Pas en cache : générer
response = client.images.generate(
model="gpt-image-1",
prompt=prompt,
size=size,
quality=quality,
response_format="b64_json"
)
path = os.path.join(CACHE_DIR, f"{cache_key}.png")
os.makedirs(CACHE_DIR, exist_ok=True)
data = base64.b64decode(response.data[0].b64_json)
with open(path, "wb") as f:
f.write(data)
cache[cache_key] = {
"prompt": prompt[:100],
"size": size,
"quality": quality,
"path": path
}
sauver_cache(cache)
print(f"Généré et mis en cache : {path}")
return path
# Première génération : appel API
path1 = generer_avec_cache("Logo minimaliste pour une startup tech, bleu et blanc")
# Deuxième appel identique : servi depuis le cache
path2 = generer_avec_cache("Logo minimaliste pour une startup tech, bleu et blanc")
Dimensionner au plus juste
Générer en haute résolution une vignette qui sera affichée dans un carré de 80 pixels revient à payer un tirage d’exposition pour le glisser dans un portefeuille. Le remède consiste à ne plus laisser ce choix à l’humeur du développeur mais à le dériver de l’usage déclaré. Le dictionnaire de generer_pour_usage répartit les cas en trois familles : le petit format web — vignettes et avatars — qui se contente de low ; l’usage web standard — header de blog, post social — qui appelle medium ; et les supports où la finesse se voit vraiment — bannière hero, impression A4, portfolio — qui justifient high. Les deux appels finaux illustrent l’écart : le même chat astronaute coûte un multiple selon qu’il finit en vignette ou en bannière de page d’accueil.
from openai import OpenAI
import base64
client = OpenAI()
def generer_pour_usage(prompt: str, usage: str, output: str):
"""Choisit automatiquement la résolution et qualité optimales."""
configs = {
# Usage web petit format
"thumbnail": {"size": "1024x1024", "quality": "low"},
"avatar": {"size": "1024x1024", "quality": "low"},
# Usage web standard
"blog_header": {"size": "1536x1024", "quality": "medium"},
"social_post": {"size": "1024x1024", "quality": "medium"},
# Usage nécessitant haute qualité
"hero_banner": {"size": "1536x1024", "quality": "high"},
"print_a4": {"size": "1024x1536", "quality": "high"},
"portfolio": {"size": "1024x1024", "quality": "high"},
}
config = configs.get(usage, {"size": "1024x1024", "quality": "medium"})
response = client.images.generate(
model="gpt-image-1",
prompt=prompt,
size=config["size"],
quality=config["quality"],
response_format="b64_json"
)
data = base64.b64decode(response.data[0].b64_json)
with open(output, "wb") as f:
f.write(data)
print(f"Usage {usage} ({config['quality']}/{config['size']}) → {output}")
generer_pour_usage("Chat mignon en astronaute", "thumbnail", "chat_thumb.png")
generer_pour_usage("Chat mignon en astronaute", "hero_banner", "chat_hero.png")
Éditer plutôt que tout refaire
Le client valide la photo mais veut un autre ciel. Le réflexe naturel — relancer la génération complète avec un prompt ajusté — vous fait payer une image entière et, pire, vous fait perdre le reste de la composition que tout le monde avait approuvé. images.edit() traite la situation pour le prix d’une édition : la fonction modifier_detail construit un masque qui ne libère que le rectangle concerné, ici les trois cents premiers pixels de hauteur correspondant au ciel, et le prompt ne décrit que le contenu de cette bande. Le paysage, lui, ne bouge pas d’un pixel.
from openai import OpenAI
from PIL import Image, ImageDraw
client = OpenAI()
# Au lieu de regénérer l'image entière pour changer un détail,
# éditez uniquement la zone concernée.
def modifier_detail(image_path: str, zone: tuple, nouveau_contenu: str, output: str):
"""
Modifie un détail dans une image existante.
zone : (x1, y1, x2, y2) de la zone à modifier
"""
source = Image.open(image_path)
mask = Image.new("RGBA", source.size, (0, 0, 0, 255))
draw = ImageDraw.Draw(mask)
draw.rectangle(zone, fill=(0, 0, 0, 0))
mask.save("temp_mask.png")
response = client.images.edit(
model="gpt-image-1",
image=open(image_path, "rb"),
mask=open("temp_mask.png", "rb"),
prompt=nouveau_contenu
)
import urllib.request
urllib.request.urlretrieve(response.data[0].url, output)
print(f"Détail modifié : {output}")
print("Coût : 1 édition au lieu de 1 génération complète")
# Exemple : changer juste le ciel d'une photo
modifier_detail(
"paysage.png",
(0, 0, 1024, 300),
"Ciel bleu avec quelques nuages blancs cotonneux",
"paysage_ciel_modifie.png"
)
Savoir où part l’argent
Les quatre stratégies précédentes supposent que vous sachiez ce que vous consommez, et cette visibilité ne vient pas toute seule : le tableau de bord du fournisseur vous donne un total, pas la répartition par projet ni par qualité. Un journal local comble ce vide pour un coût de développement dérisoire. log_generation ajoute une ligne horodatée à chaque appel, en tronquant le prompt à cent caractères pour rester lisible, et rapport_usage agrège ensuite le tout par niveau de qualité. C’est cette répartition qui déclenche les décisions : si votre journal révèle que 80 % de vos images sont générées en high alors que la majorité finit sur des posts sociaux, vous venez de trouver votre gisement d’économies.
import json
import os
from datetime import datetime
USAGE_FILE = "image_usage_log.json"
def log_generation(prompt: str, size: str, quality: str, n: int = 1):
"""Enregistre chaque génération pour suivre les coûts."""
if os.path.exists(USAGE_FILE):
with open(USAGE_FILE, "r") as f:
log = json.load(f)
else:
log = []
entry = {
"timestamp": datetime.now().isoformat(),
"prompt": prompt[:100],
"size": size,
"quality": quality,
"n_images": n,
}
log.append(entry)
with open(USAGE_FILE, "w") as f:
json.dump(log, f, indent=2, ensure_ascii=False)
def rapport_usage():
"""Affiche un rapport de consommation."""
if not os.path.exists(USAGE_FILE):
print("Aucune donnée de consommation.")
return
with open(USAGE_FILE, "r") as f:
log = json.load(f)
total = len(log)
par_qualite = {}
for entry in log:
q = entry["quality"]
par_qualite[q] = par_qualite.get(q, 0) + entry["n_images"]
print(f"Total des générations : {total}")
print(f"Répartition par qualité :")
for q, count in sorted(par_qualite.items()):
print(f" {q}: {count} images")
rapport_usage()
Par quoi commencer
Toutes ces stratégies ne se valent pas en rapport gain sur effort, et le tableau suivant vous aide à les ordonner.
| Stratégie | Économie estimée | Effort |
|---|---|---|
| Low pour prototypage | 50-70% | Minimal |
| Cache de prompts | 10-30% (si répétitions) | Modéré |
| Résolution adaptée | 20-40% | Minimal |
| Édition vs régénération | 30-50% (par modification) | Modéré |
| Suivi des dépenses | Indirect (visibilité) | Modéré |
Deux lignes se détachent nettement : le prototypage en low et la résolution adaptée à l’usage demandent un effort minimal pour la moitié des économies possibles. Commencez par celles-là, puis ajoutez le cache et l’édition ciblée quand votre volume le justifie. Le suivi des dépenses, lui, ne fait économiser directement rien du tout — mais c’est lui qui vous dira lesquelles des quatre autres portent réellement leurs fruits chez vous.
Le meilleur moyen de ne plus y penser est d’enfermer ces règles dans un seul point de passage. Écrivez un wrapper autour de client.images.generate() qui intègre le cache par prompt, l’adaptation automatique de la qualité et de la résolution selon l’usage déclaré, et le journal de chaque appel ; puis lancez-le sur dix générations et lisez le rapport de consommation. À partir de là, votre équipe entière économise sans avoir à se souvenir de quoi que ce soit.