semblance-dev/backend/app/services/image_description_service.py
2025-08-04 09:07:59 -05:00

164 lines
No EOL
7.8 KiB
Python

"""
Image Description Service
This service generates detailed AI-powered descriptions of creative assets for focus group research.
It helps distinguish between multiple images in the conversation context.
"""
import os
import logging
from typing import Optional
from PIL import Image
from app.services.llm_service import LLMService, LLMServiceError
from app.services.conversation_context_service import ConversationContextService
from app.utils.prompt_loader import load_prompt, PromptLoaderError
logger = logging.getLogger(__name__)
class ImageDescriptionError(Exception):
"""Exception raised for errors in image description generation."""
pass
class ImageDescriptionService:
"""Service for generating AI-powered descriptions of creative assets."""
@staticmethod
def generate_description(focus_group_id: str, asset_filename: str) -> str:
"""
Generate a detailed AI description of a creative asset image.
Args:
focus_group_id: The focus group ID containing the asset
asset_filename: The filename of the asset to describe
Returns:
A detailed description of the image
Raises:
ImageDescriptionError: If description generation fails
"""
try:
print(f"🎨 DESCRIPTION: Generating AI description for {asset_filename}")
# Resolve the full path to the asset
asset_path = ConversationContextService._resolve_asset_path(focus_group_id, asset_filename)
print(f"🔍 DESCRIPTION: Resolved asset path: {asset_path}")
# Check if file exists
if not os.path.exists(asset_path):
print(f"❌ DESCRIPTION: File does not exist at path: {asset_path}")
# List files in the directory to help debug
asset_dir = os.path.dirname(asset_path)
if os.path.exists(asset_dir):
files_in_dir = os.listdir(asset_dir)
print(f"🔍 DESCRIPTION: Files in directory {asset_dir}: {files_in_dir}")
else:
print(f"❌ DESCRIPTION: Directory does not exist: {asset_dir}")
raise ImageDescriptionError(f"Asset file not found: {asset_path}")
# Verify the image can be loaded (optional validation)
try:
image = Image.open(asset_path)
print(f"🖼️ DESCRIPTION: Validated image {asset_filename} ({image.size[0]}x{image.size[1]})")
image.close() # Close the image since we're passing the path to LLM
except Exception as e:
raise ImageDescriptionError(f"Failed to validate image {asset_filename}: {str(e)}")
# Load the description prompt
try:
prompt = load_prompt('image-description', {})
print(f"📝 DESCRIPTION: Loaded description prompt ({len(prompt)} chars)")
except PromptLoaderError as e:
raise ImageDescriptionError(f"Failed to load description prompt: {str(e)}")
# Generate description using multimodal LLM
try:
print(f"🚀 DESCRIPTION: Calling LLM service with image: {asset_path}")
description = LLMService.generate_multimodal_content(
prompt=prompt,
image_paths=[asset_path],
temperature=0.7
)
print(f"✅ DESCRIPTION: LLM returned description ({len(description)} chars): {description[:100]}...")
return description.strip()
except LLMServiceError as e:
raise ImageDescriptionError(f"LLM description generation failed: {str(e)}")
except Exception as e:
print(f"⚠️ DESCRIPTION: LLM service failed, generating fallback description: {str(e)}")
# Generate fallback description based on image info
try:
# We already validated the image exists earlier, so we can safely open it
image = Image.open(asset_path)
width, height = image.size
image.close()
# Create a simple but useful fallback description
fallback_description = f"a {width}x{height} pixel marketing advertisement image"
print(f"✅ DESCRIPTION: Generated fallback description: '{fallback_description}'")
return fallback_description
except Exception as fallback_error:
error_msg = f"Failed to generate description for {asset_filename}: LLM failed ({str(e)}) and fallback failed ({str(fallback_error)})"
print(f"❌ DESCRIPTION: {error_msg}")
raise ImageDescriptionError(error_msg)
@staticmethod
def enhance_creative_review_question(original_question: str, asset_filename: str, description: str) -> str:
"""
Enhance a creative review question by incorporating AI-generated image description.
Args:
original_question: The original question text
asset_filename: The asset filename being referenced
description: The AI-generated description of the image
Returns:
Enhanced question text with detailed visual description
"""
try:
print(f"🔧 ENHANCEMENT: Enhancing question for {asset_filename}")
print(f"🔧 Original: {original_question[:100]}...")
print(f"🔧 Description: {description[:100]}...")
# Look for the asset filename reference in the question
if asset_filename in original_question:
# Find the filename reference and enhance it
filename_pattern = f"'{asset_filename}'"
enhanced_reference = f"'{asset_filename}' - {description}"
enhanced_question = original_question.replace(filename_pattern, enhanced_reference)
print(f"✅ ENHANCEMENT: Enhanced question: {enhanced_question[:150]}...")
return enhanced_question
else:
# If filename not found in expected format, try other patterns
import re
# Try to find quoted filename patterns
patterns = [
(f'"{asset_filename}"', f'"{asset_filename}" - {description}'),
(f"titled '{asset_filename}'", f"titled '{asset_filename}' - {description}"),
(f'titled "{asset_filename}"', f'titled "{asset_filename}" - {description}'),
(asset_filename, f"{asset_filename} - {description}")
]
enhanced_question = original_question
for old_pattern, new_pattern in patterns:
if old_pattern in enhanced_question:
enhanced_question = enhanced_question.replace(old_pattern, new_pattern)
print(f"✅ ENHANCEMENT: Enhanced with pattern '{old_pattern}': {enhanced_question[:150]}...")
return enhanced_question
# If no patterns match, append description at the end
enhanced_question = f"{original_question} The image shows {description}."
print(f"⚠️ ENHANCEMENT: Appended description to end: {enhanced_question[:150]}...")
return enhanced_question
except Exception as e:
error_msg = f"Failed to enhance question for {asset_filename}: {str(e)}"
print(f"❌ ENHANCEMENT: {error_msg}")
# Return original question if enhancement fails
return original_question