ghmk's picture
Initial deployment of Character Forge
5b6e956
"""
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