""" Composition Service =================== Business logic for smart multi-image composition. Builds intelligent prompts based on image types, camera angles, and lighting. """ from typing import Optional, List from PIL import Image from services.generation_service import GenerationService from models.generation_request import GenerationRequest from models.generation_result import GenerationResult from utils.logging_utils import get_logger from config.settings import Settings logger = get_logger(__name__) class CompositionService(GenerationService): """ Service for intelligent multi-image composition. Builds prompts based on: - Image types (Subject, Background, Style, etc.) - Camera angles and shot types - Lighting conditions - Custom instructions Inherits from GenerationService for generation capabilities. """ # Image type options IMAGE_TYPES = [ "Subject/Character", "Background/Environment", "Style Reference", "Product", "Texture", "Not Used" ] # Shot type options SHOT_TYPES = [ "close-up shot", "medium shot", "full body shot", "wide shot", "extreme close-up", "establishing shot" ] # Camera angle options CAMERA_ANGLES = [ "eye-level perspective", "low-angle perspective", "high-angle perspective", "bird's-eye view", "Dutch angle (tilted)", "over-the-shoulder" ] # Lighting options LIGHTING_OPTIONS = [ "Auto (match images)", "natural daylight", "golden hour sunlight", "soft diffused light", "dramatic side lighting", "backlit silhouette", "studio lighting", "moody atmospheric lighting", "neon/artificial lighting" ] def __init__(self, api_key: Optional[str] = None): """ Initialize composition service. Args: api_key: Optional Gemini API key """ super().__init__(api_key=api_key) logger.info("CompositionService initialized") def build_composition_prompt( self, image1_type: str = "Subject/Character", image2_type: str = "Background/Environment", image3_type: str = "Not Used", camera_angles: Optional[List[str]] = None, lighting: str = "Auto (match images)", shot_type: str = "medium shot", custom_instructions: str = "", is_character_sheet: bool = False ) -> str: """ Build intelligent composition prompt. Based on Google's best practices for Gemini 2.5 Flash Image: - Narrative, descriptive language - Camera angles, lens types, lighting - Match perspectives and light direction - Specific about placement Args: image1_type: Type of first image image2_type: Type of second image image3_type: Type of third image camera_angles: List of selected camera angles lighting: Lighting description shot_type: Type of shot custom_instructions: Additional instructions is_character_sheet: Whether to generate character sheet Returns: Formatted prompt string """ parts = [] # Character sheet specific handling if is_character_sheet: parts.append("Create a character sheet design with multiple views and poses of the same character. ") if image1_type == "Subject/Character": parts.append("Based on the character from image one, ") parts.append("Include front view, side view, back view, and detail shots. ") parts.append("Maintain consistent character design, colors, and proportions across all views. ") if image2_type in ["Background/Environment", "Style Reference"]: parts.append(f"Apply the {image2_type.lower()} from image two as context. ") else: # Determine main action based on image types if image1_type == "Subject/Character" and image2_type == "Background/Environment": parts.append(f"A photorealistic {shot_type} ") parts.append(f"placing the subject from image one into the environment from image two. ") elif image1_type == "Subject/Character" and image2_type == "Style Reference": parts.append(f"Transform the subject from image one ") parts.append(f"into the artistic style shown in image two. ") elif image1_type == "Background/Environment" and image2_type == "Subject/Character": parts.append(f"A photorealistic {shot_type} ") parts.append(f"integrating the subject from image two into the environment from image one. ") else: # Generic multi-image composition parts.append("Combine ") if image1_type != "Not Used": parts.append(f"the {image1_type.lower()} from image one") if image2_type != "Not Used": parts.append(f" with the {image2_type.lower()} from image two") if image3_type != "Not Used": parts.append(f" and the {image3_type.lower()} from image three") parts.append(". ") # Add camera angle specifics (not for character sheets) if camera_angles and not is_character_sheet: angles_text = ", ".join(camera_angles) parts.append(f"Shot from a {angles_text}. ") # Add lighting if lighting and lighting != "Auto (match images)": parts.append(f"The scene is illuminated by {lighting}, ") parts.append("matching the lighting direction and quality across all elements. ") # Add perspective matching (best practice) if not is_character_sheet: parts.append("Maintain consistent perspective, scale, and depth. ") # Add realism keywords parts.append("Create a natural, seamless composition with realistic shadows and reflections. ") parts.append("Photorealistic, high quality, professional photography.") # Add custom instructions if custom_instructions: parts.append(f" {custom_instructions}") return "".join(parts) def compose_images( self, images: List[Optional[Image.Image]], image_types: List[str], camera_angles: Optional[List[str]] = None, lighting: str = "Auto (match images)", shot_type: str = "medium shot", custom_instructions: str = "", is_character_sheet: bool = False, aspect_ratio: str = "16:9", temperature: float = 0.7, backend: str = Settings.BACKEND_GEMINI ) -> GenerationResult: """ Compose images using intelligent prompt generation. Args: images: List of up to 3 images (None for unused slots) image_types: List of image types corresponding to images camera_angles: Selected camera angles lighting: Lighting option shot_type: Shot type custom_instructions: Custom instructions is_character_sheet: Character sheet mode aspect_ratio: Output aspect ratio temperature: Generation temperature backend: Backend to use Returns: GenerationResult object """ try: # Filter out None images and corresponding types valid_images = [] valid_types = [] for i, img in enumerate(images): if img is not None and i < len(image_types): valid_images.append(img) valid_types.append(image_types[i]) if not valid_images: logger.error("No valid images provided") return GenerationResult.error_result("No images provided for composition") # Pad types to 3 elements while len(valid_types) < 3: valid_types.append("Not Used") # Build prompt prompt = self.build_composition_prompt( image1_type=valid_types[0], image2_type=valid_types[1], image3_type=valid_types[2], camera_angles=camera_angles or [], lighting=lighting, shot_type=shot_type, custom_instructions=custom_instructions, is_character_sheet=is_character_sheet ) logger.info(f"Composition prompt: {prompt[:200]}...") # Create request request = GenerationRequest( prompt=prompt, backend=backend, aspect_ratio=aspect_ratio, temperature=temperature, input_images=valid_images ) # Generate result = self.router.generate(request) if result.success: logger.info("Composition generated successfully") else: logger.warning(f"Composition failed: {result.message}") return result except Exception as e: logger.exception(f"Composition error: {e}") return GenerationResult.error_result(f"Composition error: {str(e)}") def get_suggested_aspect_ratio( self, shot_type: str, is_character_sheet: bool = False ) -> str: """ Suggest aspect ratio based on composition type. Args: shot_type: Shot type is_character_sheet: Character sheet mode Returns: Suggested aspect ratio string """ if is_character_sheet: return "16:9" # Wide format for multi-view layout if shot_type in ["full body shot", "establishing shot", "wide shot"]: return "16:9" # Landscape for wide shots elif shot_type in ["close-up shot", "extreme close-up"]: return "3:4" # Portrait for closeups else: return "1:1" # Square for balanced compositions def validate_composition_inputs( self, images: List[Optional[Image.Image]], image_types: List[str] ) -> tuple[bool, Optional[str]]: """ Validate composition inputs. Args: images: List of images image_types: List of image types Returns: Tuple of (is_valid: bool, error_message: Optional[str]) """ # Check at least one image provided if not any(img is not None for img in images): return False, "At least one image is required" # Check image types length matches if len(image_types) < len(images): return False, "Image types must be specified for all images" # Check for valid image types for img_type in image_types: if img_type not in self.IMAGE_TYPES: return False, f"Invalid image type: {img_type}" return True, None