sushilideaclan01's picture
add new things in the extensive flow
026f283
"""
Ad Generator Service
Generates high-converting ad creatives using psychological triggers, LLM copy,
and image generation. Uses maximum randomization for variety and saves to the
Neon database with optional R2 image storage.
"""
import asyncio
import json
import os
import random
import uuid
from datetime import datetime
from typing import Any, Dict, List, Optional
from config import settings
from data import auto_insurance, glp1, home_insurance
from data.frameworks import (
get_all_frameworks,
get_framework,
get_framework_visual_guidance,
get_frameworks_for_niche,
)
from data.ecom_verticals import get_random_vertical, get_verticals_for_prompt
from data.hooks import get_power_words, get_random_cta as get_hook_cta, get_random_hook_style
from data.triggers import get_random_trigger, get_trigger_combination, get_triggers_for_niche
from data.visuals import (
get_color_palette,
get_random_camera_angle,
get_random_composition,
get_random_lighting,
get_random_mood,
get_random_visual_style,
)
from services.generator_prompts import (
get_headline_formulas,
get_numbers_section,
get_trending_section,
get_trending_image_guidance,
)
from services.image import image_service
from services.llm import llm_service
from services.matrix import matrix_service
try:
from services.database import db_service
except ImportError:
db_service = None
try:
from services.r2_storage import get_r2_storage
r2_storage_available = True
except ImportError:
r2_storage_available = False
try:
from services.third_flow import third_flow_service
third_flow_available = True
except ImportError:
third_flow_available = False
try:
from services.trend_monitor import trend_monitor
trend_monitor_available = True
except ImportError:
trend_monitor_available = False
# -----------------------------------------------------------------------------
# Constants
# -----------------------------------------------------------------------------
NICHE_DATA = {
"home_insurance": home_insurance.get_niche_data,
"glp1": glp1.get_niche_data,
"auto_insurance": auto_insurance.get_niche_data,
}
# Age brackets for identity targeting (proven high-CTR pattern)
AGE_BRACKETS = [
{"label": "21-40", "color": "yellow/gold button"},
{"label": "41-64", "color": "blue button"},
{"label": "65+", "color": "red button"},
{"label": "50-60 years", "color": "gray box"},
{"label": "60-70 years", "color": "gray box"},
{"label": "70+ years old", "color": "gray box"},
]
# Vintage film visual styles
VINTAGE_FILM_STYLES = [
"grainy 8mm home movie footage from the 1970s, warm faded colors, light leaks",
"old VHS tape recording with tracking lines, fuzzy edges, dated look",
"vintage 35mm film photograph, visible grain, slightly overexposed highlights",
"retro Super 8 film aesthetic, soft focus, amber tint, nostalgic",
"aged polaroid photograph style, faded colors, white border, worn edges",
"1960s Kodachrome film look, saturated but faded reds and yellows",
"old TV broadcast footage, scan lines, slight color bleeding",
"vintage sepia-toned photograph, crackled texture, antique feel",
"worn 16mm documentary footage, high grain, muted earth tones",
"degraded archival footage look, dust particles, scratches, light decay",
]
# Film damage effects for authenticity
FILM_DAMAGE_EFFECTS = [
"film scratches, dust specks, light leaks in corners",
"vignette darkening at edges, slight color shift",
"horizontal scan lines, minor static noise",
"subtle frame jitter effect, worn sprocket marks",
"chemical staining, uneven development marks",
"faded edges with light burn, aged patina",
"dust particles floating, hair gate scratches",
"color bleeding at high contrast edges, emulsion damage",
]
class AdGenerator:
"""
Generates ad creatives: copy (LLM) + images, with randomization and DB/R2 save.
Sections:
- Init & config: output dir, local save, niche cache
- Niche & strategy: get niche data, compatible strategies, hooks, visuals
- CTAs & numbers: generate CTAs, prices, niche numbers
- Copy prompt: _build_copy_prompt (angle × concept, frameworks)
- Image prompt: _build_image_prompt, _refine_image_prompt
- Public: generate_ad, generate_ad_with_matrix, generate_ad_extensive, generate_batch
- Matrix: _build_matrix_ad_prompt, _build_matrix_image_prompt
- Refine: refine_custom_angle_or_concept
"""
# --- Init & config ---
def __init__(self):
"""Initialize the generator."""
self.output_dir = settings.output_dir
os.makedirs(self.output_dir, exist_ok=True)
# Cache niche data to avoid repeated function calls
self._niche_data_cache: Dict[str, Dict[str, Any]] = {}
def _should_save_locally(self) -> bool:
"""
Determine if images should be saved locally based on environment settings.
Returns:
True if images should be saved locally, False otherwise
"""
# In production, only save locally if explicitly enabled
if settings.environment.lower() == "production":
return settings.save_images_locally
# In development, always save locally
return True
def _save_image_locally(self, image_bytes: bytes, filename: str) -> Optional[str]:
"""
Conditionally save image locally based on environment settings.
Args:
image_bytes: Image data to save
filename: Filename for the image
Returns:
Filepath if saved, None otherwise
"""
if not self._should_save_locally():
return None
try:
filepath = os.path.join(self.output_dir, filename)
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "wb") as f:
f.write(image_bytes)
return filepath
except Exception as e:
print(f"Warning: Failed to save image locally: {e}")
return None
# ========================================================================
# DATA RETRIEVAL & CACHING METHODS
# ========================================================================
# --- Niche & strategy ---
def _get_minimal_niche_data_for_custom(self, niche: str) -> Dict[str, Any]:
"""Return minimal niche data for custom/others niche (no raise). Used when niche is not in NICHE_DATA."""
label = (niche or "").replace("_", " ").title() or "Custom"
return {
"niche": niche,
"strategies": {},
"strategy_names": [],
"creative_directions": ["professional", "clean", "trustworthy"],
"visual_moods": ["neutral", "approachable"],
"niche_guidance": f"Focus on the {label} niche. Use authentic, low-production visuals.",
"image_guidance": f"Images should be appropriate for {label}. Authentic, relatable visuals.",
"image_niche_guidance_short": f"NICHE: {label}",
"number_config": {},
"price_config": {},
"prompt_sanitization_replacements": [],
"visual_library": {},
"image_creative_prompts": [],
"all_hooks": [],
"all_visual_styles": [],
"copy_templates": [],
}
def _get_niche_data(self, niche: str) -> Dict[str, Any]:
"""Load data for a specific niche (cached). For custom/others niche, returns minimal data instead of raising."""
if niche not in self._niche_data_cache:
if niche in NICHE_DATA:
self._niche_data_cache[niche] = NICHE_DATA[niche]()
else:
# Custom/others niche (e.g. "Roofing Programme") — use minimal data so image refinement/save don't crash
self._niche_data_cache[niche] = self._get_minimal_niche_data_for_custom(niche)
return self._niche_data_cache[niche]
# ========================================================================
# STRATEGY & COMPATIBILITY METHODS
# ========================================================================
def _get_framework_strategy_compatibility(self, framework_key: str, strategy_name: str) -> float:
"""
Calculate compatibility score between framework and strategy.
Returns score 0.0-1.0, higher = better match.
"""
# Framework-Strategy compatibility matrix
compatibility_map = {
"breaking_news": {
"accusation_opener": 0.9,
"curiosity_gap": 0.95,
"price_focused": 0.8,
"proof_based": 0.7,
"authority_backed": 0.85,
"urgent": 0.95,
},
"testimonial": {
"proof_based": 0.95,
"authority_backed": 0.8,
"social_proof": 0.9,
},
"before_after": {
"proof_based": 0.95,
"transformation": 0.9,
"price_focused": 0.8,
},
"problem_solution": {
"accusation_opener": 0.85,
"problem_awareness": 0.95,
"solution_focused": 0.9,
},
"authority": {
"authority_backed": 0.95,
"expert_recommended": 0.9,
"proof_based": 0.8,
},
"lifestyle": {
"aspirational": 0.9,
"identity_targeted": 0.85,
"emotional": 0.8,
},
"comparison": {
"price_focused": 0.9,
"proof_based": 0.8,
"comparison_logic": 0.95,
},
"storytelling": {
"emotional": 0.9,
"relatable": 0.85,
"transformation": 0.8,
},
"mobile_post": {
"convenience": 0.9,
"quick_action": 0.85,
"simple": 0.9,
},
"educational": {
"authority_backed": 0.8,
"curiosity_gap": 0.85,
"informative": 0.9,
},
}
# Normalize strategy name (remove spaces, lowercase)
strategy_key = strategy_name.lower().replace(" ", "_").replace("-", "_")
# Check direct match
if framework_key in compatibility_map:
if strategy_key in compatibility_map[framework_key]:
return compatibility_map[framework_key][strategy_key]
# Default compatibility (still usable, just not optimal)
return 0.6
def _select_compatible_strategies(self, niche_data: Dict, framework_key: str, count: int = 2) -> List[Dict]:
"""
Select strategies that are compatible with the chosen framework.
Prioritizes high-compatibility matches but ensures variety.
"""
all_strategies = list(niche_data.get("strategies", {}).values())
if not all_strategies:
return []
# Score all strategies
scored_strategies = [
(strategy, self._get_framework_strategy_compatibility(framework_key, strategy["name"]))
for strategy in all_strategies
]
# Sort by compatibility (highest first)
scored_strategies.sort(key=lambda x: x[1], reverse=True)
# Select mix: 70% top compatible, 30% random for variety
top_count = max(1, int(count * 0.7))
selected = []
# Add top compatible
for strategy, score in scored_strategies[:top_count]:
selected.append(strategy)
# Add random for variety (if we need more)
remaining = count - len(selected)
if remaining > 0:
remaining_strategies = [s for s, _ in scored_strategies[top_count:]]
if remaining_strategies:
selected.extend(random.sample(remaining_strategies, min(remaining, len(remaining_strategies))))
return selected[:count]
def _random_strategies(self, niche_data: Dict, count: int = 2) -> List[Dict]:
"""Randomly select strategies for maximum variety."""
strategy_names = random.sample(niche_data["strategy_names"], min(count, len(niche_data["strategy_names"])))
return [niche_data["strategies"][name] for name in strategy_names]
def _random_hooks(self, strategies: List[Dict], count: int = 3) -> List[str]:
"""Randomly select hooks from the chosen strategies (optimized list building)."""
# Optimized: use list comprehension instead of extend in loop
all_hooks = [hook for strategy in strategies for hook in strategy["hooks"]]
return random.sample(all_hooks, min(count, len(all_hooks))) if all_hooks else []
# ========================================================================
# VISUAL SELECTION METHODS
# ========================================================================
def _get_visual_library_for_niche(self, niche: str) -> Dict[str, List[str]]:
"""
Get visual library categories for a niche.
Returns dict mapping category names to visual descriptions.
"""
niche_data = self._get_niche_data(niche)
return niche_data.get("visual_library", {})
def _select_visuals_from_library(self, niche: str, strategy_name: str, count: int = 2) -> List[str]:
"""
Select visuals from the comprehensive visual library based on strategy.
This ensures we use the full visual library, not just strategy-specific visuals.
"""
visual_library = self._get_visual_library_for_niche(niche)
if not visual_library:
# Fallback to strategy visuals
return []
# Map strategies to visual categories (auto_insurance: 1:1 with the 6 ad formats)
auto_insurance_strategy_categories = {
"official_notification": ["official_notification_style"],
"social_post": ["social_post_style"],
"coverage_tiers": ["coverage_tiers_style"],
"car_brand_grid": ["car_brand_grid_style"],
"gift_card_cta": ["gift_card_cta_style"],
"savings_urgency": ["savings_urgency_style"],
}
default_strategy_to_category = {
"accusation_opener": ["problem_risk", "disaster_fear"],
"curiosity_gap": ["text_first", "minimal_symbolic"],
"price_focused": ["comparison_choice", "mortgage_bank"],
"proof_based": ["relief", "protection_safety"],
"authority_backed": ["mortgage_bank", "protection_safety"],
"identity_targeted": ["first_time_homebuyer", "lifestyle"],
"family_emotional": ["family_emotional", "protection_safety"],
"fear_based": ["disaster_fear", "problem_risk"],
"relief": ["relief", "protection_safety"],
}
strategy_to_category = (
auto_insurance_strategy_categories
if niche == "auto_insurance"
else default_strategy_to_category
)
# Normalize strategy name
strategy_key = strategy_name.lower().replace(" ", "_").replace("-", "_")
# Get relevant categories (only use categories that exist in this niche's library)
requested = strategy_to_category.get(strategy_key, list(visual_library.keys()))
categories = [c for c in requested if c in visual_library] or list(visual_library.keys())
# Select visuals from relevant categories
selected_visuals = []
for category in categories[:2]: # Use top 2 categories
if category in visual_library:
visuals = visual_library[category]
if visuals:
selected_visuals.extend(random.sample(visuals, min(1, len(visuals))))
# If we need more, add random from any category (optimized: use list comprehension)
if len(selected_visuals) < count:
all_visuals = [v for visuals in visual_library.values() for v in visuals]
remaining = count - len(selected_visuals)
if all_visuals:
selected_visuals.extend(random.sample(all_visuals, min(remaining, len(all_visuals))))
return selected_visuals[:count]
def _random_visual_styles(self, strategies: List[Dict], count: int = 2, niche: str = "", use_library: bool = True) -> List[str]:
"""
Select visual scene descriptions from strategies and/or visual library.
Note: These are SCENE DESCRIPTIONS (what to show), not aesthetic styles.
Aesthetic styles come from data/visuals.py VISUAL_STYLES.
Args:
strategies: List of strategy dicts
count: Number of visuals to select
niche: Niche name for visual library access
use_library: Whether to use comprehensive visual library (improvement)
"""
# Optimized: use list comprehension instead of extend in loops
# Get strategy-specific visuals
all_styles = [style for strategy in strategies for style in strategy.get("visual_styles", [])]
# Add visual library visuals (improvement)
if use_library and niche:
library_visuals = [
v for strategy in strategies
for v in self._select_visuals_from_library(niche, strategy.get("name", ""), count=1)
]
all_styles.extend(library_visuals)
# Remove duplicates while preserving order (optimized using dict.fromkeys)
unique_styles = list(dict.fromkeys(all_styles))
return random.sample(unique_styles, min(count, len(unique_styles))) if unique_styles else []
# ========================================================================
# NICHE & CONTENT CONFIGURATION METHODS
# ========================================================================
# --- CTAs & numbers ---
def _get_niche_specific_guidance(self, niche: str) -> str:
"""Get niche-specific guidance for the prompt."""
niche_data = self._get_niche_data(niche)
return niche_data.get("niche_guidance", "")
async def _generate_ctas_async(
self, niche: str, framework_name: Optional[str] = None
) -> List[str]:
"""
Generate 5–8 CTAs for the niche (and optional framework) via LLM.
Returns empty list on failure; caller uses single default when empty.
"""
niche_label = (niche or "").replace("_", " ").title() or "general advertising"
context = f"Niche: {niche_label}"
if framework_name:
context += f". Ad format/framework: {framework_name}"
prompt = f"""Generate 5 to 8 short call-to-action (CTA) button/link phrases for a paid ad.
{context}
Rules:
- Generate diverse, scroll-stopping CTAs; niche is optional context; any tone or style allowed.
- Return ONLY a JSON object with one key "ctas" containing an array of strings. No other text."""
try:
result = await llm_service.generate_json(prompt=prompt, temperature=0.7)
ctas = result.get("ctas") if isinstance(result, dict) else None
if isinstance(ctas, list) and len(ctas) > 0:
return [str(c).strip() for c in ctas if c]
except Exception:
pass
return []
def _generate_specific_price(self, niche: str) -> str:
"""
Generate price guidance for the AI.
The AI will decide whether to include prices and what amounts to use.
"""
niche_data = self._get_niche_data(niche)
default = "Use contextually appropriate prices if the ad format requires them. Make them oddly specific (not rounded) for believability."
return niche_data.get("price_config", {}).get("guidance", default)
def _generate_niche_numbers(self, niche: str) -> Dict[str, str]:
"""Generate niche-specific numbers for authenticity from niche number_config."""
niche_data = self._get_niche_data(niche)
config = niche_data.get("number_config", {})
if not config:
return {}
num_type = config.get("type", "savings")
labels = config.get("labels", {})
if num_type == "savings":
before_range = config.get("before_range", [1200, 2400])
savings_range = config.get("savings_pct_range", [0.50, 0.75])
before = random.randint(before_range[0], before_range[1])
savings_pct = random.uniform(savings_range[0], savings_range[1])
after = int(before * (1 - savings_pct))
return {
"type": "savings",
"before": f"${before:,}/year",
"after": f"${after}/year",
"difference": f"${before - after:,}",
"metric": labels.get("metric", "savings per year"),
}
if num_type == "weight_loss":
before_range = config.get("before_range", [180, 280])
loss_range = config.get("loss_range", [25, 65])
days_options = config.get("days_options", [60, 90, 120])
sizes_range = config.get("sizes_range", [2, 5])
before_weight = random.randint(before_range[0], before_range[1])
lbs_lost = random.randint(loss_range[0], loss_range[1])
after_weight = before_weight - lbs_lost
days = random.choice(days_options)
sizes_dropped = random.randint(sizes_range[0], sizes_range[1])
return {
"type": "weight_loss",
"before": f"{before_weight} lbs",
"after": f"{after_weight} lbs",
"difference": f"{lbs_lost} lbs",
"days": f"{days} days",
"sizes": f"{sizes_dropped} dress sizes",
"metric": labels.get("metric", "pounds lost"),
}
return {}
# --- Copy prompt ---
def _build_copy_prompt(
self,
niche: str,
niche_data: Dict,
strategies: List[Dict],
hooks: List[str],
creative_direction: str,
framework: str,
framework_data: Dict[str, Any],
cta: str,
trigger_data: Dict[str, Any] = None,
trigger_combination: Dict[str, Any] = None,
power_words: List[str] = None,
angle: Dict[str, Any] = None,
concept: Dict[str, Any] = None,
target_audience: Optional[str] = None,
offer: Optional[str] = None,
trending_context: Optional[str] = None,
) -> str:
"""
Build professional LLM prompt for ad copy generation.
Uses angle × concept matrix approach for psychological targeting.
Can optionally incorporate trending topics for increased relevance.
"""
strategy_names = [s["name"] for s in strategies]
strategy_descriptions = [f"- {s['name']}: {s['description']}" for s in strategies]
niche_guidance = self._get_niche_specific_guidance(niche)
# Same framework drives ad idea, copy angle, AND visual format (no separate container)
price_guidance = self._generate_specific_price(niche)
niche_numbers = self._generate_niche_numbers(niche)
age_bracket = random.choice(AGE_BRACKETS)
# Numbers and headline formulas from shared prompt content
num_type = niche_data.get("number_config", {}).get("type", "savings")
numbers_section = get_numbers_section(
niche, num_type, niche_numbers, age_bracket, price_guidance
)
headline_formulas = get_headline_formulas(niche, num_type)
trending_section = get_trending_section(trending_context)
prompt = f"""You are an elite direct-response copywriter who has reverse-engineered hundreds of 7-8 figure Facebook ad campaigns. You understand the psychology of scroll-stopping creatives that bypass ad-blindness and trigger immediate emotional response.
=== CONTEXT ===
NICHE: {niche.replace("_", " ").title()}
ADVERTISING FRAMEWORK: {framework}
FRAMEWORK DESCRIPTION: {framework_data.get('description', '')}
FRAMEWORK TONE: {framework_data.get('tone', '')}
FRAMEWORK VISUAL STYLE: {framework_data.get('visual_style', '')}
FRAMEWORK HEADLINE STYLE: {framework_data.get('headline_style', '') or 'N/A'}
CREATIVE DIRECTION: {creative_direction}
CALL-TO-ACTION: {cta}
{trending_section}
=== ANGLE × CONCEPT (inspiration—invent or extend) ===
ANGLE: {angle.get('name') if angle else 'N/A'} | Trigger: {angle.get('trigger') if angle else 'N/A'} | Example: "{angle.get('example') if angle else 'N/A'}"
CONCEPT: {concept.get('name') if concept else 'N/A'} | Structure: {concept.get('structure') if concept else 'N/A'} | Visual: {concept.get('visual') if concept else 'N/A'}
Use the above or invent your own triggers/angles/concepts.
{f'=== USER INPUTS ===' if target_audience or offer else ''}
{f'TARGET AUDIENCE: {target_audience}' if target_audience else ''}
{f'OFFER: {offer}' if offer else ''}
Target audience can be the given one or any hyperrealistic segment you invent (including outside the niche). You may invent or mix demographics, psychographics, and life stages for maximum diversity and scroll-stopping relevance.
For variety, use angles, concepts, and motivators suited to different ecom verticals: fashion, beauty, supplements, fitness, electronics, home goods, pets, food & beverage, jewelry, subscription. This creative can lean into: {get_random_vertical()['name']}.
=== FRAMEWORK VISUAL FORMAT (same framework for copy + image) ===
FRAMEWORK: {framework_data.get('name', framework)}
DESCRIPTION: {framework_data.get('description', '')}
VISUAL GUIDANCE: {get_framework_visual_guidance(framework_data.get('key', ''))}
FONT STYLE: {framework_data.get('font_style', '')}
COLORS: {', '.join(f'{k}: {v}' for k, v in framework_data.get('colors', {}).items())}
AUTHENTICITY TIPS: {', '.join(framework_data.get('authenticity_tips', [])[:3])}
{numbers_section}
=== PSYCHOLOGICAL STRATEGIES TO DEPLOY ===
{chr(10).join(strategy_descriptions)}
=== PSYCHOLOGICAL TRIGGERS ===
PRIMARY TRIGGER: {trigger_data.get('name', 'N/A') if trigger_data else 'N/A'}
DESCRIPTION: {trigger_data.get('description', '') if trigger_data else ''}
COPY ANGLES: {', '.join(trigger_data.get('copy_angles', [])[:3]) if trigger_data else 'N/A'}
TRIGGER COMBINATION: {trigger_combination.get('name', 'N/A') if trigger_combination else 'N/A'}
COMBINATION DESCRIPTION: {trigger_combination.get('description', '') if trigger_combination else ''}
=== POWER WORDS TO USE ===
Incorporate these power words naturally: {', '.join(power_words) if power_words else 'N/A'}
=== HOOK INSPIRATION (create your own powerful variation) ===
{chr(10).join(f'- "{hook}"' for hook in hooks)}
{niche_guidance}
{headline_formulas}
Use as inspiration only; invent new scroll-stopping or clickbait headlines as needed.
=== YOUR MISSION ===
Create a scroll-stopping Facebook ad that looks like organic content. Use the angle/concept/trigger above (or invent your own). No restrictions—clickbait, bold statements, and any audience are allowed.
=== OUTPUT REQUIREMENTS ===
1. HEADLINE (The "Arrest")
- MAXIMUM 10 words; instant pattern interrupt. Use the angle and trigger above.
- Include numbers/prices only when they strengthen the hook; use oddly specific amounts (e.g. $97.33). Include demographic targeting where appropriate. Be SPECIFIC and EMOTIONAL.
2. PRIMARY TEXT (The "Agitation")
- 2-3 punchy sentences that amplify the emotional hook. Reference the demographic; create urgency; make them FEEL the pain or desire. Oddly specific numbers when they fit.
3. DESCRIPTION (The "Payoff")
- ONE powerful sentence (max 10 words). Create action urgency. Oddly specific metrics when they enhance the message.
4. IMAGE BRIEF (CRITICAL - must match {framework_data.get('name', framework)} framework style)
- Follow the concept above: {concept.get('structure') if concept else 'authentic visual'}
- Describe the scene for the {framework_data.get('name', framework)} framework ONLY
- Visual guidance: {get_framework_visual_guidance(framework_data.get('key', ''))}
- The image should look like ORGANIC CONTENT, not an ad
- Include: setting, props, mood. People are OPTIONAL—creatives can be product-only, layout-only, document-only, or text-only; only add people when the concept clearly calls for them.
- Follow framework authenticity tips: {', '.join(framework_data.get('authenticity_tips', [])[:2])}
- CRITICAL: Use ONLY {framework_data.get('name', framework)} format - DO NOT mix with other formats
- {f"If chat-style framework (e.g. iMessage, WhatsApp): Include 2-4 readable, coherent messages related to {niche.replace('_', ' ').title()}. Use the headline or a variation as one message." if 'chat_style' in framework_data.get('tags', []) else ""}
- {f"If document-style framework (e.g. memo, email): Include readable, properly formatted text related to {niche.replace('_', ' ').title()}." if 'document_style' in framework_data.get('tags', []) else ""}
- FOR AUTO INSURANCE: Describe ONLY one of these 6 ad-format layouts: (1) official notification (seal, rate buttons), (2) social post card, (3) coverage tier panels, (4) car brand grid, (5) gift card CTA, (6) savings/urgency (yellow, CONTACT US). No other creative types. Do NOT describe testimonial portraits, couples, speech bubbles, quote bubbles, or people holding documents. Do NOT describe elderly or senior people. Typography, layout, prices, and buttons only. All text in the image must be readable and correctly spelled; no gibberish.
- FOR HOME INSURANCE: Document, savings proof, home setting. People optional (e.g. document on table only, or person with document).
- FOR GLP-1: REQUIRED - every image brief MUST describe either (1) a GLP-1 medication bottle or pen visible in the scene (e.g. Ozempic, Wegovy, Mounjaro, Zepbound pen or box), OR (2) the text "GLP-1" or a medication name visible on a label, screen, or document. Use VARIETY: product-only (bottle/pen, screen), quiz interfaces, document/surface, or with people when the concept calls for it. People optional.
- Visuals and concepts can be invented and need not match niche stereotypes; aim for hyperrealistic, diverse representation (any age, demographic, psychographic).
=== PSYCHOLOGICAL PRINCIPLES ===
- Loss Aversion: Make them feel what they're losing/missing
- Specificity = Believability: Specific numbers beat round numbers
- Identity Targeting: Direct demographic callouts create self-selection
- Curiosity Gap: "THIS" and "Instead" demand click to close loop
- Social Proof: "Thousands are doing X" triggers herd behavior
- Native Disguise: Content that doesn't look like an ad bypasses filters
=== CRITICAL RULES ===
1. Use the niche as inspiration only—you may invent or blend angles, concepts, triggers, and audiences from any domain for maximum diversity.
2. Use SPECIFIC numbers from the numbers section above when they fit; invent or adapt when it strengthens the hook.
3. ALWAYS create curiosity gap with "THIS", "Instead", "After", "Secret"; use any clickbait or bold phrasing that works.
4. NEVER look like an ad - look like NEWS, PROOF, or UGC
5. Use ACCUSATION framing for maximum impact
6. The image MUST match the {framework_data.get('name', framework)} framework style
=== OUTPUT FORMAT (JSON) ===
{{
"headline": "<10 words, pattern interrupt>",
"primary_text": "<2-3 emotional sentences>",
"description": "<one sentence, max 10 words>",
"body_story": "<8-12 sentence story: relatable pain, tension, transformation, hope, soft CTA; first/second person>",
"image_brief": "<scene following concept and {framework_data.get('name', framework)} style; organic feel>",
"cta": "{cta}",
"psychological_angle": "<angle name or primary trigger>",
"why_it_works": "<brief mechanism>"
}}
Generate the ad copy now. Organic content, immediate emotional response."""
return prompt
# --- Image prompt ---
def _build_image_prompt(
self,
niche: str,
ad_copy: Dict[str, Any],
visual_styles: List[str],
visual_mood: str,
camera_angle: str,
lighting: str,
composition: str,
visual_style_data: Optional[Dict[str, Any]] = None,
trending_context: Optional[str] = None,
) -> str:
"""
Build professional image generation prompt.
Uses detailed specifications, style guidance, and negative prompts.
Creates AUTHENTIC, ORGANIC CONTENT aesthetic.
Text (if included) should be part of the natural scene, NOT an overlay.
When trending_context is set, mood and atmosphere align with the current occasion.
"""
image_brief = ad_copy.get("image_brief", "")
headline = ad_copy.get("headline", "")
psychological_angle = ad_copy.get("psychological_angle", "")
# GLP-1: optionally use spreadsheet-style image creative prompts (Creative ad images sheet)
visual_scene_description = image_brief
if niche == "glp1":
niche_data_img = self._get_niche_data(niche)
creative_prompts = niche_data_img.get("image_creative_prompts") or []
if creative_prompts and random.random() < 0.5:
chosen = random.choice(creative_prompts)
visual_scene_description = chosen.get("image_prompt", image_brief)
# Same framework used for copy is used for visual (no separate container)
framework_key = ad_copy.get("framework_key") or ad_copy.get("container_key", "")
framework_name = ad_copy.get("framework") or ad_copy.get("container_used", "Standard Ad")
price_anchor = ad_copy.get("price_anchor", "$97")
framework_data_img = get_framework(framework_key) if framework_key else None
if not framework_data_img:
framework_data_img = {"name": framework_name, "visual_style": "Standard ad format"}
# Select visual style (use visuals.py data if available, otherwise strategy visuals)
if visual_style_data and isinstance(visual_style_data, dict):
visual_style = visual_style_data.get("prompt_guidance", "")
visual_style_name = visual_style_data.get("name", "")
else:
visual_style = random.choice(visual_styles) if visual_styles else ""
visual_style_name = ""
# Randomly decide which elements to include (for variety)
include_vintage_effects = random.random() < 0.7 # 70% chance
include_text_overlay = random.random() < 0.8 # 80% chance (headline on image)
include_framework_format = random.random() < 0.4 # 40% chance (many images clean without framework UI style)
# Select vintage film style and damage effects (only if including vintage)
vintage_style = random.choice(VINTAGE_FILM_STYLES) if include_vintage_effects else None
film_damage = random.choice(FILM_DAMAGE_EFFECTS) if include_vintage_effects else None
# Get color palette from visuals.py based on trigger/mood
color_palette = get_color_palette(visual_mood.lower().replace(" ", "_").replace("-", "_"))
# Auto insurance ad-format graphics: allow headline + prices/rates/CTA as part of layout
is_auto_insurance_ad_format = niche == "auto_insurance"
# Text styling options for variety - natural text in scene (NO overlays/banners)
text_positions = [
"naturally integrated into the scene",
"as part of a document or sign in the image",
"on a surface within the scene (wall, paper, etc.)",
"as natural text element in the environment",
"integrated into the scene naturally",
]
text_position = random.choice(text_positions)
text_colors = [
"natural text color that fits the scene",
"text that appears naturally in the environment",
"text that looks like it belongs in the scene",
"authentic text appearance, not overlaid",
"text as part of the natural scene elements",
"realistic text that fits the environment",
]
text_color = random.choice(text_colors)
# Niche-specific image guidance (for auto_insurance: no forced subjects/props; for GLP-1: bottle or name required)
if niche == "auto_insurance":
niche_data = self._get_niche_data(niche)
niche_image_guidance = (niche_data.get("image_guidance", "") + """
PEOPLE, FACES, AND CARS ARE OPTIONAL. Only include them when the VISUAL SCENE description explicitly mentions them. Most ad formats are typography, layout, and buttons only.
NO fake or made-up brand/company names (no gibberish); use generic text only or omit. NO in-car dashboard mockups or screens inside car interiors; stick to the 6 defined ad formats only (official notification, social post, coverage tiers, car brand grid, gift card CTA, savings/urgency)."""
)
elif niche == "glp1":
niche_data = self._get_niche_data(niche)
niche_image_guidance = (niche_data.get("image_guidance", "") + """
CRITICAL - GLP-1 PRODUCT VISIBILITY: The image MUST show at least one of: (1) A GLP-1 medication bottle or injectable pen (e.g. Ozempic, Wegovy, Mounjaro, Zepbound) in the scene, OR (2) The text "GLP-1" or a medication name (Ozempic, Wegovy, Mounjaro, etc.) visible on a label, screen, document, or surface. Do not generate a GLP-1 ad image without the product or name visible."""
)
else:
niche_data = self._get_niche_data(niche)
niche_image_guidance = niche_data.get("image_guidance", "")
# Framework visual guidance (same framework as copy)
framework_visual_guidance = get_framework_visual_guidance(framework_key) if framework_key else (framework_data_img.get("visual_style") or framework_data_img.get("visual_guidance", ""))
is_chat_style = "chat_style" in framework_data_img.get("tags", [])
is_document_style = "document_style" in framework_data_img.get("tags", [])
framework_format_section = f"""
=== FRAMEWORK FORMAT REQUIREMENTS ===
CRITICAL: Use ONLY the {framework_data_img.get('name', 'Standard Ad')} framework style. DO NOT mix multiple formats.
Framework: {framework_data_img.get('name', 'Standard Ad')}
Visual Guidance: {framework_visual_guidance}
REQUIREMENTS:
1. USE ONLY THIS FRAMEWORK STYLE - NO mixing (e.g. no WhatsApp + memo, no iMessage + document)
2. NO decorative borders, frames, or boxes
3. NO banners, badges, or logos
4. NO overlay boxes or rectangular overlays
5. Focus on authentic, natural appearance of the {framework_data_img.get('name', 'Standard Ad')} format only
{"=== TEXT REQUIREMENTS FOR CHAT-STYLE FRAMEWORKS ===" if is_chat_style else ""}
{"CRITICAL: All text in chat bubbles MUST be:" if is_chat_style else ""}
{"- READABLE and COHERENT (not gibberish, not placeholder text)" if is_chat_style else ""}
{f"- Realistic conversation text related to {niche.replace('_', ' ').title()}" if is_chat_style else ""}
{"- Proper spelling and grammar" if is_chat_style else ""}
{"- Natural message flow (2-4 messages max)" if is_chat_style else ""}
{"- Use the headline or a variation as one of the messages" if is_chat_style else ""}
{"- NO placeholder text like 'lorem ipsum' or random characters" if is_chat_style else ""}
{"=== TEXT REQUIREMENTS FOR DOCUMENT-STYLE FRAMEWORKS ===" if is_document_style else ""}
{"CRITICAL: All text in documents MUST be:" if is_document_style else ""}
{"- READABLE and COHERENT" if is_document_style else ""}
{f"- Related to {niche.replace('_', ' ').title()} topic" if is_document_style else ""}
{"- Proper formatting (title, body text, etc.)" if is_document_style else ""}
{"- NO gibberish or placeholder text" if is_document_style else ""}
"""
# Build flexible prompt based on what to include
vintage_section = ""
if include_vintage_effects and vintage_style and film_damage:
vintage_section = f"""
=== VINTAGE FILM AESTHETIC (OPTIONAL - apply if it fits the style) ===
- {vintage_style}
- Film damage: {film_damage}
- Warm, faded colors
- Visible grain throughout
"""
framework_section = ""
if include_framework_format:
# 40% of images use the same framework's visual style (e.g. iMessage, Reddit)
framework_section = f"""
{framework_format_section}
CRITICAL REMINDERS:
- Use ONLY {framework_data_img.get('name', 'Standard Ad')} format - NO mixing with other formats
- If chat-style framework: All text MUST be readable, coherent, and related to the {niche.replace('_', ' ').title()} niche
- If document-style framework: All text MUST be readable and properly formatted
- NO gibberish, placeholder text, or random characters
- NO decorative borders, frames, or boxes
"""
else:
# 60% of images: clean, natural (no app/screenshot style)
framework_section = """
=== STYLE GUIDANCE (NO FRAMEWORK UI STYLE) ===
- Natural, authentic image - no app/screenshot style
- Must NOT look like a polished advertisement
- Should feel like authentic, organic content
- Real, unpolished, natural appearance
- NO decorative borders, banners, overlays, or boxes
- NO native app interfaces or screenshot-style frames
- Just a clean, natural photograph or scene
"""
text_overlay_section = ""
if include_text_overlay:
text_overlay_section = f"""
=== HEADLINE TEXT (natural text in scene, NOT overlay) ===
IMPORTANT: Include this text ONLY ONCE in the image, as part of the natural scene.
Text to include: "{headline}"
CRITICAL TEXT RULES:
- Include the text ONLY ONCE - do NOT repeat or duplicate it anywhere
- Text should appear in ONE location only (on a document, sign, or surface in the scene)
- Position: {text_position}
- Style: {text_color}
- Ensure readability and correct spelling
- Text must be part of the scene, NOT an overlay on top
- NO decorative borders, boxes, or frames around text
- NO banners, badges, or logos
- Do NOT show the same message in multiple formats or locations
- If text contains numbers/prices, show them ONCE only
"""
else:
text_overlay_section = """
=== NO TEXT ===
Do NOT include any text on this image. Focus on the visual scene only.
NO text overlays, decorative elements, borders, banners, or overlays.
"""
# Build people/faces section outside f-string (f-string expression cannot contain backslash)
if is_auto_insurance_ad_format:
people_faces_section = """=== PEOPLE, FACES, CARS: OPTIONAL ===
Only include people, faces, or vehicles if the VISUAL SCENE description specifically mentions them. Most auto insurance ad formats are typography and layout only - no people or cars needed."""
else:
people_faces_section = """=== PEOPLE AND FACES: OPTIONAL ===
Only include people or faces if the VISUAL SCENE explicitly describes them. Many creatives work without people—product shots, documents, layouts, objects, text-only are all valid. If this image does include people or faces, they MUST look like real, original people with:
- Photorealistic faces with natural skin texture, visible pores, and realistic skin imperfections
- Natural facial asymmetry (no perfectly symmetrical faces)
- Unique, individual facial features (not generic or model-like)
- Natural expressions with authentic micro-expressions
- Realistic skin tones with natural variations and undertones
- Natural hair texture with individual strands
- Faces that look like real photographs of real people, NOT AI-generated portraits
- Avoid any faces that look synthetic, fake, or obviously computer-generated"""
# Build authenticity section outside f-string (f-string expression cannot contain backslash)
if is_auto_insurance_ad_format:
authenticity_section = """=== AUTHENTICITY (only if image includes people/cars per VISUAL SCENE) ===
If you included people or vehicles, they should look realistic. Otherwise focus on layout and typography only."""
else:
authenticity_section = """=== AUTHENTICITY REQUIREMENTS ===
Creatives may have no people (product-only, document, layout, objects, text). PEOPLE (only if present):
- Real, relatable individuals in everyday clothing
- Any hyperrealistic audience—any age, demographic, or psychographic; niche is a suggestion, not a rule
- Natural expressions and trustworthy energy
Visuals and concepts can be invented and need not match niche stereotypes; aim for hyperrealistic, diverse representation.
FACES (close-up):
- Photorealistic texture with visible pores and natural variation
- Subtle asymmetry and unique features—never plastic or model-perfect
- Believable micro-expressions with natural lighting and tone shifts
SETTINGS (if present):
- Real, lived-in locations with everyday props and a touch of natural clutter
DOCUMENTS (if present):
- Realistic bills or statements with legible details, highlights, and gentle wear"""
prompt = f"""Create a Facebook advertisement image that looks like AUTHENTIC, ORGANIC CONTENT.
If the image looks like it belongs on a stock website, it has failed.
{vintage_section}
{framework_section}
{text_overlay_section}
=== VISUAL SCENE ===
{visual_scene_description}
{"=== AUTO INSURANCE AD GRAPHIC (ONLY these 6 creative types) ===" if is_auto_insurance_ad_format else ""}
{"Follow the VISUAL SCENE description exactly. Use ONLY these 6 formats: (1) official notification, (2) social post card, (3) coverage tier panels, (4) car brand grid, (5) gift card CTA, (6) savings/urgency. No other creative types. Include headline, prices, rates, and CTA or button text as specified. Do NOT add people, faces, or cars unless the VISUAL SCENE explicitly asks for them. Do NOT create in-car dashboard mockups, screens inside car interiors, or headshots on displays. Do NOT include fake or made-up brand/company names; use generic text only (e.g. Compare Providers, See Rates) or omit. Render as a clean, modern ad graphic with clear typography and layout." if is_auto_insurance_ad_format else ""}
{people_faces_section}
{"- For this ad graphic layout, headline and price/rate text are part of the design; include them as specified in VISUAL SCENE." if is_auto_insurance_ad_format else "IMPORTANT: Do NOT display numbers, prices, dollar amounts, or savings figures in the image unless they naturally appear as part of the scene (e.g. on a document or sign). Focus on the visual scene (people optional; product/layout-only is fine). Numbers should be in the ad copy, not the image."}
=== VISUAL SPECIFICATIONS ===
STYLE: {visual_style} - {"clean modern ad graphic, professional layout" if is_auto_insurance_ad_format else "rendered in vintage documentary aesthetic"}
MOOD: {visual_mood} - {"trustworthy, clear, high-contrast" if is_auto_insurance_ad_format else "nostalgic, authentic, trustworthy"}
CAMERA: {camera_angle} - documentary/candid feel
LIGHTING: {lighting} - natural, not studio-polished
COMPOSITION: {composition}
{get_trending_image_guidance(trending_context)}
{niche_image_guidance}
{authenticity_section}
=== NEGATIVE PROMPTS (AVOID) ===
- No polished studio lighting or stock-photo aesthetics
- No synthetic, plastic, or perfectly symmetrical faces
- No decorative frames, overlays, badges, or repeated text
- No gibberish, placeholder strings, or fake brand/company names{" (keep labels generic and only where the layout specifies)" if is_auto_insurance_ad_format else ""}
- No mixing multiple framework formats or duplicating the same message
{"- No in-car dashboard mockups, interior screens, or faces on displays" if is_auto_insurance_ad_format else ""}
{"- Numbers, prices, or dollar amounts should appear only when they naturally belong in the scene" if not is_auto_insurance_ad_format else "- Keep typography limited to the headline, rates, and CTA exactly as described"}
=== OUTPUT ===
{"Create a scroll-stopping auto insurance ad graphic. Follow the VISUAL SCENE layout exactly: headline, rates/prices, and CTA or buttons as specified. Use only the 6 defined formats (official notification, social post, coverage tiers, car brand grid, gift card CTA, savings/urgency). No other creative types. No fake brand names; no in-car dashboard or screen mockups; no headshots on displays. Clean typography and layout only." if is_auto_insurance_ad_format else f"Create a scroll-stopping image that feels authentic and organic. {'Include the headline text ONCE as specified above - do NOT duplicate it.' if include_text_overlay else 'Focus on the visual scene without text.'} The image should feel like real content - NOT like a designed advertisement."}
CRITICAL REQUIREMENTS:
{"- Follow the VISUAL SCENE layout exactly; use borders, buttons, and rate cards only where described." if is_auto_insurance_ad_format else "- Keep the scene natural—no added frames, overlays, or decorative borders"}
- Place text or CTA elements exactly once in the location described
- Present the core message once; do not repeat it elsewhere
{"- Maintain clean typography and composition per VISUAL SCENE." if is_auto_insurance_ad_format else "- Focus on the authentic moment, not a polished ad layout"}"""
# Refine and clean the prompt before sending (pass niche for demographic fixes)
refined_prompt = self._refine_image_prompt(prompt, niche=niche)
return refined_prompt
def _refine_image_prompt(self, prompt: str, niche: str = None) -> str:
"""
Refine and clean the image prompt for affiliate marketing creatives.
Fixes illogical elements:
- Contradictory instructions
- Wrong demographics for niche
- Unrealistic visual combinations
- Corporate/stock photo aesthetics (bad for affiliate marketing)
- Meta-instructions that confuse image models
Affiliate marketing principle: Low-production, authentic images outperform polished studio shots.
"""
import re
# =====================================================================
# 1. REMOVE META-INSTRUCTIONS (confuse image models)
# =====================================================================
meta_patterns = [
r'\(for model, not to display\)',
r'\(apply these, don\'t display\)',
r'\(for reference only\)',
r'\(internal use\)',
r'IMPORTANT: Display ONLY',
r'IMPORTANT: If including',
r'IMPORTANT: Use this',
r'IMPORTANT: Follow this',
r'CRITICAL: Do not',
r'NOTE: This is for',
]
for pattern in meta_patterns:
prompt = re.sub(pattern, '', prompt, flags=re.IGNORECASE)
# =====================================================================
# 2. FIX DEMOGRAPHIC ISSUES (niche-specific from niche data)
# Skip niche demographic overrides when the prompt already describes a specific
# audience (unrestricted mode: allow diverse/invented audiences to stand).
# =====================================================================
prompt_lower = prompt.lower()
has_explicit_audience = any(
phrase in prompt_lower
for phrase in [
"aged ", "years old", "year old", "woman", "man", "demographic",
"hyperrealistic", "diverse representation", "any age",
]
)
if not has_explicit_audience:
niche_data_sanitize = self._get_niche_data(niche) if niche else {}
for pattern, replacement in niche_data_sanitize.get("prompt_sanitization_replacements", []):
prompt = re.sub(pattern, replacement, prompt, flags=re.IGNORECASE)
# =====================================================================
# 3. FIX ILLOGICAL VISUAL COMBINATIONS
# =====================================================================
# Can't have both "no text" and "headline text"
if 'no text' in prompt_lower and ('headline' in prompt_lower or 'text overlay' in prompt_lower):
# Keep "no text" instruction, remove headline sections
prompt = re.sub(
r'===.*HEADLINE.*===[\s\S]*?(?====|$)',
'',
prompt,
flags=re.IGNORECASE | re.MULTILINE
)
prompt = re.sub(r'headline[:\s]+["\']?[^"\']+["\']?', '', prompt, flags=re.IGNORECASE)
# Can't have both "minimalist" and "cluttered/busy"
if 'minimalist' in prompt_lower and any(word in prompt_lower for word in ['cluttered', 'busy', 'crowded', 'chaotic']):
prompt = re.sub(r'\b(cluttered|busy|crowded|chaotic)\b', 'clean', prompt, flags=re.IGNORECASE)
# Can't have both "dark/moody" and "bright/cheerful"
if 'dark' in prompt_lower and 'moody' in prompt_lower:
prompt = re.sub(r'\b(bright|cheerful|sunny|vibrant)\b', 'subtle', prompt, flags=re.IGNORECASE)
elif 'bright' in prompt_lower and 'cheerful' in prompt_lower:
prompt = re.sub(r'\b(dark|moody|dramatic|gloomy)\b', 'soft', prompt, flags=re.IGNORECASE)
# Can't have "indoor" scene with "outdoor" elements
if 'indoor' in prompt_lower or 'inside' in prompt_lower:
if 'sunlight streaming' not in prompt_lower: # Window light is okay
prompt = re.sub(r'\b(outdoor|outside|garden|yard|street)\b(?! view)', 'indoor', prompt, flags=re.IGNORECASE)
# =====================================================================
# 4. FIX AFFILIATE MARKETING ANTI-PATTERNS
# =====================================================================
# Remove corporate/stock photo aesthetics (bad for affiliate marketing)
stock_photo_terms = [
(r'\bstock photo\b', 'authentic photo'),
(r'\bprofessional studio\b', 'natural setting'),
(r'\bperfect lighting\b', 'natural lighting'),
(r'\bcorporate headshot\b', 'candid portrait'),
(r'\bpolished commercial\b', 'authentic UGC-style'),
(r'\bgeneric model\b', 'real person'),
(r'\bshutterstock style\b', 'authentic casual'),
(r'\bistock style\b', 'documentary style'),
]
for pattern, replacement in stock_photo_terms:
prompt = re.sub(pattern, replacement, prompt, flags=re.IGNORECASE)
# Ensure authenticity markers for affiliate marketing
if 'ugc' in prompt_lower or 'authentic' in prompt_lower:
# Remove conflicting professional markers
prompt = re.sub(r'\b(studio backdrop|professional lighting setup|commercial shoot)\b',
'natural environment', prompt, flags=re.IGNORECASE)
# =====================================================================
# 5. SOFTEN EXTREME BODY LANGUAGE ONLY (allow diverse/invented visuals)
# =====================================================================
# Only replace truly extreme/harmful terms; allow dramatic transformation etc.
unrealistic_patterns = [
(r'\b(impossibly thin|skeletal|anorexic)\b', 'healthy fit'),
]
for pattern, replacement in unrealistic_patterns:
prompt = re.sub(pattern, replacement, prompt, flags=re.IGNORECASE)
# =====================================================================
# 6. CLEAN UP FORMATTING AND STRUCTURE
# =====================================================================
# Remove developer instructions
lines = prompt.split('\n')
cleaned_lines = []
for line in lines:
line_lower = line.lower()
# Skip developer-only lines
if any(phrase in line_lower for phrase in [
'do not display', 'not to be displayed', 'for debugging',
'metadata:', 'internal:', 'developer note'
]):
continue
# Skip empty placeholder content
if any(phrase in line_lower for phrase in ['n/a', 'not provided', 'see above', 'tbd']):
if len(line.strip()) < 20: # Short placeholder lines
continue
cleaned_lines.append(line)
prompt = '\n'.join(cleaned_lines)
# Simplify emphatic markers (image models don't need shouting)
prompt = re.sub(r'\bCRITICAL:\s*', '', prompt, flags=re.IGNORECASE)
prompt = re.sub(r'\bIMPORTANT:\s*', '', prompt, flags=re.IGNORECASE)
prompt = re.sub(r'\bNOTE:\s*', '', prompt, flags=re.IGNORECASE)
prompt = re.sub(r'\bMUST:\s*', '', prompt, flags=re.IGNORECASE)
# Remove excessive blank lines
prompt = re.sub(r'\n{3,}', '\n\n', prompt)
# Remove empty sections
prompt = re.sub(r'===\s*===', '', prompt)
prompt = re.sub(r'===\s*\n\s*===', '===', prompt)
# =====================================================================
# 7. ENSURE LOGICAL COHERENCE
# =====================================================================
# Remove duplicate prohibitions
seen_prohibitions = set()
final_lines = []
for line in prompt.split('\n'):
line_lower = line.lower().strip()
if line_lower.startswith('- no ') or line_lower.startswith('no '):
prohibition_key = re.sub(r'[^a-z\s]', '', line_lower)[:50]
if prohibition_key in seen_prohibitions:
continue
seen_prohibitions.add(prohibition_key)
final_lines.append(line)
prompt = '\n'.join(final_lines)
# =====================================================================
# 8. ADD AFFILIATE MARKETING QUALITY MARKERS
# =====================================================================
# Ensure the prompt has authenticity markers if not present
if 'authentic' not in prompt_lower and 'ugc' not in prompt_lower and 'real' not in prompt_lower:
prompt += "\n\nStyle: Authentic; can be real-person, product-only, or layout-only. Not overly polished or corporate."
# Final trim
prompt = prompt.strip()
# Ensure prompt doesn't end mid-sentence
if prompt and not prompt[-1] in '.!?"\'':
prompt += '.'
return prompt
# --- Public: single ad (standard) ---
async def generate_ad(
self,
niche: str,
num_images: int = 1,
image_model: Optional[str] = None,
username: Optional[str] = None, # Username of the user generating the ad
target_audience: Optional[str] = None,
offer: Optional[str] = None,
use_trending: bool = False, # Whether to incorporate trending topics
trending_context: Optional[str] = None, # Optional specific trending context
) -> Dict[str, Any]:
"""
Generate a complete ad creative with copy and image.
Uses maximum randomization to ensure different results every time:
- Random strategy selection (2-3 from pool)
- Random hook selection
- Random visual style
- Random creative direction
- Random visual mood
- Random camera angle, lighting, composition
- Random seed for image generation
Can optionally incorporate current trending topics from Google News.
Args:
niche: Target niche (home_insurance or glp1)
num_images: Number of images to generate
use_trending: Whether to incorporate current trending topics
trending_context: Specific trending context (auto-fetched if not provided)
Returns:
Dict with ad copy, image path, and metadata
"""
# Load niche data
niche_data = self._get_niche_data(niche)
# Get framework first (needed for compatibility scoring)
all_frameworks = get_all_frameworks()
framework_keys = list(all_frameworks.keys())
# 70% chance to use niche-preferred, 30% chance for any framework (ensures all are used)
if random.random() < 0.7:
framework_data = get_frameworks_for_niche(niche, count=1)[0]
else:
# Use any framework for maximum variety
framework_key = random.choice(framework_keys)
framework_data = {"key": framework_key, **all_frameworks[framework_key]}
framework = framework_data["name"]
framework_key = framework_data["key"]
# IMPROVEMENT: Select compatible strategies based on framework
num_strategies = random.randint(2, 3)
strategies = self._select_compatible_strategies(niche_data, framework_key, count=num_strategies)
hooks = self._random_hooks(strategies, count=3)
# IMPROVEMENT: Use visual library in addition to strategy visuals
visual_styles = self._random_visual_styles(strategies, count=2, niche=niche, use_library=True)
creative_direction = random.choice(niche_data["creative_directions"])
visual_mood = random.choice(niche_data["visual_moods"])
ctas = await self._generate_ctas_async(niche, framework_data.get("name"))
cta = random.choice(ctas) if ctas else "Learn More"
# Use visual elements from visuals.py (instead of hardcoded)
visual_style_data = get_random_visual_style()
camera_angle = get_random_camera_angle()
lighting = get_random_lighting()
composition = get_random_composition()
# Get psychological trigger for additional guidance
trigger_data = get_random_trigger()
trigger_combination = get_trigger_combination()
# Get power words for copy enhancement
power_words = get_power_words(category=None, count=5)
# Get random angle × concept combination (like matrix generation)
combination = matrix_service.generate_single_combination(niche)
angle = combination["angle"]
concept = combination["concept"]
# Fetch trending context if requested
trending_info = None
if use_trending and trend_monitor_available:
try:
if not trending_context:
# Auto-fetch current trends (occasions + news); occasions are date-based (e.g. Valentine's Week)
print("📰 Fetching current trending topics (occasions + news)...")
trends_data = await trend_monitor.get_relevant_trends_for_niche(niche)
if trends_data and trends_data.get("relevant_trends"):
# Use top trend for context
top_trend = trends_data["relevant_trends"][0]
trending_context = f"{top_trend['title']} - {top_trend['summary']}"
trending_info = {
"title": top_trend["title"],
"summary": top_trend["summary"],
"category": top_trend.get("category", "General"),
"source": "Google News",
}
print(f"✓ Using trend: {top_trend['title']}")
else:
# User provided specific trending context
trending_info = {
"context": trending_context,
"source": "User provided",
}
print(f"✓ Using user-provided trending context")
except Exception as e:
print(f"Warning: Failed to fetch trending topics: {e}")
use_trending = False
elif use_trending and not trend_monitor_available:
print("Warning: Trending topics requested but trend monitor not available")
use_trending = False
# Generate ad copy via LLM with professional prompt
copy_prompt = self._build_copy_prompt(
niche=niche,
niche_data=niche_data,
strategies=strategies,
hooks=hooks,
creative_direction=creative_direction,
framework=framework,
framework_data=framework_data,
cta=cta,
trigger_data=trigger_data,
trigger_combination=trigger_combination,
power_words=power_words,
angle=angle,
concept=concept,
target_audience=target_audience,
offer=offer,
trending_context=trending_context if use_trending else None,
)
ad_copy = await llm_service.generate_json(
prompt=copy_prompt,
temperature=0.95, # High for variety
)
# Same framework drives visuals: set framework on ad_copy for image step
ad_copy["framework_key"] = framework_data["key"]
ad_copy["framework"] = framework_data["name"]
ad_copy["container_key"] = framework_data["key"] # backward compat
ad_copy["container_used"] = framework_data["name"] # backward compat
# Generate image(s) with professional prompt - PARALLELIZED
async def generate_single_image(image_index: int):
"""Helper function to generate a single image with all processing."""
# Build image prompt with all parameters (include trending context so images match the occasion)
image_prompt = self._build_image_prompt(
niche=niche,
ad_copy=ad_copy,
visual_styles=visual_styles,
visual_mood=visual_mood,
camera_angle=camera_angle,
lighting=lighting,
composition=composition,
visual_style_data=visual_style_data,
trending_context=trending_context if use_trending else None,
)
# Store the refined prompt for database saving
refined_image_prompt = image_prompt
# Generate with random seed
seed = random.randint(1, 2147483647)
try:
# Generate image (async)
image_bytes, model_used, image_url = await image_service.generate(
prompt=image_prompt,
width=settings.image_width,
height=settings.image_height,
seed=seed,
model_key=image_model,
)
# Generate filename
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
unique_id = str(uuid.uuid4())[:8]
filename = f"{niche}_{timestamp}_{unique_id}_{image_index}.png"
# Upload to R2 in parallel thread (sync operation)
r2_url = None
if r2_storage_available:
try:
r2_storage = get_r2_storage()
if r2_storage:
# Run R2 upload in thread pool (sync operation)
loop = asyncio.get_event_loop()
r2_url = await loop.run_in_executor(
None,
lambda: r2_storage.upload_image(
image_bytes=image_bytes,
filename=filename,
niche=niche,
)
)
print(f"Image {image_index + 1} uploaded to R2: {r2_url}")
except Exception as e:
print(f"Warning: Failed to upload to R2: {e}. Saving locally as backup.")
# Save image locally conditionally (based on environment settings)
filepath = self._save_image_locally(image_bytes, filename)
# Use R2 URL if available, otherwise use Replicate URL, fallback to local
final_image_url = r2_url or image_url
return {
"filename": filename,
"filepath": filepath,
"image_url": final_image_url,
"r2_url": r2_url,
"model_used": model_used,
"seed": seed,
"image_prompt": refined_image_prompt,
}
except Exception as e:
return {
"error": str(e),
"seed": seed,
"image_prompt": refined_image_prompt,
}
# Generate all images in parallel using asyncio.gather
if num_images > 1:
print(f"🔄 Generating {num_images} images in parallel...")
image_tasks = [generate_single_image(i) for i in range(num_images)]
generated_images = await asyncio.gather(*image_tasks)
else:
# Single image - no need for parallelization
generated_images = [await generate_single_image(0)]
# Generate unique ID
ad_id = str(uuid.uuid4())
# Build metadata
metadata = {
"strategies_used": [s["name"] for s in strategies],
"creative_direction": creative_direction,
"visual_mood": visual_mood,
"framework": framework,
"camera_angle": camera_angle,
"lighting": lighting,
"composition": composition,
"hooks_inspiration": hooks,
"visual_styles": visual_styles,
"visual_style": visual_style_data.get("name") if visual_style_data else None,
}
# Save to database (save EACH image as a separate record for gallery visibility)
# Ensure database connection is initialized
if db_service and db_service.collection is None and settings.mongodb_url:
try:
await db_service.connect()
except Exception as e:
print(f"Warning: Could not connect to database: {e}")
# Save each image variation as a separate database record
saved_ad_ids = []
if db_service and db_service.collection is not None and username:
for img_idx, image in enumerate(generated_images):
if image.get("error"):
continue # Skip failed images
try:
db_id = await db_service.save_ad_creative(
niche=niche,
title=None, # No title for standard flow
headline=ad_copy.get("headline", ""),
primary_text=ad_copy.get("primary_text", ""),
description=ad_copy.get("description", ""),
body_story=ad_copy.get("body_story", ""),
cta=ad_copy.get("cta", ""),
psychological_angle=ad_copy.get("psychological_angle", ""),
why_it_works=ad_copy.get("why_it_works", ""),
username=username, # Pass username
image_url=image.get("image_url"),
r2_url=image.get("r2_url"),
image_filename=image.get("filename"),
image_model=image.get("model_used"),
image_seed=image.get("seed"),
image_prompt=image.get("image_prompt"), # Save the final refined prompt
generation_method="standard",
angle_key=angle.get("key"),
angle_name=angle.get("name"),
angle_trigger=angle.get("trigger"),
angle_category=angle.get("category"),
concept_key=concept.get("key"),
concept_name=concept.get("name"),
concept_structure=concept.get("structure"),
concept_visual=concept.get("visual"),
concept_category=concept.get("category"),
metadata={**metadata, "variation_index": img_idx, "total_variations": len(generated_images)},
)
if db_id:
saved_ad_ids.append(db_id)
print(f"✓ Saved ad creative variation {img_idx + 1}/{len(generated_images)} to database: {db_id}")
except Exception as e:
print(f"Warning: Failed to save variation {img_idx + 1} to database: {e}")
# Use first saved ID as the main ad_id
if saved_ad_ids:
ad_id = saved_ad_ids[0]
# Build response
result = {
"id": ad_id,
"niche": niche,
"created_at": datetime.now().isoformat(),
# Ad copy
"headline": ad_copy.get("headline", ""),
"primary_text": ad_copy.get("primary_text", ""),
"description": ad_copy.get("description", ""),
"body_story": ad_copy.get("body_story", ""),
"cta": ad_copy.get("cta", ""),
"psychological_angle": ad_copy.get("psychological_angle", ""),
"why_it_works": ad_copy.get("why_it_works", ""),
# Image(s) with URLs
"images": generated_images,
# Metadata for debugging/learning
"metadata": metadata,
}
return result
# --- Public: matrix ad ---
async def generate_ad_with_matrix(
self,
niche: str,
angle_key: Optional[str] = None,
concept_key: Optional[str] = None,
custom_angle: Optional[str] = None,
custom_concept: Optional[str] = None,
num_images: int = 1,
image_model: Optional[str] = None,
username: Optional[str] = None,
core_motivator: Optional[str] = None,
target_audience: Optional[str] = None,
offer: Optional[str] = None,
) -> Dict[str, Any]:
"""
Generate ad using angle × concept matrix approach.
This provides more systematic ad generation with explicit
control over psychological angle and visual concept.
Args:
niche: Target niche
angle_key: Specific angle key (optional, random if not provided)
concept_key: Specific concept key (optional, random if not provided)
custom_angle: Custom angle dict (used when angle_key is 'custom')
custom_concept: Custom concept dict (used when concept_key is 'custom')
num_images: Number of images to generate
Returns:
Complete ad creative with angle and concept metadata
"""
from data.angles import get_angle_by_key
from data.concepts import get_concept_by_key
# Handle custom or predefined angle
angle = None
if angle_key == "custom" and custom_angle:
# Parse custom angle - it should be a dict with name, trigger, example
if isinstance(custom_angle, str):
# If it's a JSON string, parse it
try:
import json
angle = json.loads(custom_angle)
except:
# If plain text, create a basic structure
angle = {
"key": "custom",
"name": "Custom Angle",
"trigger": "Emotion",
"example": custom_angle,
"category": "Custom",
}
else:
angle = custom_angle
# Ensure required fields
angle["key"] = "custom"
angle["category"] = angle.get("category", "Custom")
elif angle_key:
angle = get_angle_by_key(angle_key)
if not angle:
raise ValueError(f"Invalid angle_key: {angle_key}")
# Handle custom or predefined concept
concept = None
if concept_key == "custom" and custom_concept:
# Parse custom concept - it should be a dict with name, structure, visual
if isinstance(custom_concept, str):
# If it's a JSON string, parse it
try:
import json
concept = json.loads(custom_concept)
except:
# If plain text, create a basic structure
concept = {
"key": "custom",
"name": "Custom Concept",
"structure": custom_concept,
"visual": custom_concept,
"category": "Custom",
}
else:
concept = custom_concept
# Ensure required fields
concept["key"] = "custom"
concept["category"] = concept.get("category", "Custom")
elif concept_key:
concept = get_concept_by_key(concept_key)
if not concept:
raise ValueError(f"Invalid concept_key: {concept_key}")
# If both angle and concept are provided (custom or predefined)
if angle and concept:
combination = {
"angle": angle,
"concept": concept,
"prompt_guidance": f"""
ANGLE: {angle.get('name', 'Custom Angle')}
- Psychological trigger: {angle.get('trigger', 'Emotion')}
- Example: "{angle.get('example', '')}"
CONCEPT: {concept.get('name', 'Custom Concept')}
- Structure: {concept.get('structure', '')}
- Visual: {concept.get('visual', '')}
""",
}
else:
# Fall back to auto-generation
combination = matrix_service.generate_single_combination(niche)
angle = combination["angle"]
concept = combination["concept"]
# Get niche data
niche_data = NICHE_DATA.get(niche, home_insurance.get_niche_data)()
ctas = await self._generate_ctas_async(niche)
cta = random.choice(ctas) if ctas else "Learn More"
# Build specialized prompt using angle + concept
ad_copy_prompt = self._build_matrix_ad_prompt(
niche=niche,
angle=angle,
concept=concept,
niche_data=niche_data,
cta=cta,
core_motivator=core_motivator,
target_audience=target_audience,
offer=offer,
)
# Generate ad copy
response_format = {
"type": "json_schema",
"json_schema": {
"name": "ad_copy",
"schema": {
"type": "object",
"properties": {
"headline": {"type": "string"},
"primary_text": {"type": "string"},
"description": {"type": "string"},
"body_story": {"type": "string"},
"image_brief": {"type": "string"},
"cta": {"type": "string"},
"psychological_angle": {"type": "string"},
"why_it_works": {"type": "string"},
},
"required": ["headline", "primary_text", "description", "body_story", "image_brief", "cta"],
},
},
}
ad_copy_response = await llm_service.generate(
prompt=ad_copy_prompt,
temperature=0.8,
response_format=response_format,
)
import json
ad_copy = json.loads(ad_copy_response)
# Build image prompt using concept's visual guidance
image_prompt = self._build_matrix_image_prompt(
niche=niche,
angle=angle,
concept=concept,
ad_copy=ad_copy,
core_motivator=core_motivator,
)
# Store the refined prompt for database saving (this is the final prompt sent to image service)
refined_image_prompt = image_prompt
# Generate images - PARALLELIZED
async def generate_single_matrix_image(image_index: int):
"""Helper function to generate a single matrix image with all processing."""
seed = random.randint(1, 2**31 - 1)
try:
# Generate image (async)
image_bytes, model_used, image_url = await image_service.generate(
prompt=image_prompt,
model_key=image_model or settings.image_model,
width=1024,
height=1024,
seed=seed,
)
# Generate filename
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
unique_id = uuid.uuid4().hex[:8]
filename = f"{niche}_{timestamp}_{unique_id}_{image_index}.png"
# Upload to R2 in parallel thread (sync operation)
r2_url = None
if r2_storage_available:
try:
r2_storage = get_r2_storage()
if r2_storage:
# Run R2 upload in thread pool (sync operation)
loop = asyncio.get_event_loop()
r2_url = await loop.run_in_executor(
None,
lambda: r2_storage.upload_image(
image_bytes=image_bytes,
filename=filename,
niche=niche,
)
)
print(f"Matrix image {image_index + 1} uploaded to R2: {r2_url}")
except Exception as e:
print(f"Warning: Failed to upload to R2: {e}. Saving locally as backup.")
# Save image locally conditionally (based on environment settings)
filepath = self._save_image_locally(image_bytes, filename)
# Use R2 URL if available, otherwise use Replicate URL
final_image_url = r2_url or image_url
return {
"filename": filename,
"filepath": filepath,
"image_url": final_image_url,
"r2_url": r2_url,
"model_used": model_used,
"seed": seed,
"image_prompt": refined_image_prompt,
"error": None,
}
except Exception as e:
return {
"filename": None,
"filepath": None,
"image_url": None,
"model_used": settings.image_model,
"seed": seed,
"image_prompt": refined_image_prompt if 'refined_image_prompt' in locals() else None,
"error": str(e),
}
# Generate all images in parallel using asyncio.gather
if num_images > 1:
print(f"🔄 Generating {num_images} matrix images in parallel...")
image_tasks = [generate_single_matrix_image(i) for i in range(num_images)]
images = await asyncio.gather(*image_tasks)
else:
# Single image - no need for parallelization
images = [await generate_single_matrix_image(0)]
# Generate unique ID
ad_id = str(uuid.uuid4())
# Save to database (save EACH image as a separate record for gallery visibility)
# Ensure database connection is initialized
if db_service and db_service.collection is None and settings.mongodb_url:
try:
await db_service.connect()
except Exception as e:
print(f"Warning: Could not connect to database: {e}")
# Save each image variation as a separate database record
saved_ad_ids = []
if db_service and db_service.collection is not None and username:
for img_idx, image in enumerate(images):
if image.get("error"):
continue # Skip failed images
try:
db_id = await db_service.save_ad_creative(
niche=niche,
title=None, # No title for matrix flow
headline=ad_copy.get("headline", ""),
primary_text=ad_copy.get("primary_text", ""),
description=ad_copy.get("description", ""),
body_story=ad_copy.get("body_story", ""),
cta=ad_copy.get("cta", ""),
psychological_angle=ad_copy.get("psychological_angle", angle.get("name", "")),
why_it_works=ad_copy.get("why_it_works", ""),
username=username, # Pass username
image_url=image.get("image_url"),
r2_url=image.get("r2_url"),
image_filename=image.get("filename"),
image_model=image.get("model_used"),
image_seed=image.get("seed"),
image_prompt=image.get("image_prompt"), # Save the final refined prompt
angle_key=angle.get("key"),
angle_name=angle.get("name"),
angle_trigger=angle.get("trigger"),
angle_category=angle.get("category"),
concept_key=concept.get("key"),
concept_name=concept.get("name"),
concept_structure=concept.get("structure"),
concept_visual=concept.get("visual"),
concept_category=concept.get("category"),
generation_method="angle_concept_matrix",
metadata={
"generation_method": "angle_concept_matrix",
"variation_index": img_idx,
"total_variations": len(images),
"visual_style": concept.get("name"), # Use concept name as visual style for matrix generation
},
)
if db_id:
saved_ad_ids.append(db_id)
print(f"✓ Saved matrix ad creative variation {img_idx + 1}/{len(images)} to database: {db_id}")
except Exception as e:
print(f"Warning: Failed to save matrix variation {img_idx + 1} to database: {e}")
# Use first saved ID as the main ad_id
if saved_ad_ids:
ad_id = saved_ad_ids[0]
return {
"id": ad_id,
"niche": niche,
"created_at": datetime.now().isoformat(),
"headline": ad_copy.get("headline", ""),
"primary_text": ad_copy.get("primary_text", ""),
"description": ad_copy.get("description", ""),
"body_story": ad_copy.get("body_story", ""),
"cta": ad_copy.get("cta", ""),
"psychological_angle": ad_copy.get("psychological_angle", angle.get("name", "")),
"why_it_works": ad_copy.get("why_it_works", ""),
"images": images,
"matrix": {
"angle": {
"key": angle.get("key"),
"name": angle.get("name"),
"trigger": angle.get("trigger"),
"category": angle.get("category"),
},
"concept": {
"key": concept.get("key"),
"name": concept.get("name"),
"structure": concept.get("structure"),
"visual": concept.get("visual"),
"category": concept.get("category"),
},
},
"metadata": {
"generation_method": "angle_concept_matrix",
},
}
# --- Public: extensive flow ---
async def generate_ad_extensive(
self,
niche: str,
target_audience: Optional[str] = None,
offer: Optional[str] = None,
num_images: int = 1,
image_model: Optional[str] = None,
num_strategies: int = 5,
username: Optional[str] = None, # Username of the user generating the ad
use_creative_inventor: bool = True,
trend_context: Optional[str] = None,
) -> Dict[str, Any]:
"""
Generate ad using extensive flow: (inventor or researcher) → creative director → designer → copywriter.
When use_creative_inventor=True, the system invents new ad angles, concepts, visuals, and
psychological triggers by itself instead of using the fixed researcher step.
Args:
niche: Target niche (home_insurance, glp1, auto_insurance, or custom)
target_audience: Optional target audience description
offer: Optional offer to run
num_images: Number of images to generate per strategy
image_model: Image generation model to use
num_strategies: Number of creative strategies to generate
use_creative_inventor: If True, use Creative Inventor to generate new angles/concepts/visuals/triggers; if False, use researcher
trend_context: Optional trend or occasion context (used when use_creative_inventor=True)
Returns:
Dict with ad copy, images, and metadata
"""
if not third_flow_available:
raise ValueError("Extensive service not available")
# Map known niche keys to display names; custom niches (e.g. from 'others') pass through as-is
niche_map = {
"home_insurance": "Home Insurance",
"glp1": "GLP-1",
"auto_insurance": "Auto Insurance",
}
niche_display = niche_map.get(niche, niche.replace("_", " ").title())
# Provide default offer if not provided; target_audience is optional when using inventor (AI decides audiences)
if not offer:
offer = f"Get the best {niche_display} solution"
audience_for_retrieve = target_audience or f"People interested in {niche_display}"
# Step 1: Invent new angles/concepts/visuals/triggers + hyper-specific audiences (Creative Inventor) or research (Researcher)
target_audiences_per_strategy: Optional[List[str]] = None
if use_creative_inventor:
print("🧠 Step 1: Inventing new ad angles, concepts, visuals, triggers, and hyper-specific audiences...")
researcher_output, target_audiences_per_strategy = await asyncio.to_thread(
third_flow_service.get_essentials_via_inventor,
niche=niche_display,
offer=offer,
n=num_strategies,
target_audience_hint=target_audience if target_audience else None,
trend_context=trend_context,
)
else:
print("🔍 Step 1: Researching psychology triggers, angles, and concepts...")
if not target_audience:
target_audience = audience_for_retrieve
researcher_output = await asyncio.to_thread(
third_flow_service.researcher,
target_audience=target_audience,
offer=offer,
niche=niche_display
)
if not researcher_output:
raise ValueError("Step 1 returned no results (inventor or researcher)")
# Step 2: Retrieve knowledge (in parallel)
print("📚 Step 2: Retrieving marketing knowledge...")
book_knowledge, ads_knowledge = await asyncio.gather(
asyncio.to_thread(
third_flow_service.retrieve_search,
audience_for_retrieve, offer, niche_display
),
asyncio.to_thread(
third_flow_service.retrieve_ads,
audience_for_retrieve, offer, niche_display
)
)
# Step 3: Creative Director (with per-strategy hyper-specific audiences when from inventor)
print(f"🎨 Step 3: Creating {num_strategies} creative strategy/strategies...")
print(f"📋 Parameters: num_strategies={num_strategies}, num_images={num_images}")
creative_director_target = (
"Various hyper-specific audiences (see per-strategy)" if target_audiences_per_strategy else audience_for_retrieve
)
creative_strategies = await asyncio.to_thread(
third_flow_service.creative_director,
researcher_output=researcher_output,
book_knowledge=book_knowledge,
ads_knowledge=ads_knowledge,
target_audience=creative_director_target,
offer=offer,
niche=niche_display,
n=num_strategies,
target_audiences=target_audiences_per_strategy,
)
if not creative_strategies:
raise ValueError("Creative director returned no strategies")
# Limit to requested number of strategies (in case LLM returns more)
creative_strategies = creative_strategies[:num_strategies]
print(f"📊 Using {len(creative_strategies)} strategy/strategies (requested: {num_strategies})")
# Step 4: Process strategies in parallel (designer + copywriter)
print(f"⚡ Step 4: Processing {len(creative_strategies)} strategies in parallel...")
strategy_tasks = [
asyncio.to_thread(
third_flow_service.process_strategy,
strategy,
niche=niche_display,
)
for strategy in creative_strategies
]
strategy_results = await asyncio.gather(*strategy_tasks)
# Step 5: Generate images for each strategy
# Ensure we only process the requested number of strategies
strategies_to_process = min(len(strategy_results), num_strategies)
print(f"🖼️ Step 5: Generating {num_images} image(s) per strategy for {strategies_to_process} strategy/strategies...")
all_results = []
for idx, (prompt, title, body, description) in enumerate(strategy_results[:strategies_to_process]):
if idx >= num_strategies:
print(f"⚠️ Stopping at strategy {idx + 1} (requested {num_strategies} strategies)")
break
if not prompt:
print(f"Warning: Strategy {idx + 1} has no prompt, skipping...")
continue
# Generate images for this strategy (respecting num_images parameter) - PARALLELIZED
print(f" Generating {num_images} image(s) for strategy {idx + 1}/{len(creative_strategies)}...")
# Variation modifiers to ensure different images
variation_modifiers = [
"different camera angle, unique composition",
"alternative perspective, varied lighting",
"distinct framing, different visual style",
"unique viewpoint, varied composition",
"alternative angle, different mood",
]
# Title/headline to show in image (from copywriter; use strategy title ideas if needed)
text_for_image = (title or creative_strategies[idx].titleIdeas or "").strip() if idx < len(creative_strategies) else (title or "").strip()
text_instruction = ""
if text_for_image:
text_instruction = f'\n\n=== TEXT THAT MUST APPEAR IN THE IMAGE ===\nInclude this text visibly and readably in the image (e.g. on a sign, document, phone screen, poster, or surface): "{text_for_image}"\n- Spell it correctly; make it clearly readable.\n- Include it once as part of the natural scene, not as a separate overlay.'
async def generate_single_extensive_image(img_idx: int):
"""Helper function to generate a single extensive image with all processing."""
try:
# Add text instruction then low quality camera instruction so the image contains the headline/title
prompt_with_text_and_camera = f"{prompt}{text_instruction}\n\n=== CAMERA QUALITY ===\n- The image should look like it was shot from a low quality camera\n- Include characteristics of low quality camera: slight grain, reduced sharpness, lower resolution appearance, authentic camera imperfections\n- Should have the authentic feel of a real photo taken with a basic or older camera device"
# Refine prompt and add variation for each image (pass niche for demographic fixes)
base_refined_prompt = self._refine_image_prompt(prompt_with_text_and_camera, niche=niche)
# Add variation modifier if generating multiple images
if num_images > 1:
variation = variation_modifiers[img_idx % len(variation_modifiers)]
refined_prompt = f"{base_refined_prompt}, {variation}"
else:
refined_prompt = base_refined_prompt
# Generate unique seed for each image
actual_seed = random.randint(1, 2147483647)
# Generate image (async)
image_bytes, model_used, image_url = await image_service.generate(
prompt=refined_prompt,
seed=actual_seed,
model_key=image_model,
)
if not image_bytes:
print(f"Warning: Failed to generate image {img_idx + 1} for strategy {idx + 1}")
return {
"error": "Image generation returned no image data",
"seed": actual_seed,
"image_prompt": refined_prompt,
}
# Generate filename with unique identifier
filename = f"{niche}_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}_{img_idx}.png"
# Upload to R2 in parallel thread (sync operation)
r2_url = None
if r2_storage_available:
try:
r2_storage = get_r2_storage()
if r2_storage:
# Run R2 upload in thread pool (sync operation)
loop = asyncio.get_event_loop()
r2_url = await loop.run_in_executor(
None,
lambda: r2_storage.upload_image(
image_bytes, filename=filename, niche=niche
)
)
print(f"Extensive image {img_idx + 1} uploaded to R2: {r2_url}")
except Exception as r2_e:
print(f"Warning: Failed to upload image to R2: {r2_e}")
# Save image locally conditionally (based on environment settings)
filepath = self._save_image_locally(image_bytes, filename)
# Use R2 URL if available, otherwise use Replicate URL
final_image_url = r2_url or image_url
result = {
"filename": filename,
"filepath": filepath,
"image_url": final_image_url,
"r2_url": r2_url,
"model_used": model_used,
"seed": actual_seed,
"image_prompt": refined_prompt,
}
print(f" ✓ Image {img_idx + 1}/{num_images} generated with seed {actual_seed}")
return result
except Exception as e:
print(f"Error generating image {img_idx + 1} for strategy {idx + 1}: {e}")
error_seed = random.randint(1, 2147483647)
return {
"error": str(e),
"seed": error_seed,
"image_prompt": refined_prompt if 'refined_prompt' in locals() else None,
}
# Generate all images in parallel using asyncio.gather
if num_images > 1:
print(f" 🔄 Generating {num_images} images in parallel for strategy {idx + 1}...")
image_tasks = [generate_single_extensive_image(img_idx) for img_idx in range(num_images)]
generated_images = await asyncio.gather(*image_tasks)
else:
# Single image - no need for parallelization
generated_images = [await generate_single_extensive_image(0)]
if not generated_images:
print(f"Warning: No images generated for strategy {idx + 1}, skipping...")
continue
# Build ad copy
strategy = creative_strategies[idx]
headline = title or strategy.titleIdeas or "Check this out"
primary_text = body or strategy.bodyIdeas or ""
description_text = description or strategy.captionIdeas or ""
cta = strategy.cta or "Learn More"
# Save to database (save EACH image as a separate record for gallery visibility)
ad_id = str(uuid.uuid4())
saved_ad_ids = []
if db_service and username:
for img_idx, image in enumerate(generated_images):
if image.get("error"):
continue # Skip failed images
try:
db_id = await db_service.save_ad_creative(
niche=niche,
title=title or "",
headline=headline,
primary_text=primary_text,
description=description_text,
body_story=primary_text, # Use primary_text as body_story
cta=cta,
psychological_angle=strategy.phsychologyTrigger or "",
why_it_works=f"Angle: {strategy.angle}, Concept: {strategy.concept}",
username=username, # Pass username
image_url=image.get("image_url"),
r2_url=image.get("r2_url"),
image_filename=image.get("filename"),
image_model=image.get("model_used"),
image_prompt=image.get("image_prompt"),
generation_method="extensive",
metadata={
"generation_method": "extensive",
"target_audience": target_audience,
"offer": offer,
"strategy_index": idx,
"psychology_trigger": strategy.phsychologyTrigger,
"angle": strategy.angle,
"concept": strategy.concept,
"visual_direction": strategy.visualDirection,
"variation_index": img_idx,
"total_variations": len(generated_images),
},
)
if db_id:
saved_ad_ids.append(db_id)
print(f"✓ Saved extensive ad creative variation {img_idx + 1}/{len(generated_images)} to database: {db_id}")
except Exception as e:
print(f"Warning: Failed to save extensive variation {img_idx + 1} to database: {e}")
# Use first saved ID as the main ad_id
if saved_ad_ids:
ad_id = saved_ad_ids[0]
all_results.append({
"id": ad_id,
"niche": niche,
"created_at": datetime.now().isoformat(),
"title": title or "",
"headline": headline,
"primary_text": primary_text,
"description": description_text,
"body_story": primary_text,
"cta": cta,
"psychological_angle": strategy.phsychologyTrigger or "",
"why_it_works": f"Angle: {strategy.angle}, Concept: {strategy.concept}",
"images": generated_images, # Include ALL images for this strategy
"metadata": {
"strategies_used": [strategy.phsychologyTrigger] if strategy.phsychologyTrigger else ["extensive"],
"creative_direction": f"Extensive: {strategy.angle} × {strategy.concept}",
"visual_mood": strategy.visualDirection.split(".")[0] if strategy.visualDirection else "authentic",
"framework": None,
"camera_angle": None,
"lighting": None,
"composition": None,
"hooks_inspiration": [strategy.titleIdeas] if strategy.titleIdeas else [],
"visual_styles": [strategy.concept] if strategy.concept else [],
},
})
print(f"✓ Strategy {idx + 1} completed with {len(generated_images)} image(s)")
# Return all results (like batch generation)
if all_results:
print(f"📤 Returning {len(all_results)} strategy result(s)")
return all_results
else:
raise ValueError("No ads generated from extensive")
# ========================================================================
# CUSTOM ANGLE/CONCEPT REFINEMENT
# ========================================================================
# --- Refine custom angle/concept ---
async def refine_custom_angle_or_concept(
self,
text: str,
type: str, # "angle" or "concept"
niche: str,
goal: Optional[str] = None,
) -> Dict[str, Any]:
"""
Refine a custom angle or concept text using AI.
Takes raw user input and structures it properly according to
the angle/concept framework used in ad generation.
Args:
text: Raw user input text
type: "angle" or "concept"
niche: Target niche for context
goal: Optional user goal or context
Returns:
Structured angle or concept dict
"""
import json
if type == "angle":
prompt = f"""You are an expert in direct-response advertising psychology.
The user wants to create a custom marketing ANGLE for {niche.replace('_', ' ')} ads.
An ANGLE answers "WHY should I care?" - it's the psychological hook that makes someone stop scrolling.
User's custom angle idea:
"{text}"
{f'User goal/context: {goal}' if goal else ''}
Example format: Name (2-4 words), Trigger (e.g. Fear, Greed, Relief, FOMO), Example hook (5-10 words). e.g. "Save Money" / Greed / "Save $600/year".
Structure the user's idea into a proper angle format.
Return JSON:
{{
"name": "Short descriptive name (2-4 words)",
"trigger": "Primary psychological trigger (e.g., Fear, Hope, Pride, Greed, Relief, FOMO, Curiosity, Anger, Trust)",
"example": "A compelling example hook using this angle (5-10 words)"
}}"""
response_format = {
"type": "json_schema",
"json_schema": {
"name": "refined_angle",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"trigger": {"type": "string"},
"example": {"type": "string"},
},
"required": ["name", "trigger", "example"],
},
},
}
response = await llm_service.generate(
prompt=prompt,
temperature=0.7,
response_format=response_format,
)
result = json.loads(response)
result["key"] = "custom"
result["category"] = "Custom"
result["original_text"] = text
return result
else: # concept
prompt = f"""You are an expert in visual advertising and creative direction.
The user wants to create a custom visual CONCEPT for {niche.replace('_', ' ')} ads.
A CONCEPT answers "HOW do we show it?" - it's the visual approach and structure of the ad.
User's custom concept idea:
"{text}"
{f'User goal/context: {goal}' if goal else ''}
Example format: Name (2-4 words), Structure (one sentence), Visual (one sentence with specific details). e.g. "Before/After Split" / "Side-by-side comparison" / "Split screen, clear contrast".
Structure the user's idea into a proper concept format.
Return JSON:
{{
"name": "Short descriptive name (2-4 words)",
"structure": "How to structure the visual/ad (one sentence)",
"visual": "Visual guidance for the image (one sentence, specific details)"
}}"""
response_format = {
"type": "json_schema",
"json_schema": {
"name": "refined_concept",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"structure": {"type": "string"},
"visual": {"type": "string"},
},
"required": ["name", "structure", "visual"],
},
},
}
response = await llm_service.generate(
prompt=prompt,
temperature=0.7,
response_format=response_format,
)
result = json.loads(response)
result["key"] = "custom"
result["category"] = "Custom"
result["original_text"] = text
return result
# --- Matrix prompt builders ---
def _build_matrix_ad_prompt(
self,
niche: str,
angle: Dict[str, Any],
concept: Dict[str, Any],
niche_data: Dict[str, Any],
cta: str,
core_motivator: Optional[str] = None,
target_audience: Optional[str] = None,
offer: Optional[str] = None,
) -> str:
"""Build ad copy prompt using angle + concept framework."""
# AI will decide whether to include numbers based on ad format and strategy
# Always provide guidance, AI decides usage
# Get niche-specific numbers guidance from number_config type (AI decides if/when to use)
numbers = self._generate_niche_numbers(niche)
num_type = niche_data.get("number_config", {}).get("type", "savings")
if num_type == "weight_loss":
numbers_section = f"""SPECIFIC NUMBERS TO USE:
- Weight Lost: {numbers['difference']}
- Timeframe: {numbers['days']}
- Starting: {numbers['before']}, Current: {numbers['after']}"""
else:
price_guidance = self._generate_specific_price(niche)
numbers_section = f"""NUMBERS GUIDANCE (you decide if/when to use):
- Price Guidance: {price_guidance}
- Saved: {numbers['difference']}/year
- Before: {numbers['before']}, After: {numbers['after']}
DECISION: Include prices/numbers only if they enhance believability and fit the ad format/strategy.
Use oddly specific amounts (e.g., "$97.33" not "$100") when including prices."""
# Note: AI decides whether to use the numbers based on format and strategy
motivator_block = ""
if core_motivator:
motivator_block = f"""
=== CORE EMOTIONAL MOTIVATOR (USE THIS) ===
The user chose this motivator: "{core_motivator}"
Use it to guide the hook, headline, and primary text. The ad must amplify this motivator.
"""
return f"""You are an elite direct-response copywriter creating a Facebook ad.
{motivator_block}
=== ANGLE × CONCEPT FRAMEWORK ===
ANGLE: {angle.get('name')} (Trigger: {angle.get('trigger')}) — Example: "{angle.get('example')}"
CONCEPT: {concept.get('name')} — Structure: {concept.get('structure')} | Visual: {concept.get('visual')}
For variety, adapt to different ecom verticals (fashion, beauty, supplements, fitness, electronics, home, pets, food). This ad can lean into: {get_random_vertical()['name']}.
{f'=== USER INPUTS ===' if target_audience or offer else ''}
{f'TARGET AUDIENCE: {target_audience}' if target_audience else ''}
{f'OFFER: {offer}' if offer else ''}
=== CONTEXT ===
NICHE: {niche.replace("_", " ").title()} | CTA: {cta}
{numbers_section}
=== OUTPUT (JSON) ===
{{
"headline": "<10 words max; use angle/trigger above; add numbers if they strengthen>",
"primary_text": "<2-3 emotional sentences>",
"description": "<one sentence, 10 words max>",
"body_story": "<8-12 sentence story: pain, tension, transformation, hope; first/second person>",
"image_brief": "<scene following concept above>",
"cta": "{cta}",
"psychological_angle": "{angle.get('name')}",
"why_it_works": "<brief mechanism>"
}}
Generate the ad now. Be bold and specific."""
def _build_matrix_image_prompt(
self,
niche: str,
angle: Dict[str, Any],
concept: Dict[str, Any],
ad_copy: Dict[str, Any],
core_motivator: Optional[str] = None,
) -> str:
"""Build image prompt using concept's visual guidance."""
headline = ad_copy.get("headline", "")
# When user selected a motivator, show it on the image instead of headline (like title)
text_on_image = (core_motivator or "").strip() or (headline or "")
use_motivator = bool((core_motivator or "").strip())
image_brief = ad_copy.get("image_brief", "")
# Natural blend options: motivator/text feels part of the scene, not forced
natural_blend_options = [
"as a handwritten note or sticky note in the scene",
"as text on a phone screen, tablet, or laptop in frame",
"as a message in a chat or social feed within the image",
"on a sign, whiteboard, or poster that fits the environment",
"as a caption or note lying naturally in the scene",
"reflected in a mirror or window as part of the environment",
"on a document, receipt, or paper in the scene",
"as subtle text on a product or package in frame",
]
text_blend = random.choice(natural_blend_options)
# Fallback simpler style when not using motivator
text_styles = [
"naturally integrated into the scene",
"as part of a document or sign in the image",
"on a surface within the scene",
"as natural text element in the environment",
]
text_style = random.choice(text_styles)
# Get niche-specific guidance from niche data
niche_data_img = self._get_niche_data(niche)
niche_guidance = niche_data_img.get("image_niche_guidance_short", "").strip() or f"NICHE: {niche.replace('_', ' ').title()}"
if use_motivator:
text_section = f'''=== MOTIVATOR PHRASE (blend naturally into the scene) ===
Phrase: "{text_on_image}"
CRITICAL — natural blend only:
- Weave this phrase into the scene so it feels organic, not overlaid or forced.
- It could appear {text_blend}.
- The scene and concept come first; the phrase should feel like a natural part of that world.
- No banners, boxes, or decorative overlays. No "ad-like" text placement.
- Text must be READABLE and correctly spelled, but never the dominant focal point.
- Avoid centering the phrase or making it look pasted on. It should belong in the environment.'''
layout_section = f"""=== LAYOUT ===
- Prioritize the scene from the brief and the {concept.get('name')} concept.
- The phrase "{text_on_image}" may appear anywhere it fits naturally (e.g. on a device, note, sign, or surface in frame).
- Do NOT force a dedicated "text zone." Let the composition feel organic."""
closing = f'''Create a scroll-stopping, authentic image. The phrase "{text_on_image}" may appear naturally in the scene—never forced or overlay-style.'''
else:
text_section = f'''=== HEADLINE TEXT (if included, should be part of natural scene) ===
"{text_on_image}"
TEXT REQUIREMENTS (natural integration, NOT overlay):
- Text should appear as part of the scene (on documents, signs, surfaces)
- Position: {text_style}
- Must be READABLE
- Spell every word correctly
- CRITICAL: NO overlay boxes, banners, or decorative elements
- Text should look like it naturally belongs in the scene'''
layout_section = f"""=== LAYOUT ===
- Text zone (bottom 25%): "{text_on_image}"
- Visual zone (top 75%): Scene following {concept.get('name')} concept"""
closing = f'''Create a scroll-stopping ad image with "{text_on_image}" prominently displayed.'''
prompt = f"""Create a Facebook ad image with natural, authentic content.
=== CAMERA QUALITY ===
- The image should look like it was shot from a low quality camera
- Include characteristics of low quality camera: slight grain, reduced sharpness, lower resolution appearance, authentic camera imperfections
- Should have the authentic feel of a real photo taken with a basic or older camera device
{text_section}
=== VISUAL CONCEPT: {concept.get('name')} ===
Structure: {concept.get('structure')}
Visual Guidance: {concept.get('visual')}
=== SCENE FROM BRIEF ===
{image_brief}
=== PSYCHOLOGICAL ANGLE: {angle.get('name')} ===
This image should trigger: {angle.get('trigger')}
{niche_guidance}
=== PEOPLE AND FACES: OPTIONAL ===
Only include people or faces if the image brief/VISUAL SCENE explicitly describes them. Creatives can be product-only, document-only, or layout-only with no people. If this image does include people or faces, they MUST look like real, original people with:
- Photorealistic faces with natural skin texture, visible pores, and realistic skin imperfections
- Natural facial asymmetry (no perfectly symmetrical faces)
- Unique, individual facial features (not generic or model-like)
- Natural expressions with authentic micro-expressions
- Realistic skin tones with natural variations and undertones
- Natural hair texture with individual strands
- Faces that look like real photographs of real people, NOT AI-generated portraits
- Avoid any faces that look synthetic, fake, or obviously computer-generated
{layout_section}
=== AVOID ===
- Missing or misspelled text
- Text that is too small to read
- Generic stock photo look
- Watermarks, logos
- AI-generated faces, synthetic faces, or fake-looking faces
- Overly smooth or plastic-looking skin
- Perfectly symmetrical faces
- Generic or model-like facial features
- Uncanny valley faces
- Faces that look like they came from a face generator
- Overly perfect, flawless skin
- Cartoon-like or stylized faces
{chr(10) + "- Forced or overlay-style text; the motivator must feel naturally mixed into the scene" if use_motivator else ""}
{closing}"""
# Refine and clean the prompt before sending (pass niche for demographic fixes)
refined_prompt = self._refine_image_prompt(prompt, niche=niche)
return refined_prompt
# --- Public: batch ---
async def generate_batch(
self,
niche: str,
count: int = 5,
images_per_ad: int = 1,
image_model: Optional[str] = None,
username: Optional[str] = None, # Username of the user generating the ads
method: Optional[str] = None, # "standard", "matrix", or None (mixed)
target_audience: Optional[str] = None,
offer: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""
Generate multiple ad creatives - PARALLELIZED.
Uses semaphore to limit concurrent operations and prevent resource exhaustion.
Args:
niche: Target niche
count: Number of ads to generate
images_per_ad: Images per ad (typically 1 for batch)
image_model: Image model to use
username: Username of the user generating the ads
method: Generation method - "standard", "matrix", or None (mixed 50/50)
Returns:
List of ad results (all normalized to GenerateResponse format)
"""
# Use semaphore to limit concurrent ad generation (max 3 at a time to avoid overwhelming APIs)
semaphore = asyncio.Semaphore(3)
async def generate_single_ad(ad_index: int):
"""Helper function to generate a single ad with semaphore control."""
async with semaphore:
try:
# Use method parameter to determine generation type
if method == "matrix":
use_matrix = True
elif method == "standard":
use_matrix = False
else:
# Default: 50% standard, 50% matrix (ensures all resources used)
use_matrix = random.random() < 0.5
if use_matrix:
# Use angle × concept matrix approach
result = await self.generate_ad_with_matrix(
niche=niche,
num_images=images_per_ad,
image_model=image_model,
username=username, # Pass username
target_audience=target_audience,
offer=offer,
)
# Normalize matrix result to standard format for batch response
# Extract matrix info and convert metadata
matrix_info = result.get("matrix", {})
angle = matrix_info.get("angle", {})
concept = matrix_info.get("concept", {})
# Convert to standard AdMetadata format
result["metadata"] = {
"strategies_used": [angle.get("trigger", "emotional_trigger")],
"creative_direction": f"Angle: {angle.get('name', '')}, Concept: {concept.get('name', '')}",
"visual_mood": concept.get("visual", "").split(".")[0] if concept.get("visual") else "authentic",
"framework": None,
"camera_angle": None,
"lighting": None,
"composition": None,
"hooks_inspiration": [angle.get("example", "")] if angle.get("example") else [],
"visual_styles": [concept.get("structure", "")] if concept.get("structure") else [],
}
# Remove matrix field as it's not in GenerateResponse
result.pop("matrix", None)
else:
# Use standard framework-based approach
result = await self.generate_ad(
niche=niche,
num_images=images_per_ad,
image_model=image_model,
username=username, # Pass username
target_audience=target_audience,
offer=offer,
)
return result
except Exception as e:
return {
"error": str(e),
"index": ad_index,
}
# Generate all ads in parallel using asyncio.gather with semaphore control
print(f"🔄 Generating {count} ads in parallel (max 3 concurrent)...")
ad_tasks = [generate_single_ad(i) for i in range(count)]
results = await asyncio.gather(*ad_tasks)
return results
# Global instance
ad_generator = AdGenerator()