sushilideaclan01 commited on
Commit
505ff55
·
1 Parent(s): 4c17c06

removed the containers and made some optimizations

Browse files
config.py CHANGED
@@ -34,6 +34,7 @@ class Settings(BaseSettings):
34
  # LLM Settings
35
  llm_model: str = "gpt-4o-mini" # Cost-effective model
36
  llm_temperature: float = 0.95 # High for variety
 
37
 
38
  # Vision API Settings
39
  vision_model: str = "gpt-4o" # Vision-capable model for image analysis
 
34
  # LLM Settings
35
  llm_model: str = "gpt-4o-mini" # Cost-effective model
36
  llm_temperature: float = 0.95 # High for variety
37
+ use_ai_generated_hooks: bool = False # Set True to generate framework hook examples with AI; False = use static from frameworks.py
38
 
39
  # Vision API Settings
40
  vision_model: str = "gpt-4o" # Vision-capable model for image analysis
data/auto_insurance.py CHANGED
@@ -685,25 +685,6 @@ COPY_TEMPLATES = [
685
  },
686
  ]
687
 
688
- CTAS = [
689
- "Check Your Eligibility",
690
- "See If You Qualify",
691
- "Check Eligibility Now",
692
- "Tap To See Your Rate",
693
- "Calculate Your Savings",
694
- "Get Your Free Quote",
695
- "See Your New Rate",
696
- "Find Out How Much You Can Save",
697
- "Click To See Your Savings",
698
- "Get Protected Now",
699
- "Start Saving Today",
700
- "Don't Miss This",
701
- "Claim Your Rate",
702
- "Drivers: Check Your Rate",
703
- "See Driver Rates",
704
- "50+: Get Your Quote",
705
- ]
706
-
707
  # ============================================================================
708
  # SECTION 4: AGGREGATED DATA
709
  # ============================================================================
@@ -734,5 +715,43 @@ def get_niche_data():
734
  "creative_directions": CREATIVE_DIRECTIONS,
735
  "visual_moods": VISUAL_MOODS,
736
  "copy_templates": COPY_TEMPLATES,
737
- "ctas": CTAS,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
738
  }
 
685
  },
686
  ]
687
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
688
  # ============================================================================
689
  # SECTION 4: AGGREGATED DATA
690
  # ============================================================================
 
715
  "creative_directions": CREATIVE_DIRECTIONS,
716
  "visual_moods": VISUAL_MOODS,
717
  "copy_templates": COPY_TEMPLATES,
718
+ "niche_guidance": """
719
+ NICHE-SPECIFIC REQUIREMENTS (AUTO INSURANCE):
720
+ - Focus on REALISTIC driving scenarios and protection
721
+ - Show real situations: safe driving, accidents, family safety, financial protection
722
+ - Use authentic emotions: fear of accidents, relief of coverage, family responsibility, savings
723
+ - Target pain points: high premiums, accident fears, coverage gaps, legal requirements
724
+ - Messaging must feel URGENT but trustworthy
725
+ - Visual concepts: real drivers, real cars, real protection scenarios, savings proof
726
+ - AVOID: extreme accident scenes, fear-mongering, unrealistic scenarios
727
+ - AVOID: generic stock photos, overly dramatic crash imagery
728
+ """,
729
+ "price_config": {
730
+ "guidance": "Consider using oddly specific prices (e.g., $29.00 or $67.33 instead of $30 or $70) if the ad format calls for it. Typical range: $29-$150/month. Only include if it enhances believability and fits the ad format.",
731
+ "type": "insurance",
732
+ "before_range": [1200, 2400],
733
+ "savings_pct_range": [0.50, 0.70],
734
+ "labels": {"before": "$/year", "after": "$/year", "difference": "$", "metric": "savings per year"},
735
+ },
736
+ "number_config": {
737
+ "type": "savings",
738
+ "before_range": [1200, 2400],
739
+ "savings_pct_range": [0.50, 0.70],
740
+ "labels": {"before": "$/year", "after": "$/year", "difference": "$", "metric": "savings per year"},
741
+ },
742
+ "image_guidance": """
743
+ NICHE REQUIREMENTS (AUTO INSURANCE):
744
+ - Show REAL drivers, cars, and protection scenarios
745
+ - Include authentic elements: vehicles, roads, documents, savings proof
746
+ - People should look like real drivers (diverse, relatable)
747
+ - Accident/coverage imagery should feel realistic but not gratuitous
748
+ - AVOID: extreme crash imagery, fear-mongering, generic stock photos
749
+ """,
750
+ "image_niche_guidance_short": """
751
+ NICHE: Auto Insurance
752
+ - Show real drivers, cars, and protection scenarios
753
+ - People should be diverse, relatable drivers
754
+ - Realistic but not gratuitous accident/coverage imagery""",
755
+ "prompt_sanitization_replacements": [],
756
+ "visual_library": {},
757
  }
data/containers.py DELETED
@@ -1,434 +0,0 @@
1
- """
2
- Container Types - Visual container styles for ad creatives.
3
- These simulate different interface styles to make ads feel native and authentic.
4
- """
5
-
6
- from typing import Dict, Any, List, Optional
7
- import random
8
-
9
-
10
- # Complete list of container types with visual guidance
11
- CONTAINER_TYPES: Dict[str, Dict[str, Any]] = {
12
- "imessage": {
13
- "name": "iMessage",
14
- "description": "iOS iMessage screenshot style",
15
- "visual_guidance": "iOS iMessage screenshot style, blue/green message bubbles, iPhone interface, authentic conversation look",
16
- "font_style": "San Francisco, system font",
17
- "colors": {
18
- "primary": "#007AFF", # iOS blue
19
- "secondary": "#34C759", # iOS green
20
- "background": "#FFFFFF",
21
- "text": "#000000",
22
- },
23
- "best_for": ["personal connection", "conversation", "informal offers"],
24
- "authenticity_tips": [
25
- "Include battery %, time, signal bars",
26
- "Use realistic conversation format",
27
- "Keep messages short (2-4 messages)",
28
- ],
29
- },
30
- "whatsapp": {
31
- "name": "WhatsApp",
32
- "description": "WhatsApp chat screenshot style",
33
- "visual_guidance": "WhatsApp chat interface, green bubbles, checkmarks, authentic conversation feel",
34
- "font_style": "Helvetica Neue, system font",
35
- "colors": {
36
- "primary": "#25D366", # WhatsApp green
37
- "secondary": "#128C7E",
38
- "background": "#ECE5DD",
39
- "text": "#000000",
40
- },
41
- "best_for": ["personal recommendations", "peer-to-peer", "urgent messages"],
42
- "authenticity_tips": [
43
- "Include double checkmarks (read receipts)",
44
- "Add timestamps",
45
- "Use typical WhatsApp formatting",
46
- ],
47
- },
48
- "sms": {
49
- "name": "SMS/Text",
50
- "description": "Standard SMS text message style",
51
- "visual_guidance": "Android/iOS SMS interface, simple text bubbles, notification style",
52
- "font_style": "Roboto or San Francisco",
53
- "colors": {
54
- "primary": "#2196F3",
55
- "secondary": "#4CAF50",
56
- "background": "#FFFFFF",
57
- "text": "#000000",
58
- },
59
- "best_for": ["urgent alerts", "personal messages", "time-sensitive offers"],
60
- "authenticity_tips": [
61
- "Keep messages very short",
62
- "Use typical SMS abbreviations",
63
- "Include carrier/time info",
64
- ],
65
- },
66
- # "bank_alert": {
67
- # "name": "Bank Alert",
68
- # "description": "Bank transaction notification style",
69
- # "visual_guidance": "Bank transaction notification style, red alert box, bank app UI, urgent notification aesthetic",
70
- # "font_style": "Arial, Helvetica, system font",
71
- # "colors": {
72
- # "primary": "#D32F2F", # Alert red
73
- # "secondary": "#1976D2", # Bank blue
74
- # "background": "#FFFFFF",
75
- # "text": "#212121",
76
- # },
77
- # "best_for": ["financial urgency", "savings alerts", "money-related offers"],
78
- # "authenticity_tips": [
79
- # "Use official-looking format",
80
- # "Include dollar amounts",
81
- # "Add bank-style icons",
82
- # ],
83
- # },
84
- "news_chyron": {
85
- "name": "News Chyron",
86
- "description": "Breaking news ticker style",
87
- "visual_guidance": "Breaking news ticker style, red/white scrolling text bar, news channel aesthetic, urgent feel",
88
- "font_style": "Impact, Arial Black, bold sans-serif",
89
- "colors": {
90
- "primary": "#FF0000", # Breaking news red
91
- "secondary": "#FFFFFF",
92
- "background": "#000000",
93
- "text": "#FFFFFF",
94
- },
95
- "best_for": ["breaking announcements", "urgent news", "time-sensitive offers"],
96
- "authenticity_tips": [
97
- "Use BREAKING/ALERT prefix",
98
- "Include news channel logo area",
99
- "Add scrolling ticker effect",
100
- ],
101
- },
102
- "email_notification": {
103
- "name": "Email Notification",
104
- "description": "Email notification/preview style",
105
- "visual_guidance": "Email notification style, email client interface, notification badge, real email app look",
106
- "font_style": "System font, Segoe UI, Roboto",
107
- "colors": {
108
- "primary": "#1A73E8", # Gmail blue
109
- "secondary": "#EA4335", # Notification badge
110
- "background": "#FFFFFF",
111
- "text": "#202124",
112
- },
113
- "best_for": ["official communications", "professional offers", "formal announcements"],
114
- "authenticity_tips": [
115
- "Include sender, subject, preview",
116
- "Add unread badge if relevant",
117
- "Use email timestamp format",
118
- ],
119
- },
120
- "reddit_post": {
121
- "name": "Reddit Post",
122
- "description": "Reddit post/comment style",
123
- "visual_guidance": "Reddit discussion style, Reddit UI, comment thread appearance, authentic forum look",
124
- "font_style": "Noto Sans, Arial",
125
- "colors": {
126
- "primary": "#FF4500", # Reddit orange
127
- "secondary": "#0079D3", # Reddit blue
128
- "background": "#DAE0E6",
129
- "text": "#1A1A1B",
130
- },
131
- "best_for": ["social proof", "user discussions", "authentic testimonials"],
132
- "authenticity_tips": [
133
- "Include upvote counts",
134
- "Add username (anonymous style)",
135
- "Use subreddit reference",
136
- ],
137
- },
138
- "system_notification": {
139
- "name": "System Notification",
140
- "description": "iOS/Android system notification popup",
141
- "visual_guidance": "iOS/Android system notification style - plain white or gray rounded rectangle box, NO gradients, NO emojis, NO decorative elements, simple system font, minimal text, authentic OS notification appearance",
142
- "font_style": "San Francisco, Roboto, system font only",
143
- "colors": {
144
- "primary": "#000000",
145
- "secondary": "#666666",
146
- "background": "#F2F2F2",
147
- "text": "#000000",
148
- },
149
- "best_for": ["urgent alerts", "app notifications", "system messages"],
150
- "authenticity_tips": [
151
- "NO emojis or decorative elements",
152
- "Keep to 1-2 short lines",
153
- "Use app icon if relevant",
154
- ],
155
- "avoid": ["emojis", "decorative elements", "gradients", "colorful backgrounds"],
156
- },
157
- "push_notification": {
158
- "name": "Push Notification",
159
- "description": "Mobile app push notification style",
160
- "visual_guidance": "Mobile push notification banner, app icon, brief text, swipe-to-view format",
161
- "font_style": "System font",
162
- "colors": {
163
- "primary": "#007AFF",
164
- "secondary": "#8E8E93",
165
- "background": "#FFFFFF",
166
- "text": "#000000",
167
- },
168
- "best_for": ["app alerts", "time-sensitive messages", "quick updates"],
169
- "authenticity_tips": [
170
- "Include app icon",
171
- "Very short headline (5-7 words)",
172
- "Add timestamp (e.g., 'now', '2m ago')",
173
- ],
174
- },
175
- "sticky_note": {
176
- "name": "Sticky Note",
177
- "description": "Handwritten sticky note overlay",
178
- "visual_guidance": "Yellow sticky note overlay on image, handwritten-style text, authentic note appearance, slightly wrinkled paper texture",
179
- "font_style": "Handwriting fonts, marker style",
180
- "colors": {
181
- "primary": "#FFEB3B", # Sticky note yellow
182
- "secondary": "#FFC107",
183
- "background": "#FFEB3B",
184
- "text": "#000000",
185
- },
186
- "best_for": ["personal reminders", "quick tips", "informal notes"],
187
- "authenticity_tips": [
188
- "Slight angle/tilt",
189
- "Handwritten font style",
190
- "Paper texture/wrinkles",
191
- ],
192
- },
193
- "memo": {
194
- "name": "Internal Memo",
195
- "description": "Office memo/document style",
196
- "visual_guidance": "Internal memo document style, typewriter font, yellow/white paper, authentic document appearance",
197
- "font_style": "Courier, typewriter fonts",
198
- "colors": {
199
- "primary": "#000000",
200
- "secondary": "#333333",
201
- "background": "#FFFFCC", # Paper yellow
202
- "text": "#000000",
203
- },
204
- "best_for": ["official announcements", "leaked documents", "internal secrets"],
205
- "authenticity_tips": [
206
- "Add CONFIDENTIAL stamp if relevant",
207
- "Include date, to/from fields",
208
- "Paper texture/fold marks",
209
- ],
210
- },
211
- "browser_alert": {
212
- "name": "Browser Alert",
213
- "description": "Browser popup/alert dialog",
214
- "visual_guidance": "Browser dialog box, alert icons, OK/Cancel buttons, system alert aesthetic",
215
- "font_style": "System font, Segoe UI",
216
- "colors": {
217
- "primary": "#0078D4", # Windows blue
218
- "secondary": "#D83B01", # Warning orange
219
- "background": "#FFFFFF",
220
- "text": "#000000",
221
- },
222
- "best_for": ["urgent warnings", "system alerts", "confirmation messages"],
223
- "authenticity_tips": [
224
- "Include browser chrome",
225
- "Add alert icon",
226
- "Button styling",
227
- ],
228
- },
229
- "social_post": {
230
- "name": "Social Media Post",
231
- "description": "Facebook/Instagram post style",
232
- "visual_guidance": "Social media feed post, user profile, likes/comments, authentic social feel",
233
- "font_style": "Helvetica, system font",
234
- "colors": {
235
- "primary": "#1877F2", # Facebook blue
236
- "secondary": "#E4405F", # Instagram pink
237
- "background": "#FFFFFF",
238
- "text": "#1C1E21",
239
- },
240
- "best_for": ["social proof", "user content", "organic feel"],
241
- "authenticity_tips": [
242
- "Include profile picture",
243
- "Add like/comment counts",
244
- "Use platform-specific formatting",
245
- ],
246
- },
247
- "standard": {
248
- "name": "Standard Ad",
249
- "description": "Clean, professional ad format",
250
- "visual_guidance": "Clean ad layout, professional design, clear headline and CTA",
251
- "font_style": "Clean sans-serif fonts",
252
- "colors": {
253
- "primary": "#2196F3",
254
- "secondary": "#FF9800",
255
- "background": "#FFFFFF",
256
- "text": "#212121",
257
- },
258
- "best_for": ["professional campaigns", "brand awareness", "general advertising"],
259
- "authenticity_tips": [
260
- "Clear visual hierarchy",
261
- "Prominent CTA",
262
- "Brand-consistent design",
263
- ],
264
- },
265
- "telegram": {
266
- "name": "Telegram",
267
- "description": "Telegram chat message style",
268
- "visual_guidance": "Telegram chat interface, blue message bubbles, Telegram UI, authentic messaging look",
269
- "font_style": "Roboto, system font",
270
- "colors": {
271
- "primary": "#3390EC", # Telegram blue
272
- "secondary": "#0088CC",
273
- "background": "#FFFFFF",
274
- "text": "#000000",
275
- },
276
- "best_for": ["personal messages", "group chats", "informal communication"],
277
- "authenticity_tips": [
278
- "Include Telegram UI elements",
279
- "Use typical Telegram formatting",
280
- "Add read receipts if relevant",
281
- ],
282
- },
283
- "slack": {
284
- "name": "Slack",
285
- "description": "Slack workspace message style",
286
- "visual_guidance": "Slack workspace interface, channel messages, Slack UI, professional team communication",
287
- "font_style": "Lato, Slack font",
288
- "colors": {
289
- "primary": "#4A154B", # Slack purple
290
- "secondary": "#36C5F0",
291
- "background": "#FFFFFF",
292
- "text": "#1D1C1D",
293
- },
294
- "best_for": ["team communication", "workplace announcements", "professional updates"],
295
- "authenticity_tips": [
296
- "Include channel name",
297
- "Add user avatar",
298
- "Use Slack message formatting",
299
- ],
300
- },
301
- "instagram_story": {
302
- "name": "Instagram Story",
303
- "description": "Instagram story frame style",
304
- "visual_guidance": "Instagram story format, vertical 9:16 aspect ratio, story UI elements, authentic Instagram look",
305
- "font_style": "Instagram font, system font",
306
- "colors": {
307
- "primary": "#E4405F", # Instagram pink
308
- "secondary": "#833AB4",
309
- "background": "#000000",
310
- "text": "#FFFFFF",
311
- },
312
- "best_for": ["social media engagement", "story-style content", "mobile-first ads"],
313
- "authenticity_tips": [
314
- "Vertical format (9:16)",
315
- "Include story UI elements",
316
- "Use Instagram-style fonts",
317
- ],
318
- },
319
- "tiktok_style": {
320
- "name": "TikTok Style",
321
- "description": "TikTok video frame style",
322
- "visual_guidance": "TikTok video frame, vertical format, TikTok UI overlay, authentic TikTok appearance",
323
- "font_style": "TikTok font, bold sans-serif",
324
- "colors": {
325
- "primary": "#000000",
326
- "secondary": "#FE2C55", # TikTok red
327
- "background": "#000000",
328
- "text": "#FFFFFF",
329
- },
330
- "best_for": ["youth engagement", "viral content", "trending topics"],
331
- "authenticity_tips": [
332
- "Vertical video format",
333
- "Bold text overlays",
334
- "Trending style elements",
335
- ],
336
- },
337
- "linkedin_post": {
338
- "name": "LinkedIn Post",
339
- "description": "LinkedIn feed post style",
340
- "visual_guidance": "LinkedIn feed post, professional network UI, LinkedIn interface, authentic professional look",
341
- "font_style": "LinkedIn font, professional sans-serif",
342
- "colors": {
343
- "primary": "#0077B5", # LinkedIn blue
344
- "secondary": "#000000",
345
- "background": "#FFFFFF",
346
- "text": "#000000",
347
- },
348
- "best_for": ["professional networking", "B2B marketing", "career-focused content"],
349
- "authenticity_tips": [
350
- "Include profile elements",
351
- "Professional tone",
352
- "LinkedIn-style formatting",
353
- ],
354
- },
355
- "app_store_listing": {
356
- "name": "App Store Listing",
357
- "description": "App store screenshot style",
358
- "visual_guidance": "App store listing screenshot, app icon, ratings, screenshots, authentic app store appearance",
359
- "font_style": "San Francisco, system font",
360
- "colors": {
361
- "primary": "#007AFF", # iOS blue
362
- "secondary": "#FF9500",
363
- "background": "#FFFFFF",
364
- "text": "#000000",
365
- },
366
- "best_for": ["app promotion", "mobile apps", "app features"],
367
- "authenticity_tips": [
368
- "Include app icon",
369
- "Star ratings",
370
- "App store UI elements",
371
- ],
372
- },
373
- "email_signature": {
374
- "name": "Email Signature",
375
- "description": "Professional email signature style",
376
- "visual_guidance": "Professional email signature, contact info, logo, authentic email signature appearance",
377
- "font_style": "Arial, Helvetica, professional fonts",
378
- "colors": {
379
- "primary": "#000000",
380
- "secondary": "#666666",
381
- "background": "#FFFFFF",
382
- "text": "#000000",
383
- },
384
- "best_for": ["professional communication", "B2B outreach", "formal announcements"],
385
- "authenticity_tips": [
386
- "Include contact information",
387
- "Professional formatting",
388
- "Company logo if relevant",
389
- ],
390
- },
391
- }
392
-
393
-
394
- def get_all_containers() -> Dict[str, Dict[str, Any]]:
395
- """Get all available container types."""
396
- return CONTAINER_TYPES
397
-
398
-
399
- def get_container(key: str) -> Optional[Dict[str, Any]]:
400
- """Get a specific container type by key."""
401
- return CONTAINER_TYPES.get(key.lower().replace(" ", "_"))
402
-
403
-
404
- def get_random_container() -> Dict[str, Any]:
405
- """Get a random container type."""
406
- key = random.choice(list(CONTAINER_TYPES.keys()))
407
- return {"key": key, **CONTAINER_TYPES[key]}
408
-
409
-
410
- def get_container_visual_guidance(container_key: str) -> str:
411
- """Get visual guidance for a container type."""
412
- container = get_container(container_key)
413
- if container:
414
- return container.get("visual_guidance", "")
415
- return f"Container type: {container_key}, authentic appearance"
416
-
417
-
418
- def get_native_containers() -> List[str]:
419
- """Get container types that look like native app interfaces."""
420
- native = ["imessage", "whatsapp", "sms", "system_notification", "push_notification", "email_notification"]
421
- return native
422
-
423
-
424
- def get_ugc_containers() -> List[str]:
425
- """Get container types that look like user-generated content."""
426
- ugc = ["reddit_post", "social_post", "sticky_note", "memo"]
427
- return ugc
428
-
429
-
430
- def get_alert_containers() -> List[str]:
431
- """Get container types that create urgency."""
432
- alerts = ["bank_alert", "news_chyron", "browser_alert", "system_notification"]
433
- return alerts
434
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
data/frameworks.py CHANGED
@@ -24,6 +24,7 @@ FRAMEWORKS: Dict[str, Dict[str, Any]] = {
24
  "headline_style": "ALL CAPS with news prefix (BREAKING:, URGENT:, ALERT:)",
25
  "tone": "Urgent, newsworthy, time-sensitive",
26
  "psychological_triggers": ["FOMO", "Urgency", "Curiosity"],
 
27
  },
28
  "mobile_post": {
29
  "name": "Mobile Post",
@@ -40,6 +41,7 @@ FRAMEWORKS: Dict[str, Dict[str, Any]] = {
40
  "headline_style": "Short, punchy, action-oriented (under 8 words)",
41
  "tone": "Quick, casual, conversational",
42
  "psychological_triggers": ["Convenience", "Speed", "Ease"],
 
43
  },
44
  "before_after": {
45
  "name": "Before/After",
@@ -56,6 +58,7 @@ FRAMEWORKS: Dict[str, Dict[str, Any]] = {
56
  "headline_style": "Comparison format, specific numbers, transformation language",
57
  "tone": "Results-focused, proof-based, transformation",
58
  "psychological_triggers": ["Transformation", "Proof", "Results"],
 
59
  },
60
  "testimonial": {
61
  "name": "Testimonial",
@@ -72,6 +75,7 @@ FRAMEWORKS: Dict[str, Dict[str, Any]] = {
72
  "headline_style": "Quote format, numbers, social proof indicators",
73
  "tone": "Trustworthy, relatable, authentic",
74
  "psychological_triggers": ["Social Proof", "Trust", "Belonging"],
 
75
  },
76
  "lifestyle": {
77
  "name": "Lifestyle",
@@ -88,6 +92,7 @@ FRAMEWORKS: Dict[str, Dict[str, Any]] = {
88
  "headline_style": "Aspirational, emotional, lifestyle-focused",
89
  "tone": "Aspirational, emotional, inspiring",
90
  "psychological_triggers": ["Aspiration", "Desire", "Freedom"],
 
91
  },
92
  "educational": {
93
  "name": "Educational",
@@ -104,6 +109,7 @@ FRAMEWORKS: Dict[str, Dict[str, Any]] = {
104
  "headline_style": "Numbers, curiosity gaps, valuable information promise",
105
  "tone": "Informative, helpful, authoritative",
106
  "psychological_triggers": ["Curiosity", "Knowledge", "Fear of Missing Out"],
 
107
  },
108
  "comparison": {
109
  "name": "Comparison",
@@ -120,6 +126,7 @@ FRAMEWORKS: Dict[str, Dict[str, Any]] = {
120
  "headline_style": "Comparison language, competitive positioning",
121
  "tone": "Confident, comparative, fact-based",
122
  "psychological_triggers": ["Comparison", "Value", "Smart Choice"],
 
123
  },
124
  "storytelling": {
125
  "name": "Storytelling",
@@ -136,6 +143,7 @@ FRAMEWORKS: Dict[str, Dict[str, Any]] = {
136
  "headline_style": "Narrative hooks, story beginnings, curiosity builders",
137
  "tone": "Narrative, emotional, personal",
138
  "psychological_triggers": ["Empathy", "Curiosity", "Emotion"],
 
139
  },
140
  "problem_solution": {
141
  "name": "Problem/Solution",
@@ -152,6 +160,7 @@ FRAMEWORKS: Dict[str, Dict[str, Any]] = {
152
  "headline_style": "Problem questions, solution statements, relief language",
153
  "tone": "Empathetic, problem-aware, solution-focused",
154
  "psychological_triggers": ["Pain Relief", "Problem Awareness", "Hope"],
 
155
  },
156
  "authority": {
157
  "name": "Authority",
@@ -168,6 +177,7 @@ FRAMEWORKS: Dict[str, Dict[str, Any]] = {
168
  "headline_style": "Authority indicators, credentials, expert language",
169
  "tone": "Professional, authoritative, trustworthy",
170
  "psychological_triggers": ["Authority", "Trust", "Expertise"],
 
171
  },
172
  "scarcity": {
173
  "name": "Scarcity",
@@ -184,6 +194,7 @@ FRAMEWORKS: Dict[str, Dict[str, Any]] = {
184
  "headline_style": "Numbers, time limits, availability language",
185
  "tone": "Urgent, exclusive, time-sensitive",
186
  "psychological_triggers": ["FOMO", "Urgency", "Exclusivity"],
 
187
  },
188
  "benefit_stack": {
189
  "name": "Benefit Stack",
@@ -200,6 +211,7 @@ FRAMEWORKS: Dict[str, Dict[str, Any]] = {
200
  "headline_style": "Multiple benefits, parallel structure, value stacking",
201
  "tone": "Value-focused, comprehensive, efficient",
202
  "psychological_triggers": ["Value", "Convenience", "Completeness"],
 
203
  },
204
  "risk_reversal": {
205
  "name": "Risk Reversal",
@@ -216,6 +228,7 @@ FRAMEWORKS: Dict[str, Dict[str, Any]] = {
216
  "headline_style": "Guarantee language, risk removal, confidence builders",
217
  "tone": "Reassuring, confident, risk-free",
218
  "psychological_triggers": ["Security", "Trust", "Risk Reduction"],
 
219
  },
220
  "contrarian": {
221
  "name": "Contrarian",
@@ -232,6 +245,7 @@ FRAMEWORKS: Dict[str, Dict[str, Any]] = {
232
  "headline_style": "Contrarian statements, pattern breaks, unexpected angles",
233
  "tone": "Bold, provocative, thought-provoking",
234
  "psychological_triggers": ["Curiosity", "Differentiation", "Intellectual"],
 
235
  },
236
  "case_study": {
237
  "name": "Case Study",
@@ -248,6 +262,7 @@ FRAMEWORKS: Dict[str, Dict[str, Any]] = {
248
  "headline_style": "Specific examples, real names, concrete results",
249
  "tone": "Proof-based, specific, results-focused",
250
  "psychological_triggers": ["Proof", "Social Proof", "Results"],
 
251
  },
252
  "interactive": {
253
  "name": "Interactive",
@@ -264,8 +279,627 @@ FRAMEWORKS: Dict[str, Dict[str, Any]] = {
264
  "headline_style": "Questions, interactive prompts, participation language",
265
  "tone": "Engaging, interactive, personalized",
266
  "psychological_triggers": ["Engagement", "Personalization", "Curiosity"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  },
268
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
 
270
  # Framework examples by niche
271
  NICHE_FRAMEWORK_EXAMPLES: Dict[str, Dict[str, List[str]]] = {
@@ -392,3 +1026,49 @@ def get_framework_hook_examples(framework_key: str, niche: Optional[str] = None)
392
  framework = FRAMEWORKS.get(framework_key)
393
  return framework.get("hook_examples", []) if framework else []
394
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  "headline_style": "ALL CAPS with news prefix (BREAKING:, URGENT:, ALERT:)",
25
  "tone": "Urgent, newsworthy, time-sensitive",
26
  "psychological_triggers": ["FOMO", "Urgency", "Curiosity"],
27
+ "tags": [],
28
  },
29
  "mobile_post": {
30
  "name": "Mobile Post",
 
41
  "headline_style": "Short, punchy, action-oriented (under 8 words)",
42
  "tone": "Quick, casual, conversational",
43
  "psychological_triggers": ["Convenience", "Speed", "Ease"],
44
+ "tags": [],
45
  },
46
  "before_after": {
47
  "name": "Before/After",
 
58
  "headline_style": "Comparison format, specific numbers, transformation language",
59
  "tone": "Results-focused, proof-based, transformation",
60
  "psychological_triggers": ["Transformation", "Proof", "Results"],
61
+ "tags": [],
62
  },
63
  "testimonial": {
64
  "name": "Testimonial",
 
75
  "headline_style": "Quote format, numbers, social proof indicators",
76
  "tone": "Trustworthy, relatable, authentic",
77
  "psychological_triggers": ["Social Proof", "Trust", "Belonging"],
78
+ "tags": [],
79
  },
80
  "lifestyle": {
81
  "name": "Lifestyle",
 
92
  "headline_style": "Aspirational, emotional, lifestyle-focused",
93
  "tone": "Aspirational, emotional, inspiring",
94
  "psychological_triggers": ["Aspiration", "Desire", "Freedom"],
95
+ "tags": [],
96
  },
97
  "educational": {
98
  "name": "Educational",
 
109
  "headline_style": "Numbers, curiosity gaps, valuable information promise",
110
  "tone": "Informative, helpful, authoritative",
111
  "psychological_triggers": ["Curiosity", "Knowledge", "Fear of Missing Out"],
112
+ "tags": [],
113
  },
114
  "comparison": {
115
  "name": "Comparison",
 
126
  "headline_style": "Comparison language, competitive positioning",
127
  "tone": "Confident, comparative, fact-based",
128
  "psychological_triggers": ["Comparison", "Value", "Smart Choice"],
129
+ "tags": [],
130
  },
131
  "storytelling": {
132
  "name": "Storytelling",
 
143
  "headline_style": "Narrative hooks, story beginnings, curiosity builders",
144
  "tone": "Narrative, emotional, personal",
145
  "psychological_triggers": ["Empathy", "Curiosity", "Emotion"],
146
+ "tags": [],
147
  },
148
  "problem_solution": {
149
  "name": "Problem/Solution",
 
160
  "headline_style": "Problem questions, solution statements, relief language",
161
  "tone": "Empathetic, problem-aware, solution-focused",
162
  "psychological_triggers": ["Pain Relief", "Problem Awareness", "Hope"],
163
+ "tags": [],
164
  },
165
  "authority": {
166
  "name": "Authority",
 
177
  "headline_style": "Authority indicators, credentials, expert language",
178
  "tone": "Professional, authoritative, trustworthy",
179
  "psychological_triggers": ["Authority", "Trust", "Expertise"],
180
+ "tags": [],
181
  },
182
  "scarcity": {
183
  "name": "Scarcity",
 
194
  "headline_style": "Numbers, time limits, availability language",
195
  "tone": "Urgent, exclusive, time-sensitive",
196
  "psychological_triggers": ["FOMO", "Urgency", "Exclusivity"],
197
+ "tags": [],
198
  },
199
  "benefit_stack": {
200
  "name": "Benefit Stack",
 
211
  "headline_style": "Multiple benefits, parallel structure, value stacking",
212
  "tone": "Value-focused, comprehensive, efficient",
213
  "psychological_triggers": ["Value", "Convenience", "Completeness"],
214
+ "tags": [],
215
  },
216
  "risk_reversal": {
217
  "name": "Risk Reversal",
 
228
  "headline_style": "Guarantee language, risk removal, confidence builders",
229
  "tone": "Reassuring, confident, risk-free",
230
  "psychological_triggers": ["Security", "Trust", "Risk Reduction"],
231
+ "tags": [],
232
  },
233
  "contrarian": {
234
  "name": "Contrarian",
 
245
  "headline_style": "Contrarian statements, pattern breaks, unexpected angles",
246
  "tone": "Bold, provocative, thought-provoking",
247
  "psychological_triggers": ["Curiosity", "Differentiation", "Intellectual"],
248
+ "tags": [],
249
  },
250
  "case_study": {
251
  "name": "Case Study",
 
262
  "headline_style": "Specific examples, real names, concrete results",
263
  "tone": "Proof-based, specific, results-focused",
264
  "psychological_triggers": ["Proof", "Social Proof", "Results"],
265
+ "tags": [],
266
  },
267
  "interactive": {
268
  "name": "Interactive",
 
279
  "headline_style": "Questions, interactive prompts, participation language",
280
  "tone": "Engaging, interactive, personalized",
281
  "psychological_triggers": ["Engagement", "Personalization", "Curiosity"],
282
+ "tags": [],
283
+ },
284
+
285
+ "imessage": {
286
+ "name": "iMessage",
287
+ "description": "iOS iMessage screenshot style",
288
+ "best_for": ["personal connection", "conversation", "informal offers"],
289
+ "visual_style": "iOS iMessage screenshot style, blue/green message bubbles, iPhone interface, authentic conversation look",
290
+ "headline_style": "Short, conversational, text-message phrasing",
291
+ "tone": "Personal, native, conversational",
292
+ "hook_examples": [
293
+ "You need to see this",
294
+ "Just saved a ton on this",
295
+ "Wait have you tried this?",
296
+ "My friend sent me this",
297
+ "This actually worked for me",
298
+ ],
299
+ "psychological_triggers": ["Personalization", "Trust", "Intimacy"],
300
+ "container_type": True,
301
+ "tags": ["container_type", "native", "chat_style"],
302
+ "font_style": "San Francisco, system font",
303
+ "colors": {
304
+ "primary": "#007AFF",
305
+ "secondary": "#34C759",
306
+ "background": "#FFFFFF",
307
+ "text": "#000000",
308
+ },
309
+ "authenticity_tips": [
310
+ "Include battery %, time, signal bars",
311
+ "Use realistic conversation format",
312
+ "Keep messages short (2–4 messages)",
313
+ ],
314
+ },
315
+
316
+ "whatsapp": {
317
+ "name": "WhatsApp",
318
+ "description": "WhatsApp chat screenshot style",
319
+ "best_for": ["personal recommendations", "peer-to-peer", "urgent messages"],
320
+ "visual_style": "WhatsApp chat interface, green bubbles, checkmarks, authentic conversation feel",
321
+ "headline_style": "Casual chat-style lines, informal phrasing",
322
+ "tone": "Friendly, casual, personal",
323
+ "hook_examples": [
324
+ "Hey did you see this?",
325
+ "Just got this deal",
326
+ "You have to check this out",
327
+ "OMG this actually works",
328
+ "Sent you the link",
329
+ ],
330
+ "psychological_triggers": ["Trust", "Social Proof", "Urgency"],
331
+ "container_type": True,
332
+ "tags": ["container_type", "native", "chat_style"],
333
+ "font_style": "Helvetica Neue, system font",
334
+ "colors": {
335
+ "primary": "#25D366",
336
+ "secondary": "#128C7E",
337
+ "background": "#ECE5DD",
338
+ "text": "#000000",
339
+ },
340
+ "authenticity_tips": [
341
+ "Include double checkmarks (read receipts)",
342
+ "Add timestamps",
343
+ "Use typical WhatsApp formatting",
344
+ ],
345
+ },
346
+
347
+ "sms": {
348
+ "name": "SMS/Text",
349
+ "description": "Standard SMS text message style",
350
+ "best_for": ["urgent alerts", "personal messages", "time-sensitive offers"],
351
+ "visual_style": "Android/iOS SMS interface, simple text bubbles, notification style",
352
+ "headline_style": "Ultra-short alert-style text",
353
+ "tone": "Direct, urgent, concise",
354
+ "hook_examples": [
355
+ "Urgent: Your rate is ready",
356
+ "Limited time - reply now",
357
+ "Action required",
358
+ "Expires in 24 hrs",
359
+ "You qualify - tap to see",
360
+ ],
361
+ "psychological_triggers": ["Urgency", "Attention", "Interrupt"],
362
+ "container_type": True,
363
+ "tags": ["container_type", "native", "chat_style"],
364
+ "font_style": "Roboto or San Francisco",
365
+ "colors": {
366
+ "primary": "#2196F3",
367
+ "secondary": "#4CAF50",
368
+ "background": "#FFFFFF",
369
+ "text": "#000000",
370
+ },
371
+ "authenticity_tips": [
372
+ "Keep messages very short",
373
+ "Use typical SMS abbreviations",
374
+ "Include carrier/time info",
375
+ ],
376
+ },
377
+
378
+ "news_chyron": {
379
+ "name": "News Chyron",
380
+ "description": "Breaking news ticker style",
381
+ "best_for": ["breaking announcements", "urgent news", "time-sensitive offers"],
382
+ "visual_style": "Red/white scrolling ticker, TV news channel aesthetic, urgent feel",
383
+ "headline_style": "ALL CAPS with BREAKING / ALERT prefixes",
384
+ "tone": "Urgent, authoritative, time-sensitive",
385
+ "hook_examples": [
386
+ "BREAKING: Rates Drop 40%",
387
+ "ALERT: Limited Time Offer",
388
+ ],
389
+ "psychological_triggers": ["Urgency", "FOMO", "Attention"],
390
+ "container_type": True,
391
+ "tags": ["container_type", "alert"],
392
+ "font_style": "Impact, Arial Black, bold sans-serif",
393
+ "colors": {
394
+ "primary": "#FF0000",
395
+ "secondary": "#FFFFFF",
396
+ "background": "#000000",
397
+ "text": "#FFFFFF",
398
+ },
399
+ "authenticity_tips": [
400
+ "Use BREAKING/ALERT prefix",
401
+ "Include news channel logo area",
402
+ "Add scrolling ticker effect",
403
+ ],
404
  },
405
+
406
+ "email_notification": {
407
+ "name": "Email Notification",
408
+ "description": "Email notification/preview style",
409
+ "best_for": ["official communications", "professional offers", "formal announcements"],
410
+ "visual_style": "Email client notification preview, sender + subject UI",
411
+ "headline_style": "Subject-line driven, professional phrasing",
412
+ "tone": "Professional, official, composed",
413
+ "hook_examples": [
414
+ "Your quote is ready for review",
415
+ "Important: Savings confirmation",
416
+ "Official notice - action required",
417
+ "Your application has been approved",
418
+ "Document attached: Your new rate",
419
+ ],
420
+ "psychological_triggers": ["Authority", "Trust"],
421
+ "container_type": True,
422
+ "tags": ["container_type", "native", "document_style"],
423
+ "font_style": "System font, Segoe UI, Roboto",
424
+ "colors": {
425
+ "primary": "#1A73E8",
426
+ "secondary": "#EA4335",
427
+ "background": "#FFFFFF",
428
+ "text": "#202124",
429
+ },
430
+ "authenticity_tips": [
431
+ "Include sender and subject",
432
+ "Add unread badge if relevant",
433
+ "Use realistic timestamps",
434
+ ],
435
+ },
436
+
437
+ "reddit_post": {
438
+ "name": "Reddit Post",
439
+ "description": "Reddit post/comment style",
440
+ "best_for": ["social proof", "user discussions", "authentic testimonials"],
441
+ "visual_style": "Reddit thread UI, upvotes, comments, forum-style layout",
442
+ "headline_style": "Casual post titles, story-driven",
443
+ "tone": "Authentic, conversational, community-driven",
444
+ "hook_examples": [
445
+ "Unpopular opinion but this actually saved me",
446
+ "Has anyone else tried this?",
447
+ "Finally something that worked",
448
+ "PSA: You might be overpaying",
449
+ "TIL there's a way to get lower rates",
450
+ ],
451
+ "psychological_triggers": ["Social Proof", "Belonging", "Authenticity"],
452
+ "container_type": True,
453
+ "tags": ["container_type", "ugc", "chat_style"],
454
+ "font_style": "Noto Sans, Arial",
455
+ "colors": {
456
+ "primary": "#FF4500",
457
+ "secondary": "#0079D3",
458
+ "background": "#DAE0E6",
459
+ "text": "#1A1A1B",
460
+ },
461
+ "authenticity_tips": [
462
+ "Include upvote counts",
463
+ "Add anonymous-style usernames",
464
+ "Reference subreddit context",
465
+ ],
466
+ },
467
+
468
+ "system_notification": {
469
+ "name": "System Notification",
470
+ "description": "iOS/Android system notification popup",
471
+ "best_for": ["urgent alerts", "app notifications", "system messages"],
472
+ "visual_style": "Minimal OS notification banner, plain system UI",
473
+ "headline_style": "1-line alert text",
474
+ "tone": "Neutral, urgent, system-level",
475
+ "hook_examples": [
476
+ "Your savings are ready",
477
+ "New message: Quote available",
478
+ "Reminder: Offer expires soon",
479
+ "Update: Action required",
480
+ "You have a new offer",
481
+ ],
482
+ "psychological_triggers": ["Interrupt", "Urgency", "Attention"],
483
+ "container_type": True,
484
+ "tags": ["container_type", "native", "alert"],
485
+ "font_style": "San Francisco, Roboto, system font",
486
+ "colors": {
487
+ "primary": "#000000",
488
+ "secondary": "#666666",
489
+ "background": "#F2F2F2",
490
+ "text": "#000000",
491
+ },
492
+ "authenticity_tips": [
493
+ "NO emojis or decorative elements",
494
+ "Keep to 1–2 short lines",
495
+ "Use app icon if relevant",
496
+ ],
497
+ "avoid": ["emojis", "decorative elements", "gradients"],
498
+ },
499
+
500
+ "push_notification": {
501
+ "name": "Push Notification",
502
+ "description": "Mobile app push notification style",
503
+ "best_for": ["app alerts", "time-sensitive messages", "quick updates"],
504
+ "visual_style": "Mobile push banner with app icon and timestamp",
505
+ "headline_style": "5–7 word alert headline",
506
+ "tone": "Immediate, concise, action-oriented",
507
+ "hook_examples": [
508
+ "Your quote is ready - tap to view",
509
+ "Limited time: Save up to 40%",
510
+ "New offer just for you",
511
+ "Don't miss out - ends soon",
512
+ "You're approved - see your rate",
513
+ ],
514
+ "psychological_triggers": ["Urgency", "Attention"],
515
+ "container_type": True,
516
+ "tags": ["container_type", "native", "alert"],
517
+ "font_style": "System font",
518
+ "colors": {
519
+ "primary": "#007AFF",
520
+ "secondary": "#8E8E93",
521
+ "background": "#FFFFFF",
522
+ "text": "#000000",
523
+ },
524
+ "authenticity_tips": [
525
+ "Include app icon",
526
+ "Very short headline",
527
+ "Add timestamp (now / 2m ago)",
528
+ ],
529
+ },
530
+ "sticky_note": {
531
+ "name": "Sticky Note",
532
+ "description": "Handwritten sticky note overlay",
533
+ "best_for": ["personal reminders", "quick tips", "informal notes"],
534
+ "visual_style": "Yellow sticky note overlay on image, handwritten-style text, slightly wrinkled paper texture",
535
+ "headline_style": "Short handwritten notes, casual phrasing",
536
+ "tone": "Casual, personal, informal",
537
+ "hook_examples": [
538
+ "Try this first!",
539
+ "Don't forget - call them",
540
+ "Saved me $$$",
541
+ "Best tip I got",
542
+ "Do this before Friday",
543
+ ],
544
+ "psychological_triggers": ["Personalization", "Informality", "Authenticity"],
545
+ "container_type": True,
546
+ "tags": ["container_type", "ugc"],
547
+ "font_style": "Handwriting fonts, marker style",
548
+ "colors": {
549
+ "primary": "#FFEB3B",
550
+ "secondary": "#FFC107",
551
+ "background": "#FFEB3B",
552
+ "text": "#000000",
553
+ },
554
+ "authenticity_tips": [
555
+ "Slight angle/tilt",
556
+ "Handwritten font style",
557
+ "Paper texture/wrinkles",
558
+ ],
559
+ },
560
+
561
+ "memo": {
562
+ "name": "Internal Memo",
563
+ "description": "Office memo/document style",
564
+ "best_for": ["official announcements", "leaked documents", "internal secrets"],
565
+ "visual_style": "Typewritten internal memo on yellow/white paper, document-style layout",
566
+ "headline_style": "Formal memo headers, internal document language",
567
+ "tone": "Formal, serious, confidential",
568
+ "hook_examples": [
569
+ "CONFIDENTIAL: Rate reduction notice",
570
+ "INTERNAL: New savings program",
571
+ "MEMO: Policy update effective immediately",
572
+ "RESTRICTED: Preferred customer rates",
573
+ "OFFICIAL: Your eligibility confirmed",
574
+ ],
575
+ "psychological_triggers": ["Authority", "Curiosity", "Exclusivity"],
576
+ "container_type": True,
577
+ "tags": ["container_type", "ugc", "document_style"],
578
+ "font_style": "Courier, typewriter fonts",
579
+ "colors": {
580
+ "primary": "#000000",
581
+ "secondary": "#333333",
582
+ "background": "#FFFFCC",
583
+ "text": "#000000",
584
+ },
585
+ "authenticity_tips": [
586
+ "Add CONFIDENTIAL stamp if relevant",
587
+ "Include date, to/from fields",
588
+ "Paper texture or fold marks",
589
+ ],
590
+ },
591
+
592
+ "browser_alert": {
593
+ "name": "Browser Alert",
594
+ "description": "Browser popup / alert dialog",
595
+ "best_for": ["urgent warnings", "system alerts", "confirmation messages"],
596
+ "visual_style": "Browser dialog box with alert icon, OK/Cancel buttons",
597
+ "headline_style": "Short alert-style system messages",
598
+ "tone": "Urgent, system-level, direct",
599
+ "hook_examples": [
600
+ "You qualify for savings. Continue?",
601
+ "Limited offer detected. Accept?",
602
+ "Your rate is ready. View now?",
603
+ "Warning: Price increases tomorrow",
604
+ "Confirm your discount before it expires",
605
+ ],
606
+ "psychological_triggers": ["Urgency", "Interrupt", "Attention"],
607
+ "container_type": True,
608
+ "tags": ["container_type", "alert", "document_style"],
609
+ "font_style": "System font, Segoe UI",
610
+ "colors": {
611
+ "primary": "#0078D4",
612
+ "secondary": "#D83B01",
613
+ "background": "#FFFFFF",
614
+ "text": "#000000",
615
+ },
616
+ "authenticity_tips": [
617
+ "Include browser chrome",
618
+ "Add alert icon",
619
+ "Use realistic button styling",
620
+ ],
621
+ },
622
+
623
+ "social_post": {
624
+ "name": "Social Media Post",
625
+ "description": "Facebook / Instagram feed post style",
626
+ "best_for": ["social proof", "user content", "organic feel"],
627
+ "visual_style": "Social media feed UI with profile, likes, comments",
628
+ "headline_style": "Casual post captions, organic phrasing",
629
+ "tone": "Social, relatable, authentic",
630
+ "hook_examples": [
631
+ "Okay so I finally did the thing",
632
+ "No one talks about this but",
633
+ "Just had to share this",
634
+ "Game changer honestly",
635
+ "Why did I wait so long",
636
+ ],
637
+ "psychological_triggers": ["Social Proof", "Belonging", "Authenticity"],
638
+ "container_type": True,
639
+ "tags": ["container_type", "ugc", "chat_style"],
640
+ "font_style": "Helvetica, system font",
641
+ "colors": {
642
+ "primary": "#1877F2",
643
+ "secondary": "#E4405F",
644
+ "background": "#FFFFFF",
645
+ "text": "#1C1E21",
646
+ },
647
+ "authenticity_tips": [
648
+ "Include profile picture",
649
+ "Add like/comment counts",
650
+ "Use platform-native formatting",
651
+ ],
652
+ },
653
+
654
+ "standard": {
655
+ "name": "Standard Ad",
656
+ "description": "Clean, professional ad format",
657
+ "best_for": ["professional campaigns", "brand awareness", "general advertising"],
658
+ "visual_style": "Clean ad layout, clear headline and CTA, professional design",
659
+ "headline_style": "Clear benefit-driven headline",
660
+ "tone": "Professional, neutral, brand-safe",
661
+ "hook_examples": [
662
+ "Get your free quote in minutes",
663
+ "Compare rates. Save money.",
664
+ "Trusted by thousands of customers",
665
+ "Simple. Fast. Affordable.",
666
+ "See how much you could save",
667
+ ],
668
+ "psychological_triggers": ["Clarity", "Trust", "Professionalism"],
669
+ "container_type": True,
670
+ "tags": ["container_type"],
671
+ "font_style": "Clean sans-serif fonts",
672
+ "colors": {
673
+ "primary": "#2196F3",
674
+ "secondary": "#FF9800",
675
+ "background": "#FFFFFF",
676
+ "text": "#212121",
677
+ },
678
+ "authenticity_tips": [
679
+ "Clear visual hierarchy",
680
+ "Prominent CTA",
681
+ "Brand-consistent styling",
682
+ ],
683
+ },
684
+
685
+ "telegram": {
686
+ "name": "Telegram",
687
+ "description": "Telegram chat message style",
688
+ "best_for": ["personal messages", "group chats", "informal communication"],
689
+ "visual_style": "Telegram chat interface, blue message bubbles",
690
+ "headline_style": "Casual chat-style lines",
691
+ "tone": "Informal, conversational",
692
+ "hook_examples": [
693
+ "Check this out",
694
+ "Thought you'd want to see",
695
+ "This is the one",
696
+ "Finally found it",
697
+ "Sending you the link",
698
+ ],
699
+ "psychological_triggers": ["Personalization", "Trust"],
700
+ "container_type": True,
701
+ "tags": ["container_type", "native", "chat_style"],
702
+ "font_style": "Roboto, system font",
703
+ "colors": {
704
+ "primary": "#3390EC",
705
+ "secondary": "#0088CC",
706
+ "background": "#FFFFFF",
707
+ "text": "#000000",
708
+ },
709
+ "authenticity_tips": [
710
+ "Include Telegram UI elements",
711
+ "Use typical Telegram formatting",
712
+ "Add read receipts if relevant",
713
+ ],
714
+ },
715
+
716
+ "slack": {
717
+ "name": "Slack",
718
+ "description": "Slack workspace message style",
719
+ "best_for": ["team communication", "workplace announcements", "professional updates"],
720
+ "visual_style": "Slack channel UI with avatars and message threads",
721
+ "headline_style": "Short internal-style announcements",
722
+ "tone": "Professional, internal, conversational",
723
+ "hook_examples": [
724
+ "Heads up - new rates live",
725
+ "FYI team discount available",
726
+ "Quick update on savings program",
727
+ "Reminder: Enrollment closes Friday",
728
+ "PSA: Worth checking your rate",
729
+ ],
730
+ "psychological_triggers": ["Authority", "Belonging", "Professionalism"],
731
+ "container_type": True,
732
+ "tags": ["container_type", "native", "chat_style"],
733
+ "font_style": "Lato, Slack font",
734
+ "colors": {
735
+ "primary": "#4A154B",
736
+ "secondary": "#36C5F0",
737
+ "background": "#FFFFFF",
738
+ "text": "#1D1C1D",
739
+ },
740
+ "authenticity_tips": [
741
+ "Include channel name",
742
+ "Add user avatar",
743
+ "Use Slack message formatting",
744
+ ],
745
+ },
746
+
747
+ "instagram_story": {
748
+ "name": "Instagram Story",
749
+ "description": "Instagram story frame style",
750
+ "best_for": ["social media engagement", "story-style content", "mobile-first ads"],
751
+ "visual_style": "Vertical 9:16 Instagram story UI with overlays",
752
+ "headline_style": "Big bold overlays, short phrases",
753
+ "tone": "Energetic, visual, trendy",
754
+ "hook_examples": [
755
+ "Swipe to see 👆",
756
+ "No one talks about this",
757
+ "POV: You finally save",
758
+ "Wait for it...",
759
+ "This changed everything",
760
+ ],
761
+ "psychological_triggers": ["Engagement", "FOMO", "Visual Appeal"],
762
+ "container_type": True,
763
+ "tags": ["container_type", "ugc"],
764
+ "font_style": "Instagram font, system font",
765
+ "colors": {
766
+ "primary": "#E4405F",
767
+ "secondary": "#833AB4",
768
+ "background": "#000000",
769
+ "text": "#FFFFFF",
770
+ },
771
+ "authenticity_tips": [
772
+ "Vertical format (9:16)",
773
+ "Include story UI elements",
774
+ "Use Instagram-style fonts",
775
+ ],
776
+ },
777
+
778
+ "tiktok_style": {
779
+ "name": "TikTok Style",
780
+ "description": "TikTok video frame style",
781
+ "best_for": ["youth engagement", "viral content", "trending topics"],
782
+ "visual_style": "Vertical TikTok video UI with overlays",
783
+ "headline_style": "Bold, punchy on-screen text",
784
+ "tone": "High-energy, playful, trend-driven",
785
+ "hook_examples": [
786
+ "POV: you finally get a good rate",
787
+ "Wait this actually works??",
788
+ "No one told me this",
789
+ "The secret they don't want you to know",
790
+ "Drop a 👋 if you need this",
791
+ ],
792
+ "psychological_triggers": ["Novelty", "Engagement", "Trendiness"],
793
+ "container_type": True,
794
+ "tags": ["container_type", "ugc"],
795
+ "font_style": "TikTok font, bold sans-serif",
796
+ "colors": {
797
+ "primary": "#000000",
798
+ "secondary": "#FE2C55",
799
+ "background": "#000000",
800
+ "text": "#FFFFFF",
801
+ },
802
+ "authenticity_tips": [
803
+ "Vertical video format",
804
+ "Bold text overlays",
805
+ "Trending style elements",
806
+ ],
807
+ },
808
+
809
+ "linkedin_post": {
810
+ "name": "LinkedIn Post",
811
+ "description": "LinkedIn feed post style",
812
+ "best_for": ["professional networking", "B2B marketing", "career-focused content"],
813
+ "visual_style": "LinkedIn feed UI with professional profile elements",
814
+ "headline_style": "Professional, insight-driven headlines",
815
+ "tone": "Professional, authoritative, thoughtful",
816
+ "hook_examples": [
817
+ "3 lessons I learned about saving on coverage",
818
+ "Why most people overpay (and how to fix it)",
819
+ "The one change that cut my costs 40%",
820
+ "A thread on what actually moves the needle",
821
+ "Unpopular take: You might be underinsured",
822
+ ],
823
+ "psychological_triggers": ["Authority", "Credibility", "Professionalism"],
824
+ "container_type": True,
825
+ "tags": ["container_type", "ugc"],
826
+ "font_style": "LinkedIn font, professional sans-serif",
827
+ "colors": {
828
+ "primary": "#0077B5",
829
+ "secondary": "#000000",
830
+ "background": "#FFFFFF",
831
+ "text": "#000000",
832
+ },
833
+ "authenticity_tips": [
834
+ "Include profile elements",
835
+ "Maintain professional tone",
836
+ "Use LinkedIn-style formatting",
837
+ ],
838
+ },
839
+
840
+ "app_store_listing": {
841
+ "name": "App Store Listing",
842
+ "description": "App store screenshot style",
843
+ "best_for": ["app promotion", "mobile apps", "app features"],
844
+ "visual_style": "App store listing UI with icon, ratings, screenshots",
845
+ "headline_style": "Feature-focused, benefit-driven",
846
+ "tone": "Polished, product-focused",
847
+ "hook_examples": [
848
+ "Get your quote in 2 minutes",
849
+ "Compare. Save. Done.",
850
+ "Rated 4.9 by 50,000+ users",
851
+ "Simple quotes. Real savings.",
852
+ "The easy way to lower your rate",
853
+ ],
854
+ "psychological_triggers": ["Trust", "Clarity", "Social Proof"],
855
+ "container_type": True,
856
+ "tags": ["container_type", "native"],
857
+ "font_style": "San Francisco, system font",
858
+ "colors": {
859
+ "primary": "#007AFF",
860
+ "secondary": "#FF9500",
861
+ "background": "#FFFFFF",
862
+ "text": "#000000",
863
+ },
864
+ "authenticity_tips": [
865
+ "Include app icon",
866
+ "Star ratings",
867
+ "Use app store UI elements",
868
+ ],
869
+ },
870
+
871
+ "email_signature": {
872
+ "name": "Email Signature",
873
+ "description": "Professional email signature style",
874
+ "best_for": ["professional communication", "B2B outreach", "formal announcements"],
875
+ "visual_style": "Professional email signature with contact details and logo",
876
+ "headline_style": "Name / role driven hierarchy",
877
+ "tone": "Formal, professional",
878
+ "hook_examples": [
879
+ "Get your free quote today",
880
+ "Trusted advisor | Licensed agent",
881
+ "Let's find your best rate",
882
+ "Serving clients since 2010",
883
+ "Reply for a personalized quote",
884
+ ],
885
+ "psychological_triggers": ["Authority", "Trust"],
886
+ "container_type": True,
887
+ "tags": ["container_type", "document_style"],
888
+ "font_style": "Arial, Helvetica, professional fonts",
889
+ "colors": {
890
+ "primary": "#000000",
891
+ "secondary": "#666666",
892
+ "background": "#FFFFFF",
893
+ "text": "#000000",
894
+ },
895
+ "authenticity_tips": [
896
+ "Include contact information",
897
+ "Clean professional formatting",
898
+ "Company logo if relevant",
899
+ ],
900
+ },
901
+
902
+ }
903
 
904
  # Framework examples by niche
905
  NICHE_FRAMEWORK_EXAMPLES: Dict[str, Dict[str, List[str]]] = {
 
1026
  framework = FRAMEWORKS.get(framework_key)
1027
  return framework.get("hook_examples", []) if framework else []
1028
 
1029
+
1030
+ # ---------------------------------------------------------------------------
1031
+ # Container-type framework helpers (visual format / "container" = framework with container_type=True)
1032
+ # ---------------------------------------------------------------------------
1033
+
1034
+ def get_framework_visual_guidance(framework_key: str) -> str:
1035
+ """Get visual guidance for a framework (used for image generation)."""
1036
+ fw = FRAMEWORKS.get(framework_key)
1037
+ if fw:
1038
+ return fw.get("visual_style", "") or fw.get("visual_guidance", "")
1039
+ return f"Framework: {framework_key}, authentic appearance"
1040
+
1041
+
1042
+ def get_all_container_type_frameworks() -> Dict[str, Dict[str, Any]]:
1043
+ """Get all frameworks that are container-type (native/visual format styles)."""
1044
+ return {k: v for k, v in FRAMEWORKS.items() if v.get("container_type")}
1045
+
1046
+
1047
+ def get_frameworks_by_tag(tag: str) -> List[str]:
1048
+ """Return framework keys that have the given tag."""
1049
+ return [k for k, v in FRAMEWORKS.items() if tag in v.get("tags", [])]
1050
+
1051
+
1052
+ def get_native_frameworks() -> List[str]:
1053
+ """Framework keys that look like native app interfaces."""
1054
+ return get_frameworks_by_tag("native")
1055
+
1056
+
1057
+ def get_ugc_frameworks() -> List[str]:
1058
+ """Framework keys that look like user-generated content."""
1059
+ return get_frameworks_by_tag("ugc")
1060
+
1061
+
1062
+ def get_alert_frameworks() -> List[str]:
1063
+ """Framework keys that create urgency / alert style."""
1064
+ return get_frameworks_by_tag("alert")
1065
+
1066
+
1067
+ def get_random_container_type_framework() -> Dict[str, Any]:
1068
+ """Get a random framework that is a container-type (for ad format selection)."""
1069
+ container_frameworks = get_all_container_type_frameworks()
1070
+ if not container_frameworks:
1071
+ return get_random_framework()
1072
+ key = random.choice(list(container_frameworks.keys()))
1073
+ return {"key": key, **container_frameworks[key]}
1074
+
data/glp1.py CHANGED
@@ -638,34 +638,6 @@ COPY_TEMPLATES = [
638
  },
639
  ]
640
 
641
- # CTAs for variety - Updated with high-converting patterns
642
- CTAS = [
643
- # Discovery/Eligibility CTAs (highest conversion)
644
- "See If You Qualify",
645
- "Check Your Eligibility",
646
- "Take The Quiz",
647
- "Calculate Your Results",
648
- "See How Much You Could Lose",
649
-
650
- # Action CTAs
651
- "Start Your Transformation",
652
- "Get Your Personalized Plan",
653
- "Claim Your Consultation",
654
- "Learn More",
655
- "See Your Options",
656
-
657
- # Urgency CTAs
658
- "Start Now",
659
- "Don't Wait Another Day",
660
- "Begin Today",
661
- "Get Started",
662
-
663
- # Specific CTAs
664
- "Get Your Prescription",
665
- "Join Thousands Who Transformed",
666
- "See Real Results",
667
- ]
668
-
669
  # ============================================================================
670
  # SECTION 3: VISUAL LIBRARY - Category Entry Points & Psychological Moments
671
  # Based on DirectMeds Marketing Brief: 7 Ws, CEPs, Life-Force 8, Stage of Awareness
@@ -986,6 +958,84 @@ def get_niche_data():
986
  "creative_directions": CREATIVE_DIRECTIONS,
987
  "visual_moods": VISUAL_MOODS,
988
  "copy_templates": COPY_TEMPLATES,
989
- "ctas": CTAS,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
990
  }
991
 
 
638
  },
639
  ]
640
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
  # ============================================================================
642
  # SECTION 3: VISUAL LIBRARY - Category Entry Points & Psychological Moments
643
  # Based on DirectMeds Marketing Brief: 7 Ws, CEPs, Life-Force 8, Stage of Awareness
 
958
  "creative_directions": CREATIVE_DIRECTIONS,
959
  "visual_moods": VISUAL_MOODS,
960
  "copy_templates": COPY_TEMPLATES,
961
+ "niche_guidance": """
962
+ NICHE-SPECIFIC REQUIREMENTS (GLP-1 / WEIGHT LOSS):
963
+ - Focus on TRANSFORMATION and emotional journey
964
+ - Use VARIETY in visual concepts: quiz/interactive interfaces, medical/doctor settings, scale/measurement moments, mirror reflections, lifestyle/confidence scenes, testimonial portraits, celebrity references, before/after (only when strategy specifically calls for it), or other diverse visual types
965
+ - Use authentic emotions: shame, hope, confidence, transformation, urgency, curiosity
966
+ - Target pain points: failed diets, body image, social acceptance, health fears
967
+ - Messaging must feel ASPIRATIONAL with urgency
968
+ - Visual concepts should vary: quiz screens, medical authority, scale moments, lifestyle changes, confidence moments, testimonial style, or transformation proof (when appropriate)
969
+ - AVOID: defaulting to before/after for every image - use diverse visual approaches
970
+ - AVOID: unrealistic body standards, extreme before/after manipulation
971
+ - AVOID: medical claims without proper framing, shame-based imagery only
972
+ - Include elements of: medical authority, social proof, simplicity, variety
973
+ """,
974
+ "price_config": {
975
+ "guidance": "Consider using specific prices if relevant to the ad format. Typical range: $197-$497. Only include if it enhances the message and fits the ad strategy.",
976
+ "type": "weight_loss",
977
+ },
978
+ "number_config": {
979
+ "type": "weight_loss",
980
+ "before_range": [180, 280],
981
+ "loss_range": [25, 65],
982
+ "days_options": [60, 90, 120],
983
+ "sizes_range": [2, 5],
984
+ "labels": {"before": "lbs", "after": "lbs", "difference": "lbs", "days": "days", "sizes": "dress sizes", "metric": "pounds lost"},
985
+ },
986
+ "image_guidance": """
987
+ NICHE REQUIREMENTS (GLP-1):
988
+ - Use VARIETY in visual types
989
+ - Visual options include: quiz/interactive interfaces, doctor/medical settings, person on scale, mirror reflections, lifestyle/confidence scenes, testimonial portraits, celebrity references, measurement moments, or before/after (only when the strategy specifically requires it)
990
+ - Show REAL people in various moments (not just transformation)
991
+ - Body types should be relatable (not fitness models)
992
+ - Include authentic lifestyle elements: real clothes, real settings
993
+ - Medical elements should look professional and trustworthy
994
+ - Confidence/transformation moments should feel genuine
995
+ - AVOID: defaulting to before/after for every image - prioritize visual variety
996
+ - AVOID: extreme body manipulation, unrealistic transformations, shame-inducing imagery
997
+ - AVOID: elderly people over 65, senior citizens, very old looking people, gray-haired elderly groups
998
+ - ENCOURAGE: diverse visual concepts that match the strategy (quiz for quiz strategy, medical for authority strategy, lifestyle for aspiration, etc.)
999
+ - AGE GUIDANCE: Show people aged 30-50 primarily. DO NOT default to elderly/senior citizens. Target audience is middle-aged adults (30s-40s), NOT seniors.
1000
+ """,
1001
+ "image_niche_guidance_short": """
1002
+ NICHE: GLP-1 / Weight Loss
1003
+ - Show real transformation moments
1004
+ - People should be relatable, not fitness models
1005
+ - Confidence and lifestyle improvement focus""",
1006
+ "prompt_sanitization_replacements": [
1007
+ (r'\b(elderly|senior|seniors|old people|old person)\b', 'middle-aged adult'),
1008
+ (r'\b(grandparent|grandfather|grandmother|grandma|grandpa)\b', 'parent'),
1009
+ (r'\b(70[\s-]?year[\s-]?old|80[\s-]?year[\s-]?old|65[\s-]?year[\s-]?old)\b', '40-year-old'),
1010
+ (r'\b(in their 60s|in their 70s|in their 80s)\b', 'in their 40s'),
1011
+ (r'\b(retirement age|retired person|retiree)\b', 'working professional'),
1012
+ (r'\bgray[\s-]?haired elderly\b', 'confident adult'),
1013
+ (r'\bsenior citizen\b', 'adult'),
1014
+ ],
1015
+ "visual_library": {
1016
+ "diet_fatigue": DIET_FATIGUE_VISUALS,
1017
+ "clothes_body_awareness": CLOTHES_BODY_AWARENESS_VISUALS,
1018
+ "health_wake_up_calls": HEALTH_WAKE_UP_CALLS_VISUALS,
1019
+ "mental_load_food_noise": MENTAL_LOAD_FOOD_NOISE_VISUALS,
1020
+ "survival_life_extension": SURVIVAL_LIFE_EXTENSION_VISUALS,
1021
+ "freedom_from_fear_pain": FREEDOM_FROM_FEAR_PAIN_VISUALS,
1022
+ "sexual_companionship": SEXUAL_COMPANIONSHIP_VISUALS,
1023
+ "comfortable_living": COMFORTABLE_LIVING_VISUALS,
1024
+ "superiority_winning": SUPERIORITY_WINNING_VISUALS,
1025
+ "care_protection_loved_ones": CARE_PROTECTION_LOVED_ONES_VISUALS,
1026
+ "social_approval": SOCIAL_APPROVAL_VISUALS,
1027
+ "why_moment": WHY_MOMENT_VISUALS,
1028
+ "when_moment": WHEN_MOMENT_VISUALS,
1029
+ "where_moment": WHERE_MOMENT_VISUALS,
1030
+ "with_whom": WITH_WHOM_VISUALS,
1031
+ "with_what_activity": WITH_WHAT_ACTIVITY_VISUALS,
1032
+ "while_wearing_using": WHILE_WEARING_USING_VISUALS,
1033
+ "while_feeling": WHILE_FEELING_VISUALS,
1034
+ "unaware": UNAWARE_VISUALS,
1035
+ "problem_aware": PROBLEM_AWARE_VISUALS,
1036
+ "solution_aware": SOLUTION_AWARE_VISUALS,
1037
+ "product_aware": PRODUCT_AWARE_VISUALS,
1038
+ "most_aware": MOST_AWARE_VISUALS,
1039
+ },
1040
  }
1041
 
data/home_insurance.py CHANGED
@@ -688,25 +688,6 @@ COPY_TEMPLATES = [
688
  },
689
  ]
690
 
691
- CTAS = [
692
- "Check Your Eligibility",
693
- "See If You Qualify",
694
- "Check Eligibility Now",
695
- "Tap To See Your Rate",
696
- "Calculate Your Savings",
697
- "Get Your Free Quote",
698
- "See Your New Rate",
699
- "Find Out How Much You Can Save",
700
- "Click To See Your Savings",
701
- "Get Protected Now",
702
- "Start Saving Today",
703
- "Don't Miss This",
704
- "Claim Your Rate",
705
- "Seniors: Check Your Rate",
706
- "See Senior Rates",
707
- "50+: Get Your Quote",
708
- ]
709
-
710
  # ============================================================================
711
  # SECTION 4: AGGREGATED DATA
712
  # ============================================================================
@@ -737,5 +718,62 @@ def get_niche_data():
737
  "creative_directions": CREATIVE_DIRECTIONS,
738
  "visual_moods": VISUAL_MOODS,
739
  "copy_templates": COPY_TEMPLATES,
740
- "ctas": CTAS,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
741
  }
 
688
  },
689
  ]
690
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
691
  # ============================================================================
692
  # SECTION 4: AGGREGATED DATA
693
  # ============================================================================
 
718
  "creative_directions": CREATIVE_DIRECTIONS,
719
  "visual_moods": VISUAL_MOODS,
720
  "copy_templates": COPY_TEMPLATES,
721
+ "niche_guidance": """
722
+ NICHE-SPECIFIC REQUIREMENTS (HOME INSURANCE):
723
+ - Focus on REALISTIC scenarios homeowners can identify with
724
+ - Show real situations: storm damage, fire, flood, theft, protection
725
+ - Use authentic emotions: fear of loss, relief of protection, family safety
726
+ - Target pain points: high premiums, disaster fears, coverage gaps, savings desire
727
+ - Messaging must feel URGENT but not fear-mongering
728
+ - Visual concepts: real homes, real disasters, real families, real protection
729
+ - AVOID: fantasy elements, castles, fortresses, unrealistic scenarios
730
+ - AVOID: generic stock photo families, overly polished imagery
731
+ """,
732
+ "price_config": {
733
+ "guidance": "Consider using oddly specific prices (e.g., $97.33 instead of $100) if the ad format calls for it. Typical range: $37-$147/month. Only include if it enhances believability and fits the ad format.",
734
+ "type": "insurance",
735
+ "before_range": [1200, 2400],
736
+ "savings_pct_range": [0.55, 0.75],
737
+ "labels": {"before": "$/year", "after": "$/year", "difference": "$", "metric": "savings per year"},
738
+ },
739
+ "number_config": {
740
+ "type": "savings",
741
+ "before_range": [1200, 2400],
742
+ "savings_pct_range": [0.55, 0.75],
743
+ "labels": {"before": "$/year", "after": "$/year", "difference": "$", "metric": "savings per year"},
744
+ },
745
+ "image_guidance": """
746
+ NICHE REQUIREMENTS:
747
+ - Show REAL American suburban homes (single-family, realistic architecture)
748
+ - Include authentic elements: lawns, driveways, neighborhoods
749
+ - People should look like real homeowners (diverse, relatable, 30-60 age range)
750
+ - Disaster scenes should be realistic but not gratuitous
751
+ - Protection/safety imagery should feel reassuring, not corporate
752
+ - AVOID: mansions, castles, fantasy homes, unrealistic scenarios
753
+ """,
754
+ "image_niche_guidance_short": """
755
+ NICHE: Home Insurance
756
+ - Show real American homes, suburban settings
757
+ - People should be diverse, relatable homeowners (30-60)
758
+ - Disaster scenes should be realistic but not gratuitous""",
759
+ "prompt_sanitization_replacements": [
760
+ (r'\b(flat|apartment|condo)\b(?! insurance)', 'house'),
761
+ (r'\b(mansion|castle|estate)\b', 'suburban home'),
762
+ (r'\b(european|british|uk) style home\b', 'American suburban home'),
763
+ ],
764
+ "visual_library": {
765
+ "protection_safety": PROTECTION_SAFETY_VISUALS,
766
+ "disaster_fear": DISASTER_FEAR_VISUALS,
767
+ "family_emotional": FAMILY_EMOTIONAL_VISUALS,
768
+ "first_time_homebuyer": FIRST_TIME_HOMEBUYER_VISUALS,
769
+ "asset_investment": ASSET_INVESTMENT_VISUALS,
770
+ "problem_risk": PROBLEM_RISK_VISUALS,
771
+ "relief": RELIEF_VISUALS,
772
+ "mortgage_bank": MORTGAGE_BANK_VISUALS,
773
+ "comparison_choice": COMPARISON_CHOICE_VISUALS,
774
+ "minimal_symbolic": MINIMAL_SYMBOLIC_VISUALS,
775
+ "lifestyle": LIFESTYLE_VISUALS,
776
+ "text_first": TEXT_FIRST_VISUALS,
777
+ "seasonal": SEASONAL_VISUALS,
778
+ },
779
  }
frontend/app/gallery/[id]/page.tsx CHANGED
@@ -55,7 +55,7 @@ export default function AdDetailPage() {
55
  setImageSrc(null);
56
  }
57
  } else {
58
- const { primary, fallback } = getImageUrlFallback(ad.r2_url || ad.image_url, ad.image_filename);
59
  setImageSrc(primary || fallback);
60
  }
61
  setImageError(false);
@@ -151,7 +151,7 @@ export default function AdDetailPage() {
151
  filename = ad.metadata.original_image_filename || `original-${ad.id}.png`;
152
  } else {
153
  // Download corrected/current image
154
- const { primary } = getImageUrlFallback(ad.r2_url || ad.image_url, ad.image_filename);
155
  imageUrl = primary || null;
156
  filename = ad.image_filename || `ad-${ad.id}.png`;
157
  }
@@ -186,7 +186,7 @@ export default function AdDetailPage() {
186
 
187
  const handleImageError = () => {
188
  if (!ad) return;
189
- const { fallback } = getImageUrlFallback(ad.image_url, ad.image_filename);
190
  if (!imageError && fallback && imageSrc) {
191
  setImageSrc(fallback);
192
  setImageError(true);
 
55
  setImageSrc(null);
56
  }
57
  } else {
58
+ const { primary, fallback } = getImageUrlFallback(ad.image_url, ad.image_filename, ad.r2_url);
59
  setImageSrc(primary || fallback);
60
  }
61
  setImageError(false);
 
151
  filename = ad.metadata.original_image_filename || `original-${ad.id}.png`;
152
  } else {
153
  // Download corrected/current image
154
+ const { primary } = getImageUrlFallback(ad.image_url, ad.image_filename, ad.r2_url);
155
  imageUrl = primary || null;
156
  filename = ad.image_filename || `ad-${ad.id}.png`;
157
  }
 
186
 
187
  const handleImageError = () => {
188
  if (!ad) return;
189
+ const { fallback } = getImageUrlFallback(ad.image_url, ad.image_filename, ad.r2_url);
190
  if (!imageError && fallback && imageSrc) {
191
  setImageSrc(fallback);
192
  setImageError(true);
frontend/app/page.tsx CHANGED
@@ -283,7 +283,7 @@ export default function Dashboard() {
283
  ) : (
284
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
285
  {recentAds.map((ad, index) => {
286
- const { primary, fallback } = getImageUrlFallback(ad.image_url, ad.image_filename);
287
  const initialSrc = primary || fallback;
288
 
289
  return (
 
283
  ) : (
284
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
285
  {recentAds.map((ad, index) => {
286
+ const { primary, fallback } = getImageUrlFallback(ad.image_url, ad.image_filename, ad.r2_url);
287
  const initialSrc = primary || fallback;
288
 
289
  return (
frontend/components/gallery/AdCard.tsx CHANGED
@@ -28,7 +28,7 @@ export const AdCard: React.FC<AdCardProps> = memo(({
28
  onSelect,
29
  hasAnySelection = false,
30
  }) => {
31
- const { primary, fallback } = getImageUrlFallback(ad.image_url, ad.image_filename);
32
  const [imageSrc, setImageSrc] = useState<string | null>(primary || fallback);
33
  const [imageError, setImageError] = useState(false);
34
 
 
28
  onSelect,
29
  hasAnySelection = false,
30
  }) => {
31
+ const { primary, fallback } = getImageUrlFallback(ad.image_url, ad.image_filename, ad.r2_url);
32
  const [imageSrc, setImageSrc] = useState<string | null>(primary || fallback);
33
  const [imageError, setImageError] = useState(false);
34
 
frontend/lib/utils/formatters.ts CHANGED
@@ -80,29 +80,38 @@ export const truncateText = (text: string, maxLength: number): string => {
80
  return text.slice(0, maxLength) + "...";
81
  };
82
 
83
- export const getImageUrl = (imageUrl: string | null | undefined, filename: string | null | undefined): string | null => {
84
- // Prefer R2/external URL if it's a valid HTTP/HTTPS URL (R2 URLs or Replicate URLs)
85
- // This ensures R2 URLs are always used when available
86
- if (imageUrl && typeof imageUrl === 'string' && (imageUrl.startsWith('http://') || imageUrl.startsWith('https://'))) {
87
- return imageUrl;
88
- }
89
- // Fallback to local filename only if no external URL exists
90
- if (filename) {
91
- const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
92
- return `${apiUrl}/images/${filename}`;
93
- }
 
 
 
 
94
  return null;
95
  };
96
 
97
- export const getImageUrlFallback = (imageUrl: string | null | undefined, filename: string | null | undefined): { primary: string | null; fallback: string | null } => {
98
- // Primary: R2/external URL if it's a valid HTTP/HTTPS URL
99
- // This ensures R2 URLs are always prioritized
100
- const primary = (imageUrl && typeof imageUrl === 'string' && (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')))
101
- ? imageUrl
102
- : null;
103
- // Fallback: local filename only if no external URL
104
- const fallback = filename
105
- ? `${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/images/${filename}`
106
- : null;
 
 
 
 
 
107
  return { primary, fallback };
108
  };
 
80
  return text.slice(0, maxLength) + "...";
81
  };
82
 
83
+ const getApiBase = () => process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
84
+
85
+ const isAbsoluteUrl = (url: string) =>
86
+ typeof url === "string" && (url.startsWith("http://") || url.startsWith("https://"));
87
+
88
+ export const getImageUrl = (
89
+ imageUrl: string | null | undefined,
90
+ filename: string | null | undefined,
91
+ r2Url?: string | null
92
+ ): string | null => {
93
+ const best = r2Url ?? imageUrl;
94
+ if (best && isAbsoluteUrl(best)) return best;
95
+ if (filename) return `${getApiBase()}/images/${filename}`;
96
+ if (imageUrl && typeof imageUrl === "string" && imageUrl.startsWith("/"))
97
+ return `${getApiBase()}${imageUrl}`;
98
  return null;
99
  };
100
 
101
+ export const getImageUrlFallback = (
102
+ imageUrl: string | null | undefined,
103
+ filename: string | null | undefined,
104
+ r2Url?: string | null
105
+ ): { primary: string | null; fallback: string | null } => {
106
+ const apiBase = getApiBase();
107
+ const best = r2Url ?? imageUrl;
108
+ const primary =
109
+ best && isAbsoluteUrl(best) ? best : null;
110
+ const fromFilename = filename ? `${apiBase}/images/${filename}` : null;
111
+ const fromRelative =
112
+ imageUrl && typeof imageUrl === "string" && imageUrl.startsWith("/")
113
+ ? `${apiBase}${imageUrl}`
114
+ : null;
115
+ const fallback = fromFilename ?? fromRelative ?? null;
116
  return { primary, fallback };
117
  };
main.py CHANGED
@@ -2321,6 +2321,7 @@ async def list_stored_ads(
2321
  "cta": ad.get("cta", ""),
2322
  "psychological_angle": ad.get("psychological_angle", ""),
2323
  "image_url": ad.get("image_url"),
 
2324
  "image_filename": ad.get("image_filename"),
2325
  "image_model": ad.get("image_model"),
2326
  "angle_key": ad.get("angle_key"),
 
2321
  "cta": ad.get("cta", ""),
2322
  "psychological_angle": ad.get("psychological_angle", ""),
2323
  "image_url": ad.get("image_url"),
2324
+ "r2_url": ad.get("r2_url"),
2325
  "image_filename": ad.get("image_filename"),
2326
  "image_model": ad.get("image_model"),
2327
  "angle_key": ad.get("angle_key"),
services/database.py CHANGED
@@ -106,6 +106,7 @@ class DatabaseService:
106
  why_it_works: str,
107
  username: str, # Required: username of the user creating the ad
108
  image_url: Optional[str] = None,
 
109
  image_filename: Optional[str] = None,
110
  image_model: Optional[str] = None,
111
  image_seed: Optional[int] = None,
@@ -146,6 +147,7 @@ class DatabaseService:
146
  "why_it_works": why_it_works,
147
  "username": username, # Store username for filtering
148
  "image_url": image_url,
 
149
  "image_filename": image_filename,
150
  "image_model": image_model,
151
  "image_seed": image_seed,
 
106
  why_it_works: str,
107
  username: str, # Required: username of the user creating the ad
108
  image_url: Optional[str] = None,
109
+ r2_url: Optional[str] = None,
110
  image_filename: Optional[str] = None,
111
  image_model: Optional[str] = None,
112
  image_seed: Optional[int] = None,
 
147
  "why_it_works": why_it_works,
148
  "username": username, # Store username for filtering
149
  "image_url": image_url,
150
+ "r2_url": r2_url,
151
  "image_filename": image_filename,
152
  "image_model": image_model,
153
  "image_seed": image_seed,
services/generator.py CHANGED
@@ -58,10 +58,9 @@ except ImportError:
58
  # Data module imports
59
  from data import home_insurance, glp1, auto_insurance
60
  from services.matrix import matrix_service
61
- from data.frameworks import get_frameworks_for_niche, get_framework_hook_examples, get_all_frameworks
62
- from data.containers import (
63
- get_random_container, get_container_visual_guidance, get_native_containers,
64
- get_ugc_containers, get_alert_containers, get_all_containers
65
  )
66
  from data.hooks import get_random_hook_style, get_power_words, get_random_cta as get_hook_cta
67
  from data.triggers import get_random_trigger, get_trigger_combination, get_triggers_for_niche
@@ -319,51 +318,8 @@ class AdGenerator:
319
  Get visual library categories for a niche.
320
  Returns dict mapping category names to visual descriptions.
321
  """
322
- if niche == "home_insurance":
323
- from data import home_insurance
324
- return {
325
- "protection_safety": home_insurance.PROTECTION_SAFETY_VISUALS,
326
- "disaster_fear": home_insurance.DISASTER_FEAR_VISUALS,
327
- "family_emotional": home_insurance.FAMILY_EMOTIONAL_VISUALS,
328
- "first_time_homebuyer": home_insurance.FIRST_TIME_HOMEBUYER_VISUALS,
329
- "asset_investment": home_insurance.ASSET_INVESTMENT_VISUALS,
330
- "problem_risk": home_insurance.PROBLEM_RISK_VISUALS,
331
- "relief": home_insurance.RELIEF_VISUALS,
332
- "mortgage_bank": home_insurance.MORTGAGE_BANK_VISUALS,
333
- "comparison_choice": home_insurance.COMPARISON_CHOICE_VISUALS,
334
- "minimal_symbolic": home_insurance.MINIMAL_SYMBOLIC_VISUALS,
335
- "lifestyle": home_insurance.LIFESTYLE_VISUALS,
336
- "text_first": home_insurance.TEXT_FIRST_VISUALS,
337
- "seasonal": home_insurance.SEASONAL_VISUALS,
338
- }
339
- elif niche == "glp1":
340
- from data import glp1
341
- return {
342
- "diet_fatigue": glp1.DIET_FATIGUE_VISUALS,
343
- "clothes_body_awareness": glp1.CLOTHES_BODY_AWARENESS_VISUALS,
344
- "health_wake_up_calls": glp1.HEALTH_WAKE_UP_CALLS_VISUALS,
345
- "mental_load_food_noise": glp1.MENTAL_LOAD_FOOD_NOISE_VISUALS,
346
- "survival_life_extension": glp1.SURVIVAL_LIFE_EXTENSION_VISUALS,
347
- "freedom_from_fear_pain": glp1.FREEDOM_FROM_FEAR_PAIN_VISUALS,
348
- "sexual_companionship": glp1.SEXUAL_COMPANIONSHIP_VISUALS,
349
- "comfortable_living": glp1.COMFORTABLE_LIVING_VISUALS,
350
- "superiority_winning": glp1.SUPERIORITY_WINNING_VISUALS,
351
- "care_protection_loved_ones": glp1.CARE_PROTECTION_LOVED_ONES_VISUALS,
352
- "social_approval": glp1.SOCIAL_APPROVAL_VISUALS,
353
- "why_moment": glp1.WHY_MOMENT_VISUALS,
354
- "when_moment": glp1.WHEN_MOMENT_VISUALS,
355
- "where_moment": glp1.WHERE_MOMENT_VISUALS,
356
- "with_whom": glp1.WITH_WHOM_VISUALS,
357
- "with_what_activity": glp1.WITH_WHAT_ACTIVITY_VISUALS,
358
- "while_wearing_using": glp1.WHILE_WEARING_USING_VISUALS,
359
- "while_feeling": glp1.WHILE_FEELING_VISUALS,
360
- "unaware": glp1.UNAWARE_VISUALS,
361
- "problem_aware": glp1.PROBLEM_AWARE_VISUALS,
362
- "solution_aware": glp1.SOLUTION_AWARE_VISUALS,
363
- "product_aware": glp1.PRODUCT_AWARE_VISUALS,
364
- "most_aware": glp1.MOST_AWARE_VISUALS,
365
- }
366
- return {}
367
 
368
  def _select_visuals_from_library(self, niche: str, strategy_name: str, count: int = 2) -> List[str]:
369
  """
@@ -447,81 +403,111 @@ class AdGenerator:
447
 
448
  def _get_niche_specific_guidance(self, niche: str) -> str:
449
  """Get niche-specific guidance for the prompt."""
450
- if niche == "home_insurance":
451
- return """
452
- NICHE-SPECIFIC REQUIREMENTS (HOME INSURANCE):
453
- - Focus on REALISTIC scenarios homeowners can identify with
454
- - Show real situations: storm damage, fire, flood, theft, protection
455
- - Use authentic emotions: fear of loss, relief of protection, family safety
456
- - Target pain points: high premiums, disaster fears, coverage gaps, savings desire
457
- - Messaging must feel URGENT but not fear-mongering
458
- - Visual concepts: real homes, real disasters, real families, real protection
459
- - AVOID: fantasy elements, castles, fortresses, unrealistic scenarios
460
- - AVOID: generic stock photo families, overly polished imagery
461
- """
462
- elif niche == "glp1":
463
- return """
464
- NICHE-SPECIFIC REQUIREMENTS (GLP-1 / WEIGHT LOSS):
465
- - Focus on TRANSFORMATION and emotional journey
466
- - Use VARIETY in visual concepts: quiz/interactive interfaces, medical/doctor settings, scale/measurement moments, mirror reflections, lifestyle/confidence scenes, testimonial portraits, celebrity references, before/after (only when strategy specifically calls for it), or other diverse visual types
467
- - Use authentic emotions: shame, hope, confidence, transformation, urgency, curiosity
468
- - Target pain points: failed diets, body image, social acceptance, health fears
469
- - Messaging must feel ASPIRATIONAL with urgency
470
- - Visual concepts should vary: quiz screens, medical authority, scale moments, lifestyle changes, confidence moments, testimonial style, or transformation proof (when appropriate)
471
- - AVOID: defaulting to before/after for every image - use diverse visual approaches
472
- - AVOID: unrealistic body standards, extreme before/after manipulation
473
- - AVOID: medical claims without proper framing, shame-based imagery only
474
- - Include elements of: medical authority, social proof, simplicity, variety
475
- """
476
- elif niche == "auto_insurance":
477
- return """
478
- NICHE-SPECIFIC REQUIREMENTS (AUTO INSURANCE):
479
- - Focus on REALISTIC driving scenarios and protection
480
- - Show real situations: safe driving, accidents, family safety, financial protection
481
- - Use authentic emotions: fear of accidents, relief of coverage, family responsibility, savings
482
- - Target pain points: high premiums, accident fears, coverage gaps, legal requirements
483
- - Messaging must feel URGENT but trustworthy
484
- - Visual concepts: real drivers, real cars, real protection scenarios, savings proof
485
- - AVOID: extreme accident scenes, fear-mongering, unrealistic scenarios
486
- - AVOID: generic stock photos, overly dramatic crash imagery
487
- """
488
- return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
 
490
  def _generate_specific_price(self, niche: str) -> str:
491
  """
492
  Generate price guidance for the AI.
493
  The AI will decide whether to include prices and what amounts to use.
494
- This method now returns guidance text rather than a hardcoded price.
495
  """
496
- if niche == "home_insurance":
497
- return "Consider using oddly specific prices (e.g., $97.33 instead of $100) if the ad format calls for it. Typical range: $37-$147/month. Only include if it enhances believability and fits the ad format."
498
- elif niche == "glp1":
499
- return "Consider using specific prices if relevant to the ad format. Typical range: $197-$497. Only include if it enhances the message and fits the ad strategy."
500
- elif niche == "auto_insurance":
501
- return "Consider using oddly specific prices (e.g., $29.00 or $67.33 instead of $30 or $70) if the ad format calls for it. Typical range: $29-$150/month. Only include if it enhances believability and fits the ad format."
502
- return "Use contextually appropriate prices if the ad format requires them. Make them oddly specific (not rounded) for believability."
503
 
504
  def _generate_niche_numbers(self, niche: str) -> Dict[str, str]:
505
- """Generate niche-specific numbers for authenticity."""
506
- if niche == "home_insurance":
507
- # Insurance savings numbers
508
- before = random.randint(1200, 2400)
509
- savings_pct = random.uniform(0.55, 0.75)
 
 
 
 
 
 
 
510
  after = int(before * (1 - savings_pct))
511
  return {
512
  "type": "savings",
513
  "before": f"${before:,}/year",
514
  "after": f"${after}/year",
515
  "difference": f"${before - after:,}",
516
- "metric": "savings per year",
517
  }
518
- elif niche == "glp1":
519
- # Weight loss numbers
520
- before_weight = random.randint(180, 280)
521
- lbs_lost = random.randint(25, 65)
 
 
 
522
  after_weight = before_weight - lbs_lost
523
- days = random.choice([60, 90, 120])
524
- sizes_dropped = random.randint(2, 5)
525
  return {
526
  "type": "weight_loss",
527
  "before": f"{before_weight} lbs",
@@ -529,100 +515,10 @@ NICHE-SPECIFIC REQUIREMENTS (AUTO INSURANCE):
529
  "difference": f"{lbs_lost} lbs",
530
  "days": f"{days} days",
531
  "sizes": f"{sizes_dropped} dress sizes",
532
- "metric": "pounds lost",
533
- }
534
- elif niche == "auto_insurance":
535
- # Auto insurance savings numbers
536
- before = random.randint(1200, 2400)
537
- savings_pct = random.uniform(0.50, 0.70)
538
- after = int(before * (1 - savings_pct))
539
- return {
540
- "type": "savings",
541
- "before": f"${before:,}/year",
542
- "after": f"${after}/year",
543
- "difference": f"${before - after:,}",
544
- "metric": "savings per year",
545
  }
546
  return {}
547
 
548
- def _get_framework_container_compatibility(self, framework_key: str) -> List[str]:
549
- """
550
- Get container types that are most compatible with a framework.
551
- Returns list of container keys ordered by compatibility.
552
- """
553
- compatibility_map = {
554
- "breaking_news": ["news_chyron", "system_notification", "push_notification", "browser_alert"],
555
- "testimonial": ["reddit_post", "social_post", "imessage", "whatsapp"],
556
- "before_after": ["social_post", "reddit_post", "sticky_note", "memo"],
557
- "problem_solution": ["imessage", "whatsapp", "sms", "email_notification"],
558
- "authority": ["email_notification", "memo", "browser_alert", "system_notification"],
559
- "lifestyle": ["social_post", "reddit_post", "imessage", "whatsapp"],
560
- "comparison": ["memo", "browser_alert", "email_notification", "standard"],
561
- "storytelling": ["social_post", "reddit_post", "imessage", "whatsapp"],
562
- "mobile_post": ["imessage", "whatsapp", "sms", "push_notification"],
563
- "educational": ["email_notification", "memo", "browser_alert", "standard"],
564
- }
565
-
566
- # Get compatible containers for this framework
567
- compatible = compatibility_map.get(framework_key, [])
568
-
569
- # Add all containers as fallback options
570
- all_containers = list(get_all_containers().keys())
571
-
572
- # Return compatible first, then others
573
- return compatible + [c for c in all_containers if c not in compatible]
574
-
575
- # ========================================================================
576
- # CONTAINER SELECTION METHODS
577
- # ========================================================================
578
-
579
- def _select_container(self, prefer_native: bool = True, strategy: str = "balanced", framework_key: Optional[str] = None) -> Dict[str, Any]:
580
- """
581
- Select a container type for native-looking ad format.
582
- Now includes framework-aware selection for better compatibility.
583
-
584
- Args:
585
- prefer_native: Prefer native containers (iMessage, WhatsApp, etc.)
586
- strategy: 'native', 'ugc', 'alert', 'balanced', 'all', or 'framework_aware'
587
- framework_key: Framework key for compatibility-based selection
588
- """
589
- from data.containers import get_container
590
-
591
- all_containers = get_all_containers()
592
- container_keys = list(all_containers.keys())
593
-
594
- # Framework-aware selection (new improvement)
595
- if strategy == "framework_aware" and framework_key:
596
- compatible_containers = self._get_framework_container_compatibility(framework_key)
597
- # 70% chance to use top compatible, 30% for variety
598
- if random.random() < 0.7 and compatible_containers:
599
- container_keys = compatible_containers[:4] # Top 4 compatible
600
- else:
601
- container_keys = compatible_containers # All compatible options
602
-
603
- elif strategy == "native":
604
- container_keys = get_native_containers()
605
- elif strategy == "ugc":
606
- container_keys = get_ugc_containers()
607
- elif strategy == "alert":
608
- container_keys = get_alert_containers()
609
- elif strategy == "balanced":
610
- # 50% native, 25% UGC, 25% alert/other
611
- rand = random.random()
612
- if rand < 0.5:
613
- container_keys = get_native_containers()
614
- elif rand < 0.75:
615
- container_keys = get_ugc_containers()
616
- else:
617
- container_keys = get_alert_containers()
618
- # else "all" - use all containers
619
-
620
- # Select random container from chosen set
621
- container_key = random.choice(container_keys) if container_keys else random.choice(list(all_containers.keys()))
622
- container = get_container(container_key)
623
-
624
- return container if container else get_random_container()
625
-
626
  # ========================================================================
627
  # PROMPT BUILDING METHODS
628
  # ========================================================================
@@ -637,6 +533,7 @@ NICHE-SPECIFIC REQUIREMENTS (AUTO INSURANCE):
637
  framework: str,
638
  framework_data: Dict[str, Any],
639
  framework_hooks: List[str],
 
640
  trigger_data: Dict[str, Any] = None,
641
  trigger_combination: Dict[str, Any] = None,
642
  power_words: List[str] = None,
@@ -653,19 +550,16 @@ NICHE-SPECIFIC REQUIREMENTS (AUTO INSURANCE):
653
  """
654
  strategy_names = [s["name"] for s in strategies]
655
  strategy_descriptions = [f"- {s['name']}: {s['description']}" for s in strategies]
656
- cta = random.choice(niche_data["ctas"])
657
  niche_guidance = self._get_niche_specific_guidance(niche)
658
 
659
- # Select container (native-looking format)
660
- # Use framework-aware container selection (improvement)
661
- container_strategy = "framework_aware" if random.random() < 0.6 else random.choice(["native", "ugc", "alert", "balanced"])
662
- container = self._select_container(prefer_native=True, strategy=container_strategy, framework_key=framework_data.get("key"))
663
  price_guidance = self._generate_specific_price(niche)
664
  niche_numbers = self._generate_niche_numbers(niche)
665
  age_bracket = random.choice(AGE_BRACKETS)
666
 
667
- # Build numbers section - AI decides whether to include prices/numbers
668
- if niche == "glp1":
 
669
  numbers_section = f"""=== NUMBERS GUIDANCE (WEIGHT LOSS) ===
670
  You may include specific numbers if they enhance the ad's believability and fit the format:
671
  - Starting Weight: {niche_numbers['before']}
@@ -699,8 +593,8 @@ DECISION: You decide whether to include prices/numbers based on:
699
  If including prices: Use oddly specific amounts (e.g., "$97.33/month" not "$100/month") for maximum believability.
700
  If NOT including prices: Focus on emotional benefits, problem-solution framing, curiosity gaps, and trust without specific dollar amounts."""
701
 
702
- # Headline formulas (updated to show both with and without numbers)
703
- if niche == "glp1":
704
  headline_formulas = """=== PROVEN WINNING HEADLINE FORMULAS (WEIGHT LOSS) ===
705
 
706
  WITH NUMBERS (use if numbers section provided):
@@ -819,13 +713,13 @@ CONCEPT: {concept.get('name') if concept else 'N/A'}
819
  {f'TARGET AUDIENCE: {target_audience}' if target_audience else ''}
820
  {f'OFFER: {offer}' if offer else ''}
821
 
822
- === CONTAINER FORMAT (Native-Looking Ad) ===
823
- CONTAINER TYPE: {container['name']}
824
- DESCRIPTION: {container.get('description', '')}
825
- VISUAL GUIDANCE: {get_container_visual_guidance(container.get('key', ''))}
826
- FONT STYLE: {container.get('font_style', '')}
827
- COLORS: {', '.join(f'{k}: {v}' for k, v in container.get('colors', {}).items())}
828
- AUTHENTICITY TIPS: {', '.join(container.get('authenticity_tips', [])[:3])}
829
 
830
  {numbers_section}
831
 
@@ -876,16 +770,16 @@ Create a SCROLL-STOPPING Facebook ad for {niche.replace("_", " ").upper()} using
876
  - You decide whether to include specific metrics/numbers based on what enhances the message. If including, use oddly specific amounts for believability.
877
  - Create action urgency
878
 
879
- 4. IMAGE BRIEF (CRITICAL - must match {container['name']} container style)
880
  - Follow the "{concept.get('name') if concept else 'visual'}" concept: {concept.get('structure') if concept else 'authentic visual'}
881
- - Describe the scene for the {container['name']} container format ONLY
882
- - Visual guidance: {get_container_visual_guidance(container.get('key', ''))}
883
  - The image should look like ORGANIC CONTENT, not an ad
884
  - Include: setting, subjects, props, mood
885
- - Follow container authenticity tips: {', '.join(container.get('authenticity_tips', [])[:2])}
886
- - CRITICAL: Use ONLY {container['name']} format - DO NOT mix with other container types (no WhatsApp + memo, no iMessage + document)
887
- - {f"If chat container: Include 2-4 readable, coherent messages related to {niche.replace('_', ' ').title()}. Use the headline or a variation as one message." if container.get('key') in ['imessage', 'whatsapp', 'sms', 'reddit_post', 'social_post'] else ""}
888
- - {f"If document container: Include readable, properly formatted text related to {niche.replace('_', ' ').title()}." if container.get('key') in ['memo', 'email_notification', 'browser_alert'] else ""}
889
  - FOR GLP-1: Use VARIETY - show different visual types: quiz interfaces, doctor/medical settings, person on scale, mirror reflections, lifestyle moments, confidence scenes, testimonial portraits, celebrity references, or before/after (only when strategy calls for it) use diverse visual concepts.
890
  - FOR HOME INSURANCE: Show person with document, savings proof, home setting
891
 
@@ -903,7 +797,7 @@ Create a SCROLL-STOPPING Facebook ad for {niche.replace("_", " ").upper()} using
903
  3. ALWAYS create curiosity gap with "THIS", "Instead", "After", "Secret"
904
  4. NEVER look like an ad - look like NEWS, PROOF, or UGC
905
  5. Use ACCUSATION framing for maximum impact
906
- 6. The image MUST match the {container['name']} container style
907
 
908
  === OUTPUT FORMAT (JSON) ===
909
  {{
@@ -911,10 +805,8 @@ Create a SCROLL-STOPPING Facebook ad for {niche.replace("_", " ").upper()} using
911
  "primary_text": "Your 2-3 sentence emotional amplification with specific numbers",
912
  "description": "Your one powerful sentence",
913
  "body_story": "A compelling 8-12 sentence STORY that hooks the reader emotionally. Start with a relatable situation or pain point. Build tension gradually. Show the transformation with vivid details. End with hope and a soft CTA. Write in first or second person for intimacy. Make it engaging and detailed enough to fully capture the reader's attention.",
914
- "image_brief": "Detailed description following '{concept.get('name') if concept else 'visual'}' concept and matching {container['name']} container style - organic content feel",
915
  "cta": "{cta}",
916
- "container_used": "{container['name']}",
917
- "container_key": "{container.get('key', '')}",
918
  "psychological_angle": "{angle.get('name') if angle else 'Primary psychological trigger being used'}",
919
  "why_it_works": "Brief explanation of the psychological mechanism"
920
  }}
@@ -944,16 +836,14 @@ Generate the ad copy now for {niche.replace("_", " ").upper()}. Make it look lik
944
  image_brief = ad_copy.get("image_brief", "")
945
  headline = ad_copy.get("headline", "")
946
  psychological_angle = ad_copy.get("psychological_angle", "")
947
- container_key = ad_copy.get("container_key", "")
948
- container_name = ad_copy.get("container_used", "Standard Ad")
 
949
  price_anchor = ad_copy.get("price_anchor", "$97")
950
 
951
- # Get container data if key is available
952
- if container_key:
953
- from data.containers import get_container
954
- container = get_container(container_key)
955
- else:
956
- container = {"name": container_name, "visual_guidance": "Standard ad format"}
957
 
958
  # Select visual style (use visuals.py data if available, otherwise strategy visuals)
959
  if visual_style_data and isinstance(visual_style_data, dict):
@@ -966,7 +856,7 @@ Generate the ad copy now for {niche.replace("_", " ").upper()}. Make it look lik
966
  # Randomly decide which elements to include (for variety)
967
  include_vintage_effects = random.random() < 0.7 # 70% chance
968
  include_text_overlay = random.random() < 0.8 # 80% chance (headline on image)
969
- include_container_format = random.random() < 0.4 # 40% chance (many images should be clean without container)
970
 
971
  # Select vintage film style and damage effects (only if including vintage)
972
  vintage_style = random.choice(VINTAGE_FILM_STYLES) if include_vintage_effects else None
@@ -995,7 +885,7 @@ Generate the ad copy now for {niche.replace("_", " ").upper()}. Make it look lik
995
  ]
996
  text_color = random.choice(text_colors)
997
 
998
- # Niche-specific image guidance (use visuals.py if available)
999
  if niche_visual_guidance_data and isinstance(niche_visual_guidance_data, dict):
1000
  niche_image_guidance = f"""
1001
  NICHE REQUIREMENTS ({niche.replace("_", " ").title()}):
@@ -1003,73 +893,46 @@ SUBJECTS: {', '.join(niche_visual_guidance_data.get('subjects', []))}
1003
  PROPS: {', '.join(niche_visual_guidance_data.get('props', []))}
1004
  AVOID: {', '.join(niche_visual_guidance_data.get('avoid', []))}
1005
  COLOR PREFERENCE: {niche_visual_guidance_data.get('color_preference', 'balanced')}
1006
- """
1007
- elif niche == "home_insurance":
1008
- niche_image_guidance = """
1009
- NICHE REQUIREMENTS:
1010
- - Show REAL American suburban homes (single-family, realistic architecture)
1011
- - Include authentic elements: lawns, driveways, neighborhoods
1012
- - People should look like real homeowners (diverse, relatable, 30-60 age range)
1013
- - Disaster scenes should be realistic but not gratuitous
1014
- - Protection/safety imagery should feel reassuring, not corporate
1015
- - AVOID: mansions, castles, fantasy homes, unrealistic scenarios
1016
- """
1017
- elif niche == "glp1":
1018
- niche_image_guidance = """
1019
- NICHE REQUIREMENTS (GLP-1):
1020
- - Use VARIETY in visual types
1021
- - Visual options include: quiz/interactive interfaces, doctor/medical settings, person on scale, mirror reflections, lifestyle/confidence scenes, testimonial portraits, celebrity references, measurement moments, or before/after (only when the strategy specifically requires it)
1022
- - Show REAL people in various moments (not just transformation)
1023
- - Body types should be relatable (not fitness models)
1024
- - Include authentic lifestyle elements: real clothes, real settings
1025
- - Medical elements should look professional and trustworthy
1026
- - Confidence/transformation moments should feel genuine
1027
- - AVOID: defaulting to before/after for every image - prioritize visual variety
1028
- - AVOID: extreme body manipulation, unrealistic transformations, shame-inducing imagery
1029
- - AVOID: elderly people over 65, senior citizens, very old looking people, gray-haired elderly groups
1030
- - ENCOURAGE: diverse visual concepts that match the strategy (quiz for quiz strategy, medical for authority strategy, lifestyle for aspiration, etc.)
1031
- - AGE GUIDANCE: Show people aged 30-50 primarily. DO NOT default to elderly/senior citizens. Target audience is middle-aged adults (30s-40s), NOT seniors.
1032
  """
1033
  else:
1034
- niche_image_guidance = ""
 
1035
 
1036
- # Container-specific visual guidance (from containers.py)
1037
- # CRITICAL: Use ONLY ONE container type, ensure readable text
1038
- container_visual_guidance = get_container_visual_guidance(container_key) if container_key else container.get("visual_guidance", "")
1039
 
1040
- # Determine if this is a chat/message container that needs readable text
1041
- is_chat_container = container_key in ["imessage", "whatsapp", "sms", "reddit_post", "social_post"]
1042
- is_document_container = container_key in ["memo", "email_notification", "browser_alert"]
1043
 
1044
- container_guidance_section = f"""
1045
- === CONTAINER FORMAT REQUIREMENTS ===
1046
- CRITICAL: Use ONLY the {container.get('name', 'Standard Ad')} container format. DO NOT mix multiple container types.
1047
 
1048
- Container Type: {container.get('name', 'Standard Ad')}
1049
- Visual Guidance: {container_visual_guidance}
1050
 
1051
  REQUIREMENTS:
1052
- 1. USE ONLY THIS CONTAINER TYPE - NO other containers (no mixing WhatsApp + memo, no mixing iMessage + document)
1053
- 2. NO decorative borders, frames, or boxes around the container
1054
  3. NO banners, badges, or logos
1055
  4. NO overlay boxes or rectangular overlays
1056
- 5. Focus on authentic, natural appearance of the {container.get('name', 'Standard Ad')} format only
1057
-
1058
- {"=== TEXT REQUIREMENTS FOR CHAT CONTAINERS ===" if is_chat_container else ""}
1059
- {"CRITICAL: All text in chat bubbles MUST be:" if is_chat_container else ""}
1060
- {"- READABLE and COHERENT (not gibberish, not placeholder text)" if is_chat_container else ""}
1061
- {f"- Realistic conversation text related to {niche.replace('_', ' ').title()}" if is_chat_container else ""}
1062
- {"- Proper spelling and grammar" if is_chat_container else ""}
1063
- {"- Natural message flow (2-4 messages max)" if is_chat_container else ""}
1064
- {"- Use the headline or a variation as one of the messages" if is_chat_container else ""}
1065
- {"- NO placeholder text like 'lorem ipsum' or random characters" if is_chat_container else ""}
1066
-
1067
- {"=== TEXT REQUIREMENTS FOR DOCUMENT CONTAINERS ===" if is_document_container else ""}
1068
- {"CRITICAL: All text in documents MUST be:" if is_document_container else ""}
1069
- {"- READABLE and COHERENT" if is_document_container else ""}
1070
- {f"- Related to {niche.replace('_', ' ').title()} topic" if is_document_container else ""}
1071
- {"- Proper formatting (title, body text, etc.)" if is_document_container else ""}
1072
- {"- NO gibberish or placeholder text" if is_document_container else ""}
1073
  """
1074
 
1075
  # Build flexible prompt based on what to include
@@ -1083,30 +946,29 @@ REQUIREMENTS:
1083
  - Visible grain throughout
1084
  """
1085
 
1086
- container_section = ""
1087
- if include_container_format:
1088
- # Only 40% of images will use container format
1089
- # CRITICAL: Use ONLY the specified container, ensure readable text
1090
- container_section = f"""
1091
- {container_guidance_section}
1092
 
1093
  CRITICAL REMINDERS:
1094
- - Use ONLY {container.get('name', 'Standard Ad')} format - NO mixing with other containers
1095
- - If using chat container: All text MUST be readable, coherent, and related to the {niche.replace('_', ' ').title()} niche
1096
- - If using document container: All text MUST be readable and properly formatted
1097
  - NO gibberish, placeholder text, or random characters
1098
  - NO decorative borders, frames, or boxes
1099
  """
1100
  else:
1101
- # 60% of images will be clean, natural images without container format
1102
- container_section = """
1103
- === STYLE GUIDANCE (NO CONTAINER FORMAT) ===
1104
- - Natural, authentic image - NO container format
1105
  - Must NOT look like a polished advertisement
1106
  - Should feel like authentic, organic content
1107
  - Real, unpolished, natural appearance
1108
  - NO decorative borders, banners, overlays, or boxes
1109
- - NO native app interfaces, screenshots, or container styles
1110
  - Just a clean, natural photograph or scene
1111
  """
1112
 
@@ -1140,7 +1002,7 @@ NO text overlays, decorative elements, borders, banners, or overlays.
1140
  prompt = f"""Create a Facebook advertisement image that looks like AUTHENTIC, ORGANIC CONTENT.
1141
 
1142
  {vintage_section}
1143
- {container_section}
1144
  {text_overlay_section}
1145
 
1146
  === VISUAL SCENE ===
@@ -1238,10 +1100,10 @@ DOCUMENTS (if present):
1238
  - NO numbers, prices, dollar amounts, or savings figures displayed prominently
1239
  - NO text overlays with numerical information
1240
  - Focus on the natural scene only, no added presentation elements or numbers
1241
- - NO mixing multiple container types (e.g., NO WhatsApp + memo, NO iMessage + document)
1242
  - NO gibberish, placeholder text, or random characters in chat bubbles or documents
1243
  - NO "lorem ipsum", placeholder text, or meaningless character strings
1244
- - If using a container format, use ONLY that one container type - NO mixing
1245
  - NO DUPLICATE TEXT - do not show the same text/message in multiple places
1246
  - NO repeating headlines or prices in different formats or locations
1247
  - Text should appear ONCE only, not multiple times in the image
@@ -1296,35 +1158,12 @@ CRITICAL REQUIREMENTS:
1296
  prompt = re.sub(pattern, '', prompt, flags=re.IGNORECASE)
1297
 
1298
  # =====================================================================
1299
- # 2. FIX DEMOGRAPHIC ISSUES (niche-specific)
1300
  # =====================================================================
1301
  prompt_lower = prompt.lower()
1302
-
1303
- # GLP-1: Fix elderly/senior defaults - target is 30-50 adults
1304
- if niche and niche.lower() == 'glp1':
1305
- # Replace elderly references with middle-aged
1306
- elderly_replacements = [
1307
- (r'\b(elderly|senior|seniors|old people|old person)\b', 'middle-aged adult'),
1308
- (r'\b(grandparent|grandfather|grandmother|grandma|grandpa)\b', 'parent'),
1309
- (r'\b(70[\s-]?year[\s-]?old|80[\s-]?year[\s-]?old|65[\s-]?year[\s-]?old)\b', '40-year-old'),
1310
- (r'\b(in their 60s|in their 70s|in their 80s)\b', 'in their 40s'),
1311
- (r'\b(retirement age|retired person|retiree)\b', 'working professional'),
1312
- (r'\bgray[\s-]?haired elderly\b', 'confident adult'),
1313
- (r'\bsenior citizen\b', 'adult'),
1314
- ]
1315
- for pattern, replacement in elderly_replacements:
1316
- prompt = re.sub(pattern, replacement, prompt, flags=re.IGNORECASE)
1317
-
1318
- # Home Insurance: Ensure American suburban context
1319
- if niche and niche.lower() == 'home_insurance':
1320
- # Fix non-American home references
1321
- home_replacements = [
1322
- (r'\b(flat|apartment|condo)\b(?! insurance)', 'house'),
1323
- (r'\b(mansion|castle|estate)\b', 'suburban home'),
1324
- (r'\b(european|british|uk) style home\b', 'American suburban home'),
1325
- ]
1326
- for pattern, replacement in home_replacements:
1327
- prompt = re.sub(pattern, replacement, prompt, flags=re.IGNORECASE)
1328
 
1329
  # =====================================================================
1330
  # 3. FIX ILLOGICAL VISUAL COMBINATIONS
@@ -1525,8 +1364,10 @@ CRITICAL REQUIREMENTS:
1525
  creative_direction = random.choice(niche_data["creative_directions"])
1526
  visual_mood = random.choice(niche_data["visual_moods"])
1527
 
1528
- # Framework already selected above for compatibility scoring
1529
- framework_hooks = get_framework_hook_examples(framework_key, niche)
 
 
1530
 
1531
  # Use visual elements from visuals.py (instead of hardcoded)
1532
  visual_style_data = get_random_visual_style()
@@ -1595,6 +1436,7 @@ CRITICAL REQUIREMENTS:
1595
  framework=framework,
1596
  framework_data=framework_data,
1597
  framework_hooks=framework_hooks,
 
1598
  trigger_data=trigger_data,
1599
  trigger_combination=trigger_combination,
1600
  power_words=power_words,
@@ -1610,6 +1452,12 @@ CRITICAL REQUIREMENTS:
1610
  temperature=0.95, # High for variety
1611
  )
1612
 
 
 
 
 
 
 
1613
  # Generate image(s) with professional prompt - PARALLELIZED
1614
  async def generate_single_image(image_index: int):
1615
  """Helper function to generate a single image with all processing."""
@@ -1743,6 +1591,7 @@ CRITICAL REQUIREMENTS:
1743
  why_it_works=ad_copy.get("why_it_works", ""),
1744
  username=username, # Pass username
1745
  image_url=image.get("image_url"),
 
1746
  image_filename=image.get("filename"),
1747
  image_model=image.get("model_used"),
1748
  image_seed=image.get("seed"),
@@ -1907,6 +1756,8 @@ CONCEPT: {concept.get('name', 'Custom Concept')}
1907
 
1908
  # Get niche data
1909
  niche_data = NICHE_DATA.get(niche, home_insurance.get_niche_data)()
 
 
1910
 
1911
  # Build specialized prompt using angle + concept
1912
  ad_copy_prompt = self._build_matrix_ad_prompt(
@@ -1914,6 +1765,7 @@ CONCEPT: {concept.get('name', 'Custom Concept')}
1914
  angle=angle,
1915
  concept=concept,
1916
  niche_data=niche_data,
 
1917
  core_motivator=core_motivator,
1918
  target_audience=target_audience,
1919
  offer=offer,
@@ -2068,6 +1920,7 @@ CONCEPT: {concept.get('name', 'Custom Concept')}
2068
  why_it_works=ad_copy.get("why_it_works", ""),
2069
  username=username, # Pass username
2070
  image_url=image.get("image_url"),
 
2071
  image_filename=image.get("filename"),
2072
  image_model=image.get("model_used"),
2073
  image_seed=image.get("seed"),
@@ -2382,6 +2235,7 @@ CONCEPT: {concept.get('name', 'Custom Concept')}
2382
  why_it_works=f"Angle: {strategy.angle}, Concept: {strategy.concept}",
2383
  username=username, # Pass username
2384
  image_url=image.get("image_url"),
 
2385
  image_filename=image.get("filename"),
2386
  image_model=image.get("model_used"),
2387
  image_prompt=image.get("image_prompt"),
@@ -2591,29 +2445,27 @@ Return JSON:
2591
  angle: Dict[str, Any],
2592
  concept: Dict[str, Any],
2593
  niche_data: Dict[str, Any],
 
2594
  core_motivator: Optional[str] = None,
2595
  target_audience: Optional[str] = None,
2596
  offer: Optional[str] = None,
2597
  ) -> str:
2598
  """Build ad copy prompt using angle + concept framework."""
2599
 
2600
- cta = random.choice(niche_data.get("ctas", ["Learn More"]))
2601
-
2602
  # AI will decide whether to include numbers based on ad format and strategy
2603
  # Always provide guidance, AI decides usage
2604
 
2605
- # Get niche-specific numbers guidance (AI decides if/when to use)
2606
- if True: # Always provide guidance
2607
- if niche == "glp1":
2608
- numbers = self._generate_niche_numbers(niche)
2609
- numbers_section = f"""SPECIFIC NUMBERS TO USE:
2610
  - Weight Lost: {numbers['difference']}
2611
  - Timeframe: {numbers['days']}
2612
  - Starting: {numbers['before']}, Current: {numbers['after']}"""
2613
- else:
2614
- numbers = self._generate_niche_numbers(niche)
2615
- price_guidance = self._generate_specific_price(niche)
2616
- numbers_section = f"""NUMBERS GUIDANCE (you decide if/when to use):
2617
  - Price Guidance: {price_guidance}
2618
  - Saved: {numbers['difference']}/year
2619
  - Before: {numbers['before']}, After: {numbers['after']}
@@ -2710,19 +2562,9 @@ Generate the ad now. Be bold, be specific, trigger {angle.get('trigger')}."""
2710
  ]
2711
  text_style = random.choice(text_styles)
2712
 
2713
- # Get niche-specific guidance
2714
- if niche == "home_insurance":
2715
- niche_guidance = """
2716
- NICHE: Home Insurance
2717
- - Show real American homes, suburban settings
2718
- - People should be diverse, relatable homeowners (30-60)
2719
- - Disaster scenes should be realistic but not gratuitous"""
2720
- else:
2721
- niche_guidance = """
2722
- NICHE: GLP-1 / Weight Loss
2723
- - Show real transformation moments
2724
- - People should be relatable, not fitness models
2725
- - Confidence and lifestyle improvement focus"""
2726
 
2727
  if use_motivator:
2728
  text_section = f'''=== MOTIVATOR PHRASE (blend naturally into the scene) ===
 
58
  # Data module imports
59
  from data import home_insurance, glp1, auto_insurance
60
  from services.matrix import matrix_service
61
+ from data.frameworks import (
62
+ get_frameworks_for_niche, get_framework_hook_examples, get_all_frameworks,
63
+ get_framework, get_framework_visual_guidance,
 
64
  )
65
  from data.hooks import get_random_hook_style, get_power_words, get_random_cta as get_hook_cta
66
  from data.triggers import get_random_trigger, get_trigger_combination, get_triggers_for_niche
 
318
  Get visual library categories for a niche.
319
  Returns dict mapping category names to visual descriptions.
320
  """
321
+ niche_data = self._get_niche_data(niche)
322
+ return niche_data.get("visual_library", {})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
  def _select_visuals_from_library(self, niche: str, strategy_name: str, count: int = 2) -> List[str]:
325
  """
 
403
 
404
  def _get_niche_specific_guidance(self, niche: str) -> str:
405
  """Get niche-specific guidance for the prompt."""
406
+ niche_data = self._get_niche_data(niche)
407
+ return niche_data.get("niche_guidance", "")
408
+
409
+ async def _get_framework_hook_examples_async(self, framework_key: str, niche: Optional[str] = None) -> List[str]:
410
+ """
411
+ Get hook examples for a framework. Uses AI generation when enabled, else static from frameworks.py.
412
+ Falls back to static examples on AI failure or when use_ai_generated_hooks is False.
413
+ """
414
+ if not getattr(settings, "use_ai_generated_hooks", False):
415
+ return get_framework_hook_examples(framework_key, niche)
416
+ framework = get_framework(framework_key)
417
+ if not framework:
418
+ return get_framework_hook_examples(framework_key, niche)
419
+ niche_label = (niche or "").replace("_", " ").title() or "general advertising"
420
+ prompt = f"""Generate 6 to 8 short ad hook examples (headline-style phrases) for this ad framework.
421
+
422
+ Framework: {framework.get('name', framework_key)}
423
+ Description: {framework.get('description', '')}
424
+ Tone: {framework.get('tone', '')}
425
+ Headline style: {framework.get('headline_style', '')}
426
+ Niche/context: {niche_label}
427
+
428
+ Rules:
429
+ - Each hook must be one short phrase or sentence (under 12 words).
430
+ - Match the framework's tone and style.
431
+ - Make them punchy and scroll-stopping; no generic filler.
432
+ - Return ONLY a JSON object with one key "hooks" containing an array of strings. No other text."""
433
+
434
+ try:
435
+ result = await llm_service.generate_json(prompt=prompt, temperature=0.8)
436
+ hooks = result.get("hooks") if isinstance(result, dict) else None
437
+ if isinstance(hooks, list) and len(hooks) > 0:
438
+ return [str(h).strip() for h in hooks if h]
439
+ except Exception:
440
+ pass
441
+ return get_framework_hook_examples(framework_key, niche)
442
+
443
+ async def _generate_ctas_async(
444
+ self, niche: str, framework_name: Optional[str] = None
445
+ ) -> List[str]:
446
+ """
447
+ Generate 5–8 CTAs for the niche (and optional framework) via LLM.
448
+ Returns empty list on failure; caller uses single default when empty.
449
+ """
450
+ niche_label = (niche or "").replace("_", " ").title() or "general advertising"
451
+ context = f"Niche: {niche_label}"
452
+ if framework_name:
453
+ context += f". Ad format/framework: {framework_name}"
454
+ prompt = f"""Generate 5 to 8 short call-to-action (CTA) button/link phrases for a paid ad.
455
+
456
+ {context}
457
+
458
+ Rules:
459
+ - Match the niche and tone; avoid generic-only.
460
+ - Return ONLY a JSON object with one key "ctas" containing an array of strings. No other text."""
461
+
462
+ try:
463
+ result = await llm_service.generate_json(prompt=prompt, temperature=0.7)
464
+ ctas = result.get("ctas") if isinstance(result, dict) else None
465
+ if isinstance(ctas, list) and len(ctas) > 0:
466
+ return [str(c).strip() for c in ctas if c]
467
+ except Exception:
468
+ pass
469
+ return []
470
 
471
  def _generate_specific_price(self, niche: str) -> str:
472
  """
473
  Generate price guidance for the AI.
474
  The AI will decide whether to include prices and what amounts to use.
 
475
  """
476
+ niche_data = self._get_niche_data(niche)
477
+ default = "Use contextually appropriate prices if the ad format requires them. Make them oddly specific (not rounded) for believability."
478
+ return niche_data.get("price_config", {}).get("guidance", default)
 
 
 
 
479
 
480
  def _generate_niche_numbers(self, niche: str) -> Dict[str, str]:
481
+ """Generate niche-specific numbers for authenticity from niche number_config."""
482
+ niche_data = self._get_niche_data(niche)
483
+ config = niche_data.get("number_config", {})
484
+ if not config:
485
+ return {}
486
+ num_type = config.get("type", "savings")
487
+ labels = config.get("labels", {})
488
+ if num_type == "savings":
489
+ before_range = config.get("before_range", [1200, 2400])
490
+ savings_range = config.get("savings_pct_range", [0.50, 0.75])
491
+ before = random.randint(before_range[0], before_range[1])
492
+ savings_pct = random.uniform(savings_range[0], savings_range[1])
493
  after = int(before * (1 - savings_pct))
494
  return {
495
  "type": "savings",
496
  "before": f"${before:,}/year",
497
  "after": f"${after}/year",
498
  "difference": f"${before - after:,}",
499
+ "metric": labels.get("metric", "savings per year"),
500
  }
501
+ if num_type == "weight_loss":
502
+ before_range = config.get("before_range", [180, 280])
503
+ loss_range = config.get("loss_range", [25, 65])
504
+ days_options = config.get("days_options", [60, 90, 120])
505
+ sizes_range = config.get("sizes_range", [2, 5])
506
+ before_weight = random.randint(before_range[0], before_range[1])
507
+ lbs_lost = random.randint(loss_range[0], loss_range[1])
508
  after_weight = before_weight - lbs_lost
509
+ days = random.choice(days_options)
510
+ sizes_dropped = random.randint(sizes_range[0], sizes_range[1])
511
  return {
512
  "type": "weight_loss",
513
  "before": f"{before_weight} lbs",
 
515
  "difference": f"{lbs_lost} lbs",
516
  "days": f"{days} days",
517
  "sizes": f"{sizes_dropped} dress sizes",
518
+ "metric": labels.get("metric", "pounds lost"),
 
 
 
 
 
 
 
 
 
 
 
 
519
  }
520
  return {}
521
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
  # ========================================================================
523
  # PROMPT BUILDING METHODS
524
  # ========================================================================
 
533
  framework: str,
534
  framework_data: Dict[str, Any],
535
  framework_hooks: List[str],
536
+ cta: str,
537
  trigger_data: Dict[str, Any] = None,
538
  trigger_combination: Dict[str, Any] = None,
539
  power_words: List[str] = None,
 
550
  """
551
  strategy_names = [s["name"] for s in strategies]
552
  strategy_descriptions = [f"- {s['name']}: {s['description']}" for s in strategies]
 
553
  niche_guidance = self._get_niche_specific_guidance(niche)
554
 
555
+ # Same framework drives ad idea, copy angle, AND visual format (no separate container)
 
 
 
556
  price_guidance = self._generate_specific_price(niche)
557
  niche_numbers = self._generate_niche_numbers(niche)
558
  age_bracket = random.choice(AGE_BRACKETS)
559
 
560
+ # Build numbers section from niche number_config type (data-driven)
561
+ num_type = niche_data.get("number_config", {}).get("type", "savings")
562
+ if num_type == "weight_loss":
563
  numbers_section = f"""=== NUMBERS GUIDANCE (WEIGHT LOSS) ===
564
  You may include specific numbers if they enhance the ad's believability and fit the format:
565
  - Starting Weight: {niche_numbers['before']}
 
593
  If including prices: Use oddly specific amounts (e.g., "$97.33/month" not "$100/month") for maximum believability.
594
  If NOT including prices: Focus on emotional benefits, problem-solution framing, curiosity gaps, and trust without specific dollar amounts."""
595
 
596
+ # Headline formulas from number_config type (data-driven)
597
+ if num_type == "weight_loss":
598
  headline_formulas = """=== PROVEN WINNING HEADLINE FORMULAS (WEIGHT LOSS) ===
599
 
600
  WITH NUMBERS (use if numbers section provided):
 
713
  {f'TARGET AUDIENCE: {target_audience}' if target_audience else ''}
714
  {f'OFFER: {offer}' if offer else ''}
715
 
716
+ === FRAMEWORK VISUAL FORMAT (same framework for copy + image) ===
717
+ FRAMEWORK: {framework_data.get('name', framework)}
718
+ DESCRIPTION: {framework_data.get('description', '')}
719
+ VISUAL GUIDANCE: {get_framework_visual_guidance(framework_data.get('key', ''))}
720
+ FONT STYLE: {framework_data.get('font_style', '')}
721
+ COLORS: {', '.join(f'{k}: {v}' for k, v in framework_data.get('colors', {}).items())}
722
+ AUTHENTICITY TIPS: {', '.join(framework_data.get('authenticity_tips', [])[:3])}
723
 
724
  {numbers_section}
725
 
 
770
  - You decide whether to include specific metrics/numbers based on what enhances the message. If including, use oddly specific amounts for believability.
771
  - Create action urgency
772
 
773
+ 4. IMAGE BRIEF (CRITICAL - must match {framework_data.get('name', framework)} framework style)
774
  - Follow the "{concept.get('name') if concept else 'visual'}" concept: {concept.get('structure') if concept else 'authentic visual'}
775
+ - Describe the scene for the {framework_data.get('name', framework)} framework ONLY
776
+ - Visual guidance: {get_framework_visual_guidance(framework_data.get('key', ''))}
777
  - The image should look like ORGANIC CONTENT, not an ad
778
  - Include: setting, subjects, props, mood
779
+ - Follow framework authenticity tips: {', '.join(framework_data.get('authenticity_tips', [])[:2])}
780
+ - CRITICAL: Use ONLY {framework_data.get('name', framework)} format - DO NOT mix with other formats
781
+ - {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 ""}
782
+ - {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 ""}
783
  - FOR GLP-1: Use VARIETY - show different visual types: quiz interfaces, doctor/medical settings, person on scale, mirror reflections, lifestyle moments, confidence scenes, testimonial portraits, celebrity references, or before/after (only when strategy calls for it) use diverse visual concepts.
784
  - FOR HOME INSURANCE: Show person with document, savings proof, home setting
785
 
 
797
  3. ALWAYS create curiosity gap with "THIS", "Instead", "After", "Secret"
798
  4. NEVER look like an ad - look like NEWS, PROOF, or UGC
799
  5. Use ACCUSATION framing for maximum impact
800
+ 6. The image MUST match the {framework_data.get('name', framework)} framework style
801
 
802
  === OUTPUT FORMAT (JSON) ===
803
  {{
 
805
  "primary_text": "Your 2-3 sentence emotional amplification with specific numbers",
806
  "description": "Your one powerful sentence",
807
  "body_story": "A compelling 8-12 sentence STORY that hooks the reader emotionally. Start with a relatable situation or pain point. Build tension gradually. Show the transformation with vivid details. End with hope and a soft CTA. Write in first or second person for intimacy. Make it engaging and detailed enough to fully capture the reader's attention.",
808
+ "image_brief": "Detailed description following '{concept.get('name') if concept else 'visual'}' concept and matching {framework_data.get('name', framework)} framework style - organic content feel",
809
  "cta": "{cta}",
 
 
810
  "psychological_angle": "{angle.get('name') if angle else 'Primary psychological trigger being used'}",
811
  "why_it_works": "Brief explanation of the psychological mechanism"
812
  }}
 
836
  image_brief = ad_copy.get("image_brief", "")
837
  headline = ad_copy.get("headline", "")
838
  psychological_angle = ad_copy.get("psychological_angle", "")
839
+ # Same framework used for copy is used for visual (no separate container)
840
+ framework_key = ad_copy.get("framework_key") or ad_copy.get("container_key", "")
841
+ framework_name = ad_copy.get("framework") or ad_copy.get("container_used", "Standard Ad")
842
  price_anchor = ad_copy.get("price_anchor", "$97")
843
 
844
+ framework_data_img = get_framework(framework_key) if framework_key else None
845
+ if not framework_data_img:
846
+ framework_data_img = {"name": framework_name, "visual_style": "Standard ad format"}
 
 
 
847
 
848
  # Select visual style (use visuals.py data if available, otherwise strategy visuals)
849
  if visual_style_data and isinstance(visual_style_data, dict):
 
856
  # Randomly decide which elements to include (for variety)
857
  include_vintage_effects = random.random() < 0.7 # 70% chance
858
  include_text_overlay = random.random() < 0.8 # 80% chance (headline on image)
859
+ include_framework_format = random.random() < 0.4 # 40% chance (many images clean without framework UI style)
860
 
861
  # Select vintage film style and damage effects (only if including vintage)
862
  vintage_style = random.choice(VINTAGE_FILM_STYLES) if include_vintage_effects else None
 
885
  ]
886
  text_color = random.choice(text_colors)
887
 
888
+ # Niche-specific image guidance (use visuals.py if available, else niche data)
889
  if niche_visual_guidance_data and isinstance(niche_visual_guidance_data, dict):
890
  niche_image_guidance = f"""
891
  NICHE REQUIREMENTS ({niche.replace("_", " ").title()}):
 
893
  PROPS: {', '.join(niche_visual_guidance_data.get('props', []))}
894
  AVOID: {', '.join(niche_visual_guidance_data.get('avoid', []))}
895
  COLOR PREFERENCE: {niche_visual_guidance_data.get('color_preference', 'balanced')}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
896
  """
897
  else:
898
+ niche_data = self._get_niche_data(niche)
899
+ niche_image_guidance = niche_data.get("image_guidance", "")
900
 
901
+ # Framework visual guidance (same framework as copy)
902
+ 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", ""))
 
903
 
904
+ is_chat_style = "chat_style" in framework_data_img.get("tags", [])
905
+ is_document_style = "document_style" in framework_data_img.get("tags", [])
 
906
 
907
+ framework_format_section = f"""
908
+ === FRAMEWORK FORMAT REQUIREMENTS ===
909
+ CRITICAL: Use ONLY the {framework_data_img.get('name', 'Standard Ad')} framework style. DO NOT mix multiple formats.
910
 
911
+ Framework: {framework_data_img.get('name', 'Standard Ad')}
912
+ Visual Guidance: {framework_visual_guidance}
913
 
914
  REQUIREMENTS:
915
+ 1. USE ONLY THIS FRAMEWORK STYLE - NO mixing (e.g. no WhatsApp + memo, no iMessage + document)
916
+ 2. NO decorative borders, frames, or boxes
917
  3. NO banners, badges, or logos
918
  4. NO overlay boxes or rectangular overlays
919
+ 5. Focus on authentic, natural appearance of the {framework_data_img.get('name', 'Standard Ad')} format only
920
+
921
+ {"=== TEXT REQUIREMENTS FOR CHAT-STYLE FRAMEWORKS ===" if is_chat_style else ""}
922
+ {"CRITICAL: All text in chat bubbles MUST be:" if is_chat_style else ""}
923
+ {"- READABLE and COHERENT (not gibberish, not placeholder text)" if is_chat_style else ""}
924
+ {f"- Realistic conversation text related to {niche.replace('_', ' ').title()}" if is_chat_style else ""}
925
+ {"- Proper spelling and grammar" if is_chat_style else ""}
926
+ {"- Natural message flow (2-4 messages max)" if is_chat_style else ""}
927
+ {"- Use the headline or a variation as one of the messages" if is_chat_style else ""}
928
+ {"- NO placeholder text like 'lorem ipsum' or random characters" if is_chat_style else ""}
929
+
930
+ {"=== TEXT REQUIREMENTS FOR DOCUMENT-STYLE FRAMEWORKS ===" if is_document_style else ""}
931
+ {"CRITICAL: All text in documents MUST be:" if is_document_style else ""}
932
+ {"- READABLE and COHERENT" if is_document_style else ""}
933
+ {f"- Related to {niche.replace('_', ' ').title()} topic" if is_document_style else ""}
934
+ {"- Proper formatting (title, body text, etc.)" if is_document_style else ""}
935
+ {"- NO gibberish or placeholder text" if is_document_style else ""}
936
  """
937
 
938
  # Build flexible prompt based on what to include
 
946
  - Visible grain throughout
947
  """
948
 
949
+ framework_section = ""
950
+ if include_framework_format:
951
+ # 40% of images use the same framework's visual style (e.g. iMessage, Reddit)
952
+ framework_section = f"""
953
+ {framework_format_section}
 
954
 
955
  CRITICAL REMINDERS:
956
+ - Use ONLY {framework_data_img.get('name', 'Standard Ad')} format - NO mixing with other formats
957
+ - If chat-style framework: All text MUST be readable, coherent, and related to the {niche.replace('_', ' ').title()} niche
958
+ - If document-style framework: All text MUST be readable and properly formatted
959
  - NO gibberish, placeholder text, or random characters
960
  - NO decorative borders, frames, or boxes
961
  """
962
  else:
963
+ # 60% of images: clean, natural (no app/screenshot style)
964
+ framework_section = """
965
+ === STYLE GUIDANCE (NO FRAMEWORK UI STYLE) ===
966
+ - Natural, authentic image - no app/screenshot style
967
  - Must NOT look like a polished advertisement
968
  - Should feel like authentic, organic content
969
  - Real, unpolished, natural appearance
970
  - NO decorative borders, banners, overlays, or boxes
971
+ - NO native app interfaces or screenshot-style frames
972
  - Just a clean, natural photograph or scene
973
  """
974
 
 
1002
  prompt = f"""Create a Facebook advertisement image that looks like AUTHENTIC, ORGANIC CONTENT.
1003
 
1004
  {vintage_section}
1005
+ {framework_section}
1006
  {text_overlay_section}
1007
 
1008
  === VISUAL SCENE ===
 
1100
  - NO numbers, prices, dollar amounts, or savings figures displayed prominently
1101
  - NO text overlays with numerical information
1102
  - Focus on the natural scene only, no added presentation elements or numbers
1103
+ - NO mixing multiple framework formats (e.g., NO WhatsApp + memo, NO iMessage + document)
1104
  - NO gibberish, placeholder text, or random characters in chat bubbles or documents
1105
  - NO "lorem ipsum", placeholder text, or meaningless character strings
1106
+ - If using a framework format (e.g. iMessage, memo), use ONLY that one format - NO mixing
1107
  - NO DUPLICATE TEXT - do not show the same text/message in multiple places
1108
  - NO repeating headlines or prices in different formats or locations
1109
  - Text should appear ONCE only, not multiple times in the image
 
1158
  prompt = re.sub(pattern, '', prompt, flags=re.IGNORECASE)
1159
 
1160
  # =====================================================================
1161
+ # 2. FIX DEMOGRAPHIC ISSUES (niche-specific from niche data)
1162
  # =====================================================================
1163
  prompt_lower = prompt.lower()
1164
+ niche_data_sanitize = self._get_niche_data(niche) if niche else {}
1165
+ for pattern, replacement in niche_data_sanitize.get("prompt_sanitization_replacements", []):
1166
+ prompt = re.sub(pattern, replacement, prompt, flags=re.IGNORECASE)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1167
 
1168
  # =====================================================================
1169
  # 3. FIX ILLOGICAL VISUAL COMBINATIONS
 
1364
  creative_direction = random.choice(niche_data["creative_directions"])
1365
  visual_mood = random.choice(niche_data["visual_moods"])
1366
 
1367
+ # Framework hook examples: AI-generated when enabled, else static from frameworks.py
1368
+ framework_hooks = await self._get_framework_hook_examples_async(framework_key, niche)
1369
+ ctas = await self._generate_ctas_async(niche, framework_data.get("name"))
1370
+ cta = random.choice(ctas) if ctas else "Learn More"
1371
 
1372
  # Use visual elements from visuals.py (instead of hardcoded)
1373
  visual_style_data = get_random_visual_style()
 
1436
  framework=framework,
1437
  framework_data=framework_data,
1438
  framework_hooks=framework_hooks,
1439
+ cta=cta,
1440
  trigger_data=trigger_data,
1441
  trigger_combination=trigger_combination,
1442
  power_words=power_words,
 
1452
  temperature=0.95, # High for variety
1453
  )
1454
 
1455
+ # Same framework drives visuals: set framework on ad_copy for image step
1456
+ ad_copy["framework_key"] = framework_data["key"]
1457
+ ad_copy["framework"] = framework_data["name"]
1458
+ ad_copy["container_key"] = framework_data["key"] # backward compat
1459
+ ad_copy["container_used"] = framework_data["name"] # backward compat
1460
+
1461
  # Generate image(s) with professional prompt - PARALLELIZED
1462
  async def generate_single_image(image_index: int):
1463
  """Helper function to generate a single image with all processing."""
 
1591
  why_it_works=ad_copy.get("why_it_works", ""),
1592
  username=username, # Pass username
1593
  image_url=image.get("image_url"),
1594
+ r2_url=image.get("r2_url"),
1595
  image_filename=image.get("filename"),
1596
  image_model=image.get("model_used"),
1597
  image_seed=image.get("seed"),
 
1756
 
1757
  # Get niche data
1758
  niche_data = NICHE_DATA.get(niche, home_insurance.get_niche_data)()
1759
+ ctas = await self._generate_ctas_async(niche)
1760
+ cta = random.choice(ctas) if ctas else "Learn More"
1761
 
1762
  # Build specialized prompt using angle + concept
1763
  ad_copy_prompt = self._build_matrix_ad_prompt(
 
1765
  angle=angle,
1766
  concept=concept,
1767
  niche_data=niche_data,
1768
+ cta=cta,
1769
  core_motivator=core_motivator,
1770
  target_audience=target_audience,
1771
  offer=offer,
 
1920
  why_it_works=ad_copy.get("why_it_works", ""),
1921
  username=username, # Pass username
1922
  image_url=image.get("image_url"),
1923
+ r2_url=image.get("r2_url"),
1924
  image_filename=image.get("filename"),
1925
  image_model=image.get("model_used"),
1926
  image_seed=image.get("seed"),
 
2235
  why_it_works=f"Angle: {strategy.angle}, Concept: {strategy.concept}",
2236
  username=username, # Pass username
2237
  image_url=image.get("image_url"),
2238
+ r2_url=image.get("r2_url"),
2239
  image_filename=image.get("filename"),
2240
  image_model=image.get("model_used"),
2241
  image_prompt=image.get("image_prompt"),
 
2445
  angle: Dict[str, Any],
2446
  concept: Dict[str, Any],
2447
  niche_data: Dict[str, Any],
2448
+ cta: str,
2449
  core_motivator: Optional[str] = None,
2450
  target_audience: Optional[str] = None,
2451
  offer: Optional[str] = None,
2452
  ) -> str:
2453
  """Build ad copy prompt using angle + concept framework."""
2454
 
 
 
2455
  # AI will decide whether to include numbers based on ad format and strategy
2456
  # Always provide guidance, AI decides usage
2457
 
2458
+ # Get niche-specific numbers guidance from number_config type (AI decides if/when to use)
2459
+ numbers = self._generate_niche_numbers(niche)
2460
+ num_type = niche_data.get("number_config", {}).get("type", "savings")
2461
+ if num_type == "weight_loss":
2462
+ numbers_section = f"""SPECIFIC NUMBERS TO USE:
2463
  - Weight Lost: {numbers['difference']}
2464
  - Timeframe: {numbers['days']}
2465
  - Starting: {numbers['before']}, Current: {numbers['after']}"""
2466
+ else:
2467
+ price_guidance = self._generate_specific_price(niche)
2468
+ numbers_section = f"""NUMBERS GUIDANCE (you decide if/when to use):
 
2469
  - Price Guidance: {price_guidance}
2470
  - Saved: {numbers['difference']}/year
2471
  - Before: {numbers['before']}, After: {numbers['after']}
 
2562
  ]
2563
  text_style = random.choice(text_styles)
2564
 
2565
+ # Get niche-specific guidance from niche data
2566
+ niche_data_img = self._get_niche_data(niche)
2567
+ niche_guidance = niche_data_img.get("image_niche_guidance_short", "").strip() or f"NICHE: {niche.replace('_', ' ').title()}"
 
 
 
 
 
 
 
 
 
 
2568
 
2569
  if use_motivator:
2570
  text_section = f'''=== MOTIVATOR PHRASE (blend naturally into the scene) ===