from typing import List, Literal, Optional from pydantic import BaseModel, Field from openai import OpenAI from dotenv import load_dotenv import os load_dotenv() client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) class VisualHierarchy(BaseModel): flow_summary: str = Field(..., description="Brief of top-to-bottom reading flow and funneling.") key_elements_order: List[str] = Field(..., description="Ordered list of elements as the eye flows.") class TypographySpec(BaseModel): casing: Literal["all-caps","title-case","sentence-case","mixed"] weight: Literal["light","regular","medium","bold","extra-bold"] color: str notes: str class CTAInfo(BaseModel): text: str shape_or_style: str bg_color: str text_color: str contrast_comment: str class VisualStructureLayout(BaseModel): background_color: str psychological_association: str fit_to_theme_comment: str typography: TypographySpec minimalism_level: Literal["none","low","medium","high"] cta: CTAInfo hierarchy: VisualHierarchy class CopywritingBreakdown(BaseModel): headline: str device_used: List[Literal["curiosity_gap","authority","fear_avoidance","education","urgency","social_proof","other"]] supporting_text: str compliance_comment: str cta_text: str funnel_feel: Literal["content_recommendation","direct_response","hybrid"] class PsychologicalBehavioralTriggers(BaseModel): curiosity: str fear_or_pain_point: str authority_appeal: str ease_of_action: str class StoryCreativeTells(BaseModel): problem: str hope: str next_step: str narrative_summary: str class StrengthWeaknessItem(BaseModel): point: str why_it_matters: str class RisksArbitrageContext(BaseModel): bounce_risk: str fatigue_risk: str compliance_risk: str class CreativeVariant(BaseModel): hypothesis: str change: str expected_effect: str metric_to_watch: Literal["CTR","CPC","CPI","CVR","RPM","BounceRate","TimeOnPage","QualityRanking","Other"] class FunnelSynergy(BaseModel): landing_page_angle: str affiliate_or_offer_fit: Optional[str] = None class TestingPlan(BaseModel): primary_metrics: List[Literal["CTR","CPC","CVR","RPM","BounceRate","TimeOnPage","QualityRanking","Other"]] sample_size_or_duration_hint: str stop_loss_rules: str class OptimizationNextSteps(BaseModel): creative_variants: List[CreativeVariant] funnel_synergy: FunnelSynergy testing_plan: TestingPlan class MetaDetect(BaseModel): language: str detected_topic: str confidence_0to1: float = Field(ge=0, le=1) red_flags: List[str] = Field(..., description="Potential policy or brand-safety flags, if any.") class AdAnalysis(BaseModel): visual_structure_layout: VisualStructureLayout copywriting_breakdown: CopywritingBreakdown psychological_behavioral_triggers: PsychologicalBehavioralTriggers story_creative_tells: StoryCreativeTells strengths: List[StrengthWeaknessItem] weaknesses: List[StrengthWeaknessItem] risks_in_arbitrage_context: RisksArbitrageContext optimization_next_steps: OptimizationNextSteps meta: MetaDetect SYSTEM_PROMPT = """ You are a senior performance-creative analyst who analyze a SINGLE image ad. Do not include explanations, markdown, or extra keys. Rules: - Be precise and concise. Avoid generic fluff. - When unsure, infer cautiously and note uncertainty. - Explain “why it matters” in business terms (CTR, CPC, CVR, RPM, bounce, fatigue, policy). - Do not invent medical claims or promises; flag policy risks instead. - Keep color names human-readable (e.g., “pure orange”, “purple”, “yellow”). """ USER_PROMPT_TEMPLATE = """ Task: Analyze the attached image ad and produce a structured breakdown: 1) Visual structure & layout 2) Copywriting breakdown 3) Psychological & behavioral triggers 4) Story the creative tells 5) Strengths 6) Weaknesses 7) Risks in arbitrage context 8) Optimization / next steps (variants, funnel synergy, testing plan) Constraints: - Use crisp, decision-ready language. - If text is in Spanish or another language, keep your summaries in English but preserve original CTA text. """ def get_analysis(img_url): messages=[ { "role": "system", "content": [ { "type": "text", "text": SYSTEM_PROMPT } ] }, { "role": "user", "content": [ { "type": "text", "text": USER_PROMPT_TEMPLATE }, { "type": "image_url", "image_url": { "url": img_url } } ] } ] completion = client.chat.completions.parse( model="gpt-4o", messages=messages, response_format=AdAnalysis, ) ad_analysis = completion.choices[0].message.parsed return ad_analysis