"""Client Scenario (voir plan Phase 2, §3) — génération d'images (fonds/ objets 2D uniquement, jamais de personnages/sprites — décision actée lors de l'étude préalable). API confirmée (docs.scenario.com) : authentification Basic (clé + secret), génération ASYNCHRONE par job — POST déclenche un job, GET /jobs/{id} jusqu'à "success"/"failure", puis GET /assets/{id} pour l'URL finale téléchargeable. Ce module reste un simple client HTTP : il renvoie l'URL de l'image, jamais ne la télécharge/l'enregistre lui-même — ça reste le travail de ai/tools.py::_dispatch_add_generated_image (voir "Mes assets", auth/user_assets_dir.py), pour que ce module soit testable seul.""" import os import time import requests _BASE_URL = "https://api.cloud.scenario.com/v1" class ScenarioNotConfiguredError(Exception): """SCENARIO_API_KEY/SCENARIO_API_SECRET/SCENARIO_MODEL_ID absents — voir .env.example. SCENARIO_MODEL_ID est l'id du modèle entraîné sur le style graphique unique de Forge Engine (pas encore créé tant que l'entraînement Scenario n'a pas été fait).""" class ScenarioGenerationError(Exception): """Le job Scenario a échoué ou n'a pas répondu à temps.""" def _credentials() -> tuple[tuple[str, str], str]: api_key = os.environ.get("SCENARIO_API_KEY") api_secret = os.environ.get("SCENARIO_API_SECRET") model_id = os.environ.get("SCENARIO_MODEL_ID") if not api_key or not api_secret or not model_id: raise ScenarioNotConfiguredError( "SCENARIO_API_KEY/SCENARIO_API_SECRET/SCENARIO_MODEL_ID absents — voir .env.example." ) return (api_key, api_secret), model_id def generate_image_url( prompt: str, width: int = 768, height: int = 768, timeout: int = 120, poll_interval: int = 2 ) -> str: """Lance une génération txt2img et attend le résultat — renvoie l'URL finale de l'image (hébergée par Scenario, à télécharger par l'appelant). Bloquant (poll_interval secondes entre chaque vérification), borné par `timeout` secondes au total.""" auth, model_id = _credentials() response = requests.post( f"{_BASE_URL}/generate/txt2img", auth=auth, timeout=30, json={"prompt": prompt, "modelId": model_id, "width": width, "height": height, "numSamples": 1}, ) response.raise_for_status() job_id = response.json()["job"]["jobId"] deadline = time.monotonic() + timeout while time.monotonic() < deadline: status_response = requests.get(f"{_BASE_URL}/jobs/{job_id}", auth=auth, timeout=30) status_response.raise_for_status() job = status_response.json()["job"] if job["status"] == "success": asset_id = job["metadata"]["assetIds"][0] asset_response = requests.get(f"{_BASE_URL}/assets/{asset_id}", auth=auth, timeout=30) asset_response.raise_for_status() return str(asset_response.json()["asset"]["url"]) if job["status"] == "failure": raise ScenarioGenerationError("Scenario a échoué à générer l'image.") time.sleep(poll_interval) raise ScenarioGenerationError("Scenario n'a pas répondu à temps.")