Aller au contenu principal

Optimiser les coûts

Optimiser les coûts

La génération d’images via l’API représente un poste de dépense significatif en production. Cette leçon vous donne les techniques concrètes pour réduire vos coûts sans sacrifier la qualité de vos visuels.

Comprendre la structure des coûts

Le coût d’une génération dépend de trois facteurs :

FacteurImpact 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)
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")

Stratégie 1 : prototyper en low, livrer en high

La technique la plus efficace : utilisez quality: low pendant toute la phase d’exploration, puis passez en high uniquement pour le rendu final.

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"
)

Stratégie 2 : cache de prompts

Évitez de regénérer des images identiques en maintenant un cache :

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")

Stratégie 3 : dimensionner au plus juste

Ne générez pas en haute résolution si le visuel sera affiché en petit :

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")

Stratégie 4 : édition plutôt que régénération complète

Quand vous devez modifier un détail, utilisez images.edit() plutôt que de tout regénérer :

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"
)

Stratégie 5 : suivi des dépenses

Mettez en place un tracker de consommation :

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()

Tableau récapitulatif des stratégies

StratégieÉconomie estiméeEffort
Low pour prototypage50-70%Minimal
Cache de prompts10-30% (si répétitions)Modéré
Résolution adaptée20-40%Minimal
Édition vs régénération30-50% (par modification)Modéré
Suivi des dépensesIndirect (visibilité)Modéré

Exercice pratique

Créez un wrapper complet autour de client.images.generate() qui intègre automatiquement : le cache par prompt, l’adaptation qualité/résolution selon l’usage, et le logging de chaque appel. Testez-le avec 10 générations et affichez le rapport de consommation.