MAAS / app /ads /image_service.py
Hammad712's picture
Update app/ads/image_service.py
aa1210b verified
import io
import logging
from typing import Tuple, List, Optional
from google import genai
from google.genai import types
# Assuming this is your schema file
from app.ads.schemas import ImageRequest, Persona, ToneEnum, GoalEnum
logger = logging.getLogger(__name__)
def _persona_to_text(persona: Persona) -> str:
"""Return a one-line description for a Persona model."""
interests = ", ".join(persona.interests) if persona.interests else "no listed interests"
return f"{persona.name} (age: {persona.age_range}, location: {persona.location}, interests: {interests})"
def generate_image(req: ImageRequest) -> Tuple[bytes, str]:
"""
Generate an Ad image using Gemini with a highly-structured prompt.
"""
# --- 1. Prepare dynamic prompt components ---
# Safely convert Persona objects into strings
if req.selected_personas and len(req.selected_personas) > 0:
personas_text = "; ".join(_persona_to_text(p) for p in req.selected_personas)
else:
personas_text = "general target audience"
# Define aspect ratio
aspect_ratio_str = f"{req.width}:{req.height}"
if aspect_ratio_str == "1200:628":
aspect_ratio_desc = "1.91:1 (Landscape Link Ad)"
elif aspect_ratio_str == "1080:1080":
aspect_ratio_desc = "1:1 (Square Feed Ad)"
elif aspect_ratio_str == "1080:1920":
aspect_ratio_desc = "9:16 (Vertical Story Ad)"
else:
aspect_ratio_desc = f"{aspect_ratio_str} (Custom)"
# Create strings for optional fields
brand_color_str = ""
if req.brand_colors:
brand_color_str = f"* **Color Palette:** Strictly use this palette as the primary and accent colors: {', '.join(req.brand_colors)}."
else:
brand_color_str = f"* **Color Palette:** Use a professional color palette (e.g., blues, greys, with one strong accent color) that matches the '{req.tone.value}' tone."
visual_pref_text = ""
if req.visual_preference:
visual_pref_text = f"The main visual *must* be: **'{req.visual_preference}'**. This should be the element that catches the eye first."
else:
visual_pref_text = "Create a single, strong, and uncluttered focal point that represents the offer (e.g., an abstract graphic of growth, a clean icon, or a person matching the persona)."
cta_text_str = ""
if req.cta_text:
cta_text_str = f"The banner *must* also include the *exact* phrase: **'{req.cta_text}'**. This can be in a button-like shape or as clear, bold text."
# --- 2. The New, Detailed, and Structured Prompt ---
prompt = f'''
**ROLE:** You are an expert Art Director at a world-class advertising agency.
**YOUR GOAL:** To generate a *single*, professional, and high-conversion ad banner for a Meta (Facebook/Instagram) campaign.
**YOUR TASK:** Generate one (1) image file based *only* on the strict hierarchy, components, and constraints below.
---
### 1. HIERARCHY OF IMPORTANCE (Strict Priority)
This is the order of what the user MUST see. Your design must follow this.
* **P1: FOCAL POINT (The Visual):**
* This is the **most important** element. It must stop the user from scrolling.
* It must be a single, clean, high-quality, professional image or graphic.
* **Visual Content:** {visual_pref_text}
* It must be the primary focus of the entire banner.
* **P2: HEADLINE (The Offer):**
* This is the **second most important** element. It is the primary text.
* It must be bold, clear, and easy to read in 1 second.
* **Text Content:** It must be the *exact* phrase: **'{req.offer_description}'**
* **P3: CALL-TO-ACTION (The Instruction):**
* This is the **third most important** element. It tells the user what to do next.
* It must be visually distinct (e.g., a button shape or contrasting bold text).
* **Text Content:** {cta_text_str}
* **P4: BRANDING (The Signature):**
* This is the **least important** element. It is for brand recognition only.
* It must be small, clean, and placed in a corner (e.g., bottom-left or top-right).
* **Text Content:** **'{req.business_name}'** (Render this as clean text, not a complex logo).
---
### 2. DESIGN & LAYOUT MANDATES
* **ASPECT RATIO:** **{aspect_ratio_desc} ({req.width}x{req.height}px).** This is a non-negotiable technical requirement.
* **STYLE & TONE:** **{req.style}, {req.tone.value}.**
* **TARGET AUDIENCE CONTEXT:** The entire design (font choice, imagery, colors) must feel professional and trustworthy to this user: **{personas_text}**.
* **COLOR PALETTE:** {brand_color_str}
---
### 3. WHAT TO AVOID (CRITICAL FAILURES)
* **DO NOT** add *any text* not explicitly listed in P2, P3, or P4.
* **DO NOT** clutter the image. White space is essential. The design must be **minimalist** and **clean**.
* **DO NOT** use generic, cheesy, or low-quality stock photos.
* **DO NOT** use hard-to-read, cursive, or overly-stylized fonts. Readability is the top priority for text.
* **DO NOT** make the branding (P4) large or the main focus. It must be subtle.
---
### FINAL OUTPUT
Generate *only* the final, polished ad banner image. Do not respond with text, questions, or comments.
'''
# --- 3. Execute the API Call ---
logger.info("Requesting Gemini image generation for business '%s'", req.business_name)
logger.debug("Full prompt:\n%s", prompt) # Good for debugging
try:
client = genai.Client()
response = client.models.generate_content(
model="gemini-2.5-flash-image",
contents=(prompt
),
config=types.GenerateContentConfig(
response_modalities=["text", "Image"]
),
)
# Parse Gemini response for images
for part in response.candidates[0].content.parts:
if getattr(part, "inline_data", None) and part.inline_data.data:
image_bytes = part.inline_data.data
return image_bytes, "image/png"
# Fallback: Gemini returned text (prompt or explanation)
for part in response.candidates[0].content.parts:
if getattr(part, "text", None):
logger.warning("Gemini returned text instead of image: %s", part.text[:300])
raise RuntimeError(f"Gemini returned text only: {part.text[:300]}...")
raise RuntimeError("No image data found in Gemini response.")
except AttributeError as e:
logger.exception(f"Failed to parse Gemini response. It's possible the API response structure is unexpected. {e}")
raise RuntimeError(f"Failed to parse Gemini response: {e}")
except Exception as e:
logger.exception("Gemini image generation failed: %s", e)
raise RuntimeError(f"Gemini image generation failed: {e}")