ChatCraft / backend /scripts /generate_sprites.py
gabraken's picture
feat: add game engine, voice commands, leaderboard, tutorial overlay, and stats tracking
29a88f8
#!/usr/bin/env python3
"""
Génère les sprites PNG (vues de dessus, style SF Starcraft/Warhammer).
Backend au choix (priorité GCP si configuré) :
- GCP Vertex AI Imagen : définir GCP_PROJECT_ID (et optionnellement GCP_LOCATION) dans .env
- Mistral : définir MISTRAL_API_KEY dans .env
Usage : cd backend && python -m scripts.generate_sprites
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
_backend = Path(__file__).resolve().parent.parent
if str(_backend) not in sys.path:
sys.path.insert(0, str(_backend))
import logging
import random
import time
from typing import Any
from config import GCP_PROJECT_ID, GCP_LOCATION, MISTRAL_API_KEY
from game.units import UnitType
from game.buildings import BuildingType, BUILDING_DEFS
from game.map import ResourceType
logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)
SPRITES_DIR = _backend / "static" / "sprites"
UNITS_DIR = SPRITES_DIR / "units"
BUILDINGS_DIR = SPRITES_DIR / "buildings"
RESOURCES_DIR = SPRITES_DIR / "resources"
ICONS_DIR = SPRITES_DIR / "icons"
# Taille cible et fond transparent obligatoires
TARGET_SIZE = 128
# Chroma vert : fond #00ff00 demandé à l'API, détourage sur cette couleur uniquement
CHROMA_GREEN_RGB = (0, 255, 0) # #00ff00 pour le prompt
CHROMA_GREEN_TOLERANCE = 55 # pour vert vif proche de #00ff00
# Vert "dominant" (G > R et G > B) pour attraper les verts plus foncés renvoyés par Imagen
CHROMA_GREEN_MIN_G = 60 # G >= ce seuil et G dominant → considéré fond vert
CHROMA_GREEN_PROMPT = " The background must be solid green #00ff00 only, like a green screen. Subject in gray/blue/metallic, no white or other background."
# Backoff pour rate limit (429)
BACKOFF_BASE_SEC = 30
BACKOFF_MAX_SEC = 300
BACKOFF_MAX_RETRIES = 8
DELAY_BETWEEN_SPRITES_SEC = 5 # Délai entre chaque génération pour éviter le 429
# Instructions pour l'agent : PETITE image (128x128), pas de grosse résolution = moins de rate limit
AGENT_INSTRUCTIONS = """You are an image generation assistant for a real-time strategy game (sci-fi, Starcraft/Warhammer style).
When asked to generate a sprite, you MUST use the image generation tool to produce exactly one image.
CRITICAL - SMALL SIZE (to avoid rate limits):
- Generate SMALL, LOW-RESOLUTION images only. For units: exactly 128x128 pixels. For buildings: longest side 128 pixels. Do NOT generate large or high-resolution images (no 512, 1024, etc.). Small output is required.
- MANDATORY transparent background: only the subject has visible pixels; everything else fully transparent (PNG with alpha). No ground, no floor, no solid color.
CRITICAL - VIEW ANGLE (NO EXCEPTIONS):
- Strict top-down view ONLY (bird's eye, camera at exactly 90 degrees directly above the subject). This applies to EVERY sprite without exception, including soldiers, mechs, vehicles, and buildings.
- You must ONLY see the top surface/footprint of the subject. No side view, no 3/4 view, no isometric, no perspective angle.
- For units with weapons (marine, goliath, tank, etc.): the weapon barrel must point straight forward, visible from above as a shape pointing toward the top of the image.
- Reject any tendency to show a front/side/3D perspective — the camera is directly overhead, period.
Other: subject fills frame, minimal margins. Dark metallic, blue/gray palette, industrial sci-fi. No text or labels. One image per request, no commentary."""
# Vue strictement de dessus + fond transparent + remplir le cadre
_TOP = "Strict top-down view only (bird's eye, camera directly above, 90°). Only the top surface/footprint visible, no side or perspective. "
_FILL = "MANDATORY green #00ff00 background only (no ground, no floor, no details). Subject must fill the frame tightly, edge to edge. "
# Petite image demandée à l'API pour éviter le rate limit (pas de post-traitement)
_SMALL = "Generate a SMALL low-resolution image only: 128x128 pixels for square sprites, or longest side 128 for rectangles. Tiny size. Do NOT output large or high-resolution (no 512, 1024, etc.). "
# Prompts par unité : petite 128x128, fond transparent
UNIT_PROMPTS: dict[str, str] = {
UnitType.SCV.value: _TOP + _FILL + _SMALL + "Single SCV worker robot seen strictly from directly above at 90 degrees, bird's eye view only. Unit faces upward (toward the top of the image). Top surface visible: mechanical body footprint, two mechanical arms extended sideways, cockpit hatch on top. Industrial gray and blue. Camera is directly overhead, no side or perspective angle.",
UnitType.MARINE.value: _TOP + _FILL + _SMALL + "Single Terran Marine soldier seen strictly from directly above at 90 degrees, bird's eye view only. Unit faces upward (toward the top of the image), rifle/assault gun barrel pointing straight up. Top of armored helmet visible at center, heavy shoulder pads on both sides. Dark blue armor. No side view, no isometric, camera directly overhead.",
UnitType.MEDIC.value: _TOP + _FILL + _SMALL + "Single Terran Medic soldier seen strictly from directly above at 90 degrees, bird's eye view only. Unit faces upward (toward the top of the image), medkit or injector pointing straight up. Top of helmet visible at center, white/light armor with red cross medical symbol on back. Sci-fi armor. Camera directly overhead, no side or perspective.",
UnitType.GOLIATH.value: _TOP + _FILL + _SMALL + "Goliath combat walker mech seen strictly from directly above at 90 degrees, bird's eye view only. Unit faces upward (toward the top of the image), both autocannon gun pods pointing straight up. Top surface: wide armored torso, two gun pods on shoulders, legs spread below. Dark metal and blue. Camera directly overhead at 90 degrees, no side view.",
UnitType.TANK.value: _TOP + _FILL + _SMALL + "Siege Tank seen strictly from directly above at 90 degrees, bird's eye view only. Tank faces upward (toward the top of the image), cannon barrel pointing straight up. Top surface: elongated armored hull, round turret on top, tank treads visible on both sides. Military gray and blue. Camera directly overhead.",
UnitType.WRAITH.value: _TOP + _FILL + _SMALL + "Wraith starfighter aircraft seen strictly from directly above at 90 degrees, bird's eye view only. Aircraft faces upward (toward the top of the image), nose and weapon pods pointing straight up. Top surface: delta wing shape, cockpit bubble at center-front. Dark metallic with blue highlights. Camera directly overhead.",
}
# Icônes UI : style flat symbolique, fond vert chroma pour détourage
ICON_PROMPTS: dict[str, str] = {
"mineral": _FILL + _SMALL + "Flat symbolic icon of a blue mineral crystal, bold angular shape, solid blue-cyan color, thick black outline, 2D game UI icon style. No background, no text, no shading.",
"gas": _FILL + _SMALL + "Flat symbolic icon of a purple vespene gas canister or flask, bold shape, solid purple-violet color, thick black outline, 2D game UI icon style. No green. No background, no text, no shading.",
"supply": _FILL + _SMALL + "Flat symbolic icon of a chevron/arrow pointing up or a small house shape, bold, solid yellow-orange color, thick black outline, 2D game UI icon style. No background, no text, no shading.",
}
# Prompts par ressource : icône top-down 64x64
RESOURCE_PROMPTS: dict[str, str] = {
ResourceType.MINERAL.value: _TOP + _FILL + _SMALL + "Mineral crystal cluster from above: angular blue-teal crystalline shards, glowing. Sci-fi RTS style. No text.",
ResourceType.GEYSER.value: _TOP + _FILL + _SMALL + "Vespene gas geyser from above: purple/violet glowing vent/crater with purple gas fumes rising. Purple and magenta colors only, NO green. Sci-fi RTS style. No text.",
}
# Prompts par bâtiment : petite image (longest side 128), fond transparent
def _building_prompt(building_type: BuildingType) -> str:
prompts = {
BuildingType.COMMAND_CENTER: _TOP + _FILL + _SMALL + "Command Center roof from above: rectangle, landing pad, antenna. Terran, dark metal and blue.",
BuildingType.SUPPLY_DEPOT: _TOP + _FILL + _SMALL + "Supply Depot from above: three silo tops. Metallic. Transparent background only.",
BuildingType.BARRACKS: _TOP + _FILL + _SMALL + "Barracks from above: rectangular roof, military. Terran.",
BuildingType.ENGINEERING_BAY: _TOP + _FILL + _SMALL + "Engineering Bay from above: tech roof, rounded or angular. Sci-fi lab.",
BuildingType.REFINERY: _TOP + _FILL + _SMALL + "Refinery from above: extractor, pipes/circles. Industrial.",
BuildingType.FACTORY: _TOP + _FILL + _SMALL + "Factory from above: rectangular industrial roof, vehicle bay. Terran, mechanical.",
BuildingType.ARMORY: _TOP + _FILL + _SMALL + "Armory from above: armored roof, angular. Transparent background only.",
BuildingType.STARPORT: _TOP + _FILL + _SMALL + "Starport from above: hangar roof, landing area.",
}
base = prompts.get(building_type, _TOP + _FILL + _SMALL + "Sci-fi building roof, Terran.")
return base + " Transparent background only. Dark metal and blue."
def _is_chroma_green(r: int, g: int, b: int) -> bool:
"""True si le pixel est fond vert à détourer : proche de #00ff00 ou vert dominant (G > R, G > B)."""
# Vert vif type #00ff00
r0, g0, b0 = CHROMA_GREEN_RGB
if (
abs(r - r0) <= CHROMA_GREEN_TOLERANCE
and abs(g - g0) <= CHROMA_GREEN_TOLERANCE
and abs(b - b0) <= CHROMA_GREEN_TOLERANCE
):
return True
# Vert plus foncé (Imagen renvoie souvent ~50,127,70) : G dominant et G assez élevé
if g >= CHROMA_GREEN_MIN_G and g >= r and g >= b:
# exclure les gris (r≈g≈b) et garder un vrai vert (g nettement > r ou b)
if r > 100 and b > 100 and abs(r - g) < 30 and abs(b - g) < 30:
return False # gris, pas vert
return True
return False
def _resize_and_make_transparent(data: bytes, out_path: Path, size: int = TARGET_SIZE) -> None:
"""Redimensionne à size px (côté max). Rend transparent : fond vert chroma ET blanc/clair. Sauvegarde en PNG."""
from PIL import Image
import io
img = Image.open(io.BytesIO(data)).convert("RGBA")
w, h = img.size
if w > size or h > size:
scale = size / max(w, h)
nw, nh = max(1, int(w * scale)), max(1, int(h * scale))
img = img.resize((nw, nh), Image.Resampling.LANCZOS)
pixels = img.load()
for y in range(img.height):
for x in range(img.width):
r, g, b, a = pixels[x, y]
if _is_chroma_green(r, g, b):
pixels[x, y] = (r, g, b, 0)
out_path.parent.mkdir(parents=True, exist_ok=True)
img.save(out_path, "PNG")
# ---------------------------------------------------------------------------
# GCP Vertex AI Imagen (optionnel)
# ---------------------------------------------------------------------------
def _generate_one_vertex(prompt: str, out_path: Path, progress: str = "") -> bool:
"""Génère une image via Vertex AI Imagen et la sauvegarde."""
from google import genai
from google.genai.types import GenerateImagesConfig
name = out_path.stem
for attempt in range(BACKOFF_MAX_RETRIES):
try:
if attempt == 0:
log.info("[%s] Appel Vertex Imagen pour %s...", progress or name, name)
else:
log.info("[%s] Retry %s/%s — Imagen pour %s...", progress or name, attempt + 1, BACKOFF_MAX_RETRIES, name)
# GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION, GOOGLE_GENAI_USE_VERTEXAI set in main()
client = genai.Client(vertexai=True)
vertex_prompt = prompt + CHROMA_GREEN_PROMPT
response = client.models.generate_images(
model="imagen-3.0-fast-generate-001",
prompt=vertex_prompt,
config=GenerateImagesConfig(
numberOfImages=1,
aspectRatio="1:1",
),
)
if not response.generated_images or not response.generated_images[0].image:
log.warning("[%s] Réponse Imagen vide pour %s", progress or name, name)
return False
img = response.generated_images[0].image
raw_bytes: bytes
if hasattr(img, "image_bytes") and img.image_bytes is not None:
raw_bytes = img.image_bytes
elif hasattr(img, "save") and callable(img.save):
import io
buf = io.BytesIO()
img.save(buf)
raw_bytes = buf.getvalue()
else:
log.error("[%s] Format image Imagen inattendu pour %s", progress or name, name)
return False
_resize_and_make_transparent(raw_bytes, out_path)
size_kb = out_path.stat().st_size / 1024
log.info("[%s] OK %s — sauvegardé 128px transparent (%s Ko) → %s", progress or name, name, round(size_kb, 1), out_path)
return True
except Exception as e:
err_msg = str(e)
if ("429" in err_msg or "rate limit" in err_msg.lower() or "resource exhausted" in err_msg.lower()) and attempt < BACKOFF_MAX_RETRIES - 1:
delay = min(BACKOFF_BASE_SEC * (2 ** attempt), BACKOFF_MAX_SEC)
jitter = random.uniform(0, delay * 0.2)
time.sleep(delay + jitter)
else:
log.error("[%s] Échec Imagen pour %s: %s", progress or name, name, err_msg)
return False
return False
# ---------------------------------------------------------------------------
# Mistral (fallback si GCP non configuré)
# ---------------------------------------------------------------------------
def _find_file_id_in_response(response: Any) -> str | None:
"""Extrait le file_id du premier ToolFileChunk dans response.outputs."""
from mistralai.models import ToolFileChunk
for entry in getattr(response, "outputs", []) or []:
content = getattr(entry, "content", None)
if content is None:
continue
chunks = content if isinstance(content, list) else [content]
for chunk in chunks:
if isinstance(chunk, ToolFileChunk) or getattr(chunk, "type", None) == "tool_file":
fid = getattr(chunk, "file_id", None)
if fid:
return fid
return None
def _is_rate_limit_error(e: Exception) -> bool:
msg = str(e).lower()
return "429" in msg or "rate limit" in msg or "too many requests" in msg
def _generate_one(client: Mistral, agent_id: str, prompt: str, out_path: Path, progress: str = "") -> bool:
"""Lance une conversation avec l'agent, récupère l'image générée et la sauvegarde.
Exponential backoff sur 429 (rate limit). Génération strictement séquentielle côté appelant."""
name = out_path.stem
res = None
for attempt in range(BACKOFF_MAX_RETRIES):
try:
if attempt == 0:
log.info("[%s] Appel API Mistral (conversations.start) pour %s...", progress or name, name)
else:
log.info("[%s] Retry %s/%s — conversation pour %s...", progress or name, attempt + 1, BACKOFF_MAX_RETRIES, name)
res = client.beta.conversations.start(
agent_id=agent_id,
inputs=prompt,
)
log.info("[%s] Réponse reçue pour %s, extraction file_id...", progress or name, name)
break
except Exception as e:
err_msg = str(e)
if _is_rate_limit_error(e) and attempt < BACKOFF_MAX_RETRIES - 1:
delay = min(BACKOFF_BASE_SEC * (2 ** attempt), BACKOFF_MAX_SEC)
jitter = random.uniform(0, delay * 0.2)
wait = delay + jitter
log.warning(
"[%s] RATE LIMIT (429) sur %s — tentative %s/%s. Attente %.0f s avant retry. Détail: %s",
progress or name, name, attempt + 1, BACKOFF_MAX_RETRIES, wait, err_msg[:500],
)
time.sleep(wait)
else:
log.error("[%s] Échec conversation pour %s (tentative %s): %s", progress or name, name, attempt + 1, err_msg)
return False
if res is None:
return False
file_id = _find_file_id_in_response(res)
if not file_id:
outputs = getattr(res, "outputs", [])
out_types = [type(o).__name__ for o in (outputs if isinstance(outputs, list) else [outputs])]
log.warning("[%s] Aucun file_id dans la réponse pour %s. outputs (%s): %s", progress or name, name, len(outputs), out_types)
return False
log.info("[%s] Téléchargement file_id=%s pour %s...", progress or name, file_id[:12] + "...", name)
data = None
for attempt in range(BACKOFF_MAX_RETRIES):
try:
data = client.files.download(file_id=file_id).read()
break
except Exception as e:
err_msg = str(e)
if _is_rate_limit_error(e) and attempt < BACKOFF_MAX_RETRIES - 1:
delay = min(BACKOFF_BASE_SEC * (2 ** attempt), BACKOFF_MAX_SEC)
jitter = random.uniform(0, delay * 0.2)
wait = delay + jitter
log.warning(
"[%s] RATE LIMIT (429) au téléchargement pour %s — tentative %s/%s. Attente %.0f s. %s",
progress or name, name, attempt + 1, BACKOFF_MAX_RETRIES, wait, err_msg[:300],
)
time.sleep(wait)
else:
log.error("[%s] Échec téléchargement %s pour %s: %s", progress or name, file_id, name, err_msg)
return False
if data is None:
return False
_resize_and_make_transparent(data, out_path)
size_kb = out_path.stat().st_size / 1024
log.info("[%s] OK %s — sauvegardé 128px transparent (%s Ko) → %s", progress or name, name, round(size_kb, 1), out_path)
return True
def main() -> None:
import argparse
parser = argparse.ArgumentParser(description="Generate unit/building sprites (Vertex Imagen or Mistral)")
parser.add_argument("--unit", type=str, metavar="ID", help="Generate only this unit (e.g. marine, scv)")
parser.add_argument("--building", type=str, metavar="ID", help="Generate only this building (e.g. barracks)")
parser.add_argument("--resource", type=str, metavar="ID", help="Generate only this resource (e.g. mineral, geyser)")
parser.add_argument("--icon", type=str, metavar="ID", help="Generate only this UI icon (e.g. mineral, gas, supply)")
parser.add_argument("--skip-existing", action="store_true", help="Skip sprites that already exist (do not regenerate)")
args = parser.parse_args()
only_unit = args.unit.strip().lower() if args.unit else None
skip_existing = args.skip_existing
only_building = args.building.strip().lower().replace(" ", "_") if args.building else None
only_resource = args.resource.strip().lower() if args.resource else None
only_icon = args.icon.strip().lower() if args.icon else None
use_vertex = bool(GCP_PROJECT_ID)
if use_vertex:
os.environ["GOOGLE_CLOUD_PROJECT"] = GCP_PROJECT_ID
os.environ["GOOGLE_CLOUD_LOCATION"] = GCP_LOCATION
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "true"
log.info("Backend: Vertex AI Imagen (project=%s, location=%s)", GCP_PROJECT_ID, GCP_LOCATION)
def do_generate(prompt: str, out: Path, progress: str) -> bool:
return _generate_one_vertex(prompt, out, progress)
else:
if not MISTRAL_API_KEY:
log.error("Aucun backend image configuré. Définir GCP_PROJECT_ID (ex: mtgbinder) ou MISTRAL_API_KEY dans .env")
sys.exit(1)
from mistralai import Mistral
client = Mistral(api_key=MISTRAL_API_KEY)
log.info("Creating Mistral image generation agent...")
try:
agent = client.beta.agents.create(
model="mistral-medium-2505",
name="Sprite Generator",
description="Generates top-down game sprites for units and buildings.",
instructions=AGENT_INSTRUCTIONS,
tools=[{"type": "image_generation"}],
completion_args={"temperature": 0.3, "top_p": 0.95},
)
agent_id = agent.id
log.info("Agent id: %s", agent_id)
except Exception as e:
log.exception("Failed to create agent: %s", e)
sys.exit(1)
def do_generate(prompt: str, out: Path, progress: str) -> bool:
return _generate_one(client, agent_id, prompt, out, progress=progress)
only_resource_flag = only_resource is not None
only_unit_flag = only_unit is not None
only_building_flag = only_building is not None
only_icon_flag = only_icon is not None
UNITS_DIR.mkdir(parents=True, exist_ok=True)
BUILDINGS_DIR.mkdir(parents=True, exist_ok=True)
RESOURCES_DIR.mkdir(parents=True, exist_ok=True)
ICONS_DIR.mkdir(parents=True, exist_ok=True)
units_to_run: list = []
if not only_building_flag and not only_resource_flag and not only_icon_flag:
units_to_run = [ut for ut in UnitType if not only_unit_flag or ut.value == only_unit]
if only_unit_flag and not units_to_run:
log.error("Unknown unit: %s. Valid: %s", only_unit, [u.value for u in UnitType])
sys.exit(1)
buildings_to_run: list = []
if not only_unit_flag and not only_resource_flag and not only_icon_flag:
buildings_to_run = [bt for bt in BuildingType if not only_building_flag or bt.value == only_building]
if only_building_flag and not buildings_to_run:
log.error("Unknown building: %s. Valid: %s", only_building, [b.value for b in BuildingType])
sys.exit(1)
resources_to_run: list = []
if not only_unit_flag and not only_building_flag and not only_icon_flag:
valid_resources = list(RESOURCE_PROMPTS.keys())
resources_to_run = [r for r in valid_resources if not only_resource_flag or r == only_resource]
if only_resource_flag and not resources_to_run:
log.error("Unknown resource: %s. Valid: %s", only_resource, valid_resources)
sys.exit(1)
icons_to_run: list = []
if not only_unit_flag and not only_building_flag and not only_resource_flag:
valid_icons = list(ICON_PROMPTS.keys())
icons_to_run = [ic for ic in valid_icons if not only_icon_flag or ic == only_icon]
if only_icon_flag and not icons_to_run:
log.error("Unknown icon: %s. Valid: %s", only_icon, valid_icons)
sys.exit(1)
total = len(units_to_run) + len(buildings_to_run) + len(resources_to_run) + len(icons_to_run)
log.info("========== GÉNÉRATION SPRITES ==========")
log.info("Unités à générer: %s (%s)", [u.value for u in units_to_run], len(units_to_run))
log.info("Bâtiments à générer: %s (%s)", [b.value for b in buildings_to_run], len(buildings_to_run))
log.info("Ressources à générer: %s (%s)", resources_to_run, len(resources_to_run))
log.info("Icônes UI à générer: %s (%s)", icons_to_run, len(icons_to_run))
log.info("Total: %s sprites. Délai entre chaque: %s s.", total, DELAY_BETWEEN_SPRITES_SEC)
log.info("=========================================")
ok, fail = 0, 0
t0 = time.monotonic()
generated_count = 0
for i, ut in enumerate(units_to_run):
out = UNITS_DIR / f"{ut.value}.png"
if skip_existing and out.exists():
log.info("[unit %s/%s] Déjà présent, ignoré: %s", i + 1, len(units_to_run), out.name)
ok += 1
continue
if generated_count > 0:
log.info("--- Pause %s s (rate limit) ---", DELAY_BETWEEN_SPRITES_SEC)
time.sleep(DELAY_BETWEEN_SPRITES_SEC)
prompt = UNIT_PROMPTS.get(ut.value, _TOP + _FILL + _SMALL + f"{ut.value} unit from above, sci-fi RTS. Unit faces upward (toward the top of the image), weapon pointing straight up. Transparent background only.")
progress = "unit %s/%s" % (i + 1, len(units_to_run))
if do_generate(prompt, out, progress):
ok += 1
else:
fail += 1
log.warning("Échec pour unité %s", ut.value)
generated_count += 1
for i, bt in enumerate(buildings_to_run):
out = BUILDINGS_DIR / f"{bt.value}.png"
if skip_existing and out.exists():
log.info("[building %s/%s] Déjà présent, ignoré: %s", i + 1, len(buildings_to_run), out.name)
ok += 1
continue
if generated_count > 0:
log.info("--- Pause %s s (rate limit) ---", DELAY_BETWEEN_SPRITES_SEC)
time.sleep(DELAY_BETWEEN_SPRITES_SEC)
prompt = _building_prompt(bt)
progress = "building %s/%s" % (i + 1, len(buildings_to_run))
if do_generate(prompt, out, progress):
ok += 1
else:
fail += 1
log.warning("Échec pour bâtiment %s", bt.value)
generated_count += 1
for i, rid in enumerate(resources_to_run):
out = RESOURCES_DIR / f"{rid}.png"
if skip_existing and out.exists():
log.info("[resource %s/%s] Déjà présent, ignoré: %s", i + 1, len(resources_to_run), out.name)
ok += 1
continue
if generated_count > 0:
log.info("--- Pause %s s (rate limit) ---", DELAY_BETWEEN_SPRITES_SEC)
time.sleep(DELAY_BETWEEN_SPRITES_SEC)
prompt = RESOURCE_PROMPTS[rid]
progress = "resource %s/%s" % (i + 1, len(resources_to_run))
if do_generate(prompt, out, progress):
ok += 1
else:
fail += 1
log.warning("Échec pour ressource %s", rid)
generated_count += 1
for i, ic in enumerate(icons_to_run):
out = ICONS_DIR / f"{ic}.png"
if skip_existing and out.exists():
log.info("[icon %s/%s] Déjà présent, ignoré: %s", i + 1, len(icons_to_run), out.name)
ok += 1
continue
if generated_count > 0:
log.info("--- Pause %s s (rate limit) ---", DELAY_BETWEEN_SPRITES_SEC)
time.sleep(DELAY_BETWEEN_SPRITES_SEC)
prompt = ICON_PROMPTS[ic]
progress = "icon %s/%s" % (i + 1, len(icons_to_run))
if do_generate(prompt, out, progress):
ok += 1
else:
fail += 1
log.warning("Échec pour icône %s", ic)
generated_count += 1
elapsed = time.monotonic() - t0
log.info("========== FIN ==========")
log.info("Réussis: %s, Échecs: %s, Total: %s. Temps: %.1f s.", ok, fail, total, elapsed)
if fail:
sys.exit(1)
if __name__ == "__main__":
main()