import logging import os import time from typing import Any, Dict, List, Optional import replicate from engines.visual_prompt_engine import VisualPromptEngine logger = logging.getLogger("ONYX") class ImageGenerationEngine: def __init__(self): print("๐Ÿš€ IMAGE ENGINE INIT START") self.api_token = os.getenv("REPLICATE_API_TOKEN") if not self.api_token: print("โŒ TOKEN MANQUANT") raise Exception("REPLICATE_API_TOKEN manquant") self.client = replicate.Client(api_token=self.api_token) self.text_model = os.getenv( "ONYX_TEXT_IMAGE_MODEL", "black-forest-labs/flux-schnell" ) self.master_model = os.getenv( "ONYX_MASTER_IMAGE_MODEL", "black-forest-labs/flux-schnell" ) self.reference_model = os.getenv( "ONYX_REFERENCE_IMAGE_MODEL", "ideogram-ai/ideogram-character" ) self.max_images = int(os.getenv("ONYX_MAX_IMAGES", "2")) self.delay_between_images = int(os.getenv("ONYX_DELAY_BETWEEN_IMAGES", "12")) self.max_attempts_per_image = int(os.getenv("ONYX_MAX_ATTEMPTS_PER_IMAGE", "2")) self.aspect_ratio = os.getenv("ONYX_IMAGE_ASPECT_RATIO", "3:4") self.visual_prompt_engine = VisualPromptEngine() print("โœ… IMAGE ENGINE READY") print(f"๐Ÿ“ text_model = {self.text_model}") print(f"๐Ÿงฌ master_model = {self.master_model}") print(f"๐ŸŽญ reference_model = {self.reference_model}") print(f"๐Ÿช™ max_images = {self.max_images}") print(f"๐Ÿช™ max_attempts_per_image = {self.max_attempts_per_image}") def _clean(self, value: Any) -> str: return " ".join(str(value or "").strip().split()) def _is_rate_limit_error(self, error: Exception) -> bool: msg = str(error).lower() return ( "429" in msg or "rate limit" in msg or "too many requests" in msg or "throttled" in msg or "throttle" in msg ) def _is_credit_or_payment_error(self, error: Exception) -> bool: msg = str(error).lower() return ( "402" in msg or "payment required" in msg or "insufficient balance" in msg or "insufficient credit" in msg or "billing" in msg or "quota exceeded" in msg ) def _wait_before_retry(self, attempt: int): schedule = [10, 20] seconds = schedule[min(attempt, len(schedule) - 1)] logger.warning(f"โณ Retry in {seconds}s...") time.sleep(seconds) def _extract_output_url(self, output) -> Optional[str]: if isinstance(output, list) and output: output = output[0] if hasattr(output, "url"): return output.url if isinstance(output, str): return output return None def _run_model_once(self, model: str, model_input: Dict[str, Any]) -> Optional[str]: logger.info(f"๐ŸŽจ Calling Replicate model: {model}") logger.info(f"๐Ÿงช Input keys: {list(model_input.keys())}") output = self.client.run(model, input=model_input) return self._extract_output_url(output) def _run_model_safe(self, model: str, model_input: Dict[str, Any]) -> Optional[str]: for attempt in range(self.max_attempts_per_image): try: logger.info(f"๐ŸŽจ Try {attempt + 1}/{self.max_attempts_per_image} on {model}") image_url = self._run_model_once(model, model_input) if image_url and isinstance(image_url, str) and image_url.strip(): logger.info("โœ… Image generated") return image_url logger.warning("โš ๏ธ Empty image response") if attempt < self.max_attempts_per_image - 1: self._wait_before_retry(attempt) except Exception as e: logger.error(f"โŒ Image error on {model}: {e}") if self._is_rate_limit_error(e): logger.warning("๐Ÿšฆ Rate limit detected") if attempt < self.max_attempts_per_image - 1: self._wait_before_retry(attempt) continue return None if self._is_credit_or_payment_error(e): logger.warning("๐Ÿ’ณ Credit/payment issue detected -> stop immediate") return None if attempt < self.max_attempts_per_image - 1: self._wait_before_retry(attempt) logger.warning(f"โŒ Final image failure on {model}") return None def generate_image_once(self, prompt: str) -> Optional[str]: return self._run_model_once( self.text_model, {"prompt": prompt} ) def generate_safe(self, prompt: str) -> Optional[str]: return self._run_model_safe( self.text_model, {"prompt": prompt} ) def generate_batch(self, prompts: List[str]) -> List[str]: prompts = (prompts or [])[:self.max_images] if not prompts: return [] images: List[str] = [] for i, prompt in enumerate(prompts): logger.info(f"๐ŸŽฌ Text-only image {i + 1}/{len(prompts)}") img = self.generate_safe(prompt) if not img: logger.warning("โŒ One text-only image failed -> returning partial batch") break images.append(img) if i < len(prompts) - 1: logger.info(f"๐Ÿ•’ Anti-throttle pause: {self.delay_between_images}s") time.sleep(self.delay_between_images) return images def build_prompts_from_pipeline( self, scenes: List[str], visual_bible: Dict[str, Any], shot_plan: List[Dict[str, Any]], creative_mode: str ) -> List[str]: prompts: List[str] = [] limited_scenes = (scenes or [])[:self.max_images] for i, scene_text in enumerate(limited_scenes): shot = shot_plan[i] if i < len(shot_plan) else {} prompt = self.visual_prompt_engine.build_prompt( scene_text=self._clean(scene_text), visual_bible=visual_bible or {}, shot_plan=shot or {}, creative_mode=creative_mode ) prompts.append(prompt) return prompts def _build_master_reference_prompt( self, visual_bible: Dict[str, Any], creative_mode: str ) -> str: main_subject = self._clean(visual_bible.get("main_subject")) identity = self._clean(visual_bible.get("identity")) face = self._clean(visual_bible.get("face")) hair = self._clean(visual_bible.get("hair")) body = self._clean(visual_bible.get("body")) outfit = self._clean(visual_bible.get("outfit")) lighting = self._clean(visual_bible.get("lighting")) color_palette = self._clean(visual_bible.get("color_palette")) environment = self._clean(visual_bible.get("environment")) style_keywords = self._clean(visual_bible.get("style_keywords")) camera_language = self._clean(visual_bible.get("camera_language")) reference_master_prompt = self._clean(visual_bible.get("reference_master_prompt")) return f""" character master reference image, single recurring protagonist, identity lock reference MAIN SUBJECT: {main_subject} IDENTITY: {identity} {face} {hair} {body} {outfit} STYLE: {style_keywords} LIGHTING: {lighting} COLOR: {color_palette} ENVIRONMENT: {environment} CAMERA LANGUAGE: {camera_language} MASTER REFERENCE INSTRUCTION: {reference_master_prompt} IMPORTANT: - one single character only - neutral pose - front-facing or slight three-quarter view - face fully readable - same outfit that will be reused later - no action pose - no running - no fight move - no collage - no text - no logo - no watermark - clean composition - identity-first image """.strip() def _build_reference_scene_input( self, prompt: str, reference_image_url: str, creative_mode: str ) -> Dict[str, Any]: style_type = "Auto" if creative_mode == "anime": style_type = "Fiction" elif creative_mode in ["story", "emotional", "tiktok"]: style_type = "Realistic" return { "prompt": prompt, "character_reference_image": reference_image_url, "style_type": style_type, "aspect_ratio": self.aspect_ratio } def generate_master_reference( self, visual_bible: Dict[str, Any], creative_mode: str ) -> Optional[str]: master_prompt = self._build_master_reference_prompt( visual_bible=visual_bible, creative_mode=creative_mode ) logger.info("๐Ÿงฌ Generating master reference image...") return self._run_model_safe( self.master_model, {"prompt": master_prompt} ) def generate_batch_v3( self, scenes: List[str], visual_bible: Dict[str, Any], shot_plan: List[Dict[str, Any]], creative_mode: str ) -> List[str]: limited_scenes = (scenes or [])[:self.max_images] if not limited_scenes: return [] image_prompts = self.build_prompts_from_pipeline( scenes=limited_scenes, visual_bible=visual_bible, shot_plan=shot_plan, creative_mode=creative_mode ) master_reference_url = self.generate_master_reference( visual_bible=visual_bible, creative_mode=creative_mode ) if not master_reference_url: logger.warning("โš ๏ธ Master reference failed -> stop image pipeline to save credits") return [] logger.info(f"๐Ÿงฌ Master reference ready: {master_reference_url}") images: List[str] = [] for i, prompt in enumerate(image_prompts): logger.info(f"๐ŸŽฌ Reference-based scene {i + 1}/{len(image_prompts)}") model_input = self._build_reference_scene_input( prompt=prompt, reference_image_url=master_reference_url, creative_mode=creative_mode ) img = self._run_model_safe(self.reference_model, model_input) if not img: logger.warning("โŒ One reference-based image failed -> stop and return partial batch") break images.append(img) if i < len(image_prompts) - 1: logger.info(f"๐Ÿ•’ Anti-throttle pause: {self.delay_between_images}s") time.sleep(self.delay_between_images) return images def generate_batch_v2( self, scenes: List[str], visual_bible: Dict[str, Any], shot_plan: List[Dict[str, Any]], creative_mode: str ) -> List[str]: return self.generate_batch_v3( scenes=scenes, visual_bible=visual_bible, shot_plan=shot_plan, creative_mode=creative_mode )