Commit ·
254d33c
1
Parent(s): e5a550b
Refactor concepts and update header branding
Browse files- Removed the "Review Stars Visual" entry from the concepts in data/concepts.py to streamline visual options.
- Updated the header component in Header.tsx to change the displayed text from "Creative Breakthrough" to "PsyAdGenesis" for consistent branding across the application.
- Enhanced the generator service in generator.py with improved import organization and optimized data retrieval methods for better performance.
- data/concepts.py +0 -1
- frontend/components/layout/Header.tsx +1 -1
- services/generator.py +87 -73
data/concepts.py
CHANGED
|
@@ -116,7 +116,6 @@ CONCEPTS = {
|
|
| 116 |
"name": "Social Proof",
|
| 117 |
"concepts": [
|
| 118 |
{"key": "testimonial_screenshot", "name": "Testimonial Screenshot", "structure": "Screenshot format", "visual": "Authentic testimonial"},
|
| 119 |
-
{"key": "review_stars", "name": "Review Stars Visual", "structure": "Stars dominate", "visual": "Large stars, rating"},
|
| 120 |
{"key": "quote_card", "name": "Quote Card", "structure": "Quote as main visual", "visual": "Clear, readable"},
|
| 121 |
{"key": "case_study_frame", "name": "Case Study Frame", "structure": "Case study format", "visual": "Results visible"},
|
| 122 |
{"key": "crowd", "name": "Crowd Visual", "structure": "Many people visible", "visual": "Popular feel"},
|
|
|
|
| 116 |
"name": "Social Proof",
|
| 117 |
"concepts": [
|
| 118 |
{"key": "testimonial_screenshot", "name": "Testimonial Screenshot", "structure": "Screenshot format", "visual": "Authentic testimonial"},
|
|
|
|
| 119 |
{"key": "quote_card", "name": "Quote Card", "structure": "Quote as main visual", "visual": "Clear, readable"},
|
| 120 |
{"key": "case_study_frame", "name": "Case Study Frame", "structure": "Case study format", "visual": "Results visible"},
|
| 121 |
{"key": "crowd", "name": "Crowd Visual", "structure": "Many people visible", "visual": "Popular feel"},
|
frontend/components/layout/Header.tsx
CHANGED
|
@@ -35,7 +35,7 @@ export const Header: React.FC = () => {
|
|
| 35 |
<div className="absolute inset-0 bg-blue-500/20 rounded-full blur-xl group-hover:bg-cyan-500/20 transition-colors duration-300"></div>
|
| 36 |
</div>
|
| 37 |
<span className="text-2xl font-bold gradient-text group-hover:scale-105 transition-transform duration-300">
|
| 38 |
-
|
| 39 |
</span>
|
| 40 |
</Link>
|
| 41 |
</div>
|
|
|
|
| 35 |
<div className="absolute inset-0 bg-blue-500/20 rounded-full blur-xl group-hover:bg-cyan-500/20 transition-colors duration-300"></div>
|
| 36 |
</div>
|
| 37 |
<span className="text-2xl font-bold gradient-text group-hover:scale-105 transition-transform duration-300">
|
| 38 |
+
PsyAdGenesis
|
| 39 |
</span>
|
| 40 |
</Link>
|
| 41 |
</div>
|
services/generator.py
CHANGED
|
@@ -5,33 +5,35 @@ Uses professional prompting techniques for PsyAdGenesis
|
|
| 5 |
Saves ad creatives to Neon database with image URLs
|
| 6 |
"""
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
import os
|
| 9 |
import sys
|
| 10 |
-
|
| 11 |
-
# Add parent directory to path for imports
|
| 12 |
-
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 13 |
-
|
| 14 |
import random
|
| 15 |
import uuid
|
| 16 |
import json
|
| 17 |
import asyncio
|
| 18 |
from datetime import datetime
|
| 19 |
from typing import Dict, Any, List, Optional
|
| 20 |
-
from concurrent.futures import ThreadPoolExecutor
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
from config import settings
|
| 23 |
from services.llm import llm_service
|
| 24 |
from services.image import image_service
|
| 25 |
|
| 26 |
-
# Optional
|
| 27 |
try:
|
| 28 |
from services.database import db_service
|
| 29 |
except ImportError:
|
| 30 |
-
# Database service not available (asyncpg not installed)
|
| 31 |
db_service = None
|
| 32 |
print("Note: Database service not available (asyncpg not installed). Ads will be generated but not saved to database.")
|
| 33 |
|
| 34 |
-
# Optional R2 storage import
|
| 35 |
try:
|
| 36 |
from services.r2_storage import get_r2_storage
|
| 37 |
r2_storage_available = True
|
|
@@ -39,7 +41,6 @@ except ImportError:
|
|
| 39 |
r2_storage_available = False
|
| 40 |
print("Note: R2 storage not available. Images will only be saved locally.")
|
| 41 |
|
| 42 |
-
# Optional extensive import
|
| 43 |
try:
|
| 44 |
from services.third_flow import third_flow_service
|
| 45 |
third_flow_available = True
|
|
@@ -47,6 +48,7 @@ except ImportError:
|
|
| 47 |
third_flow_available = False
|
| 48 |
print("Note: Extensive service not available.")
|
| 49 |
|
|
|
|
| 50 |
from data import home_insurance, glp1
|
| 51 |
from services.matrix import matrix_service
|
| 52 |
from data.frameworks import get_frameworks_for_niche, get_framework_hook_examples, get_all_frameworks
|
|
@@ -61,6 +63,9 @@ from data.visuals import (
|
|
| 61 |
get_random_composition, get_random_mood, get_color_palette, get_niche_visual_guidance
|
| 62 |
)
|
| 63 |
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
# Niche data loaders
|
| 66 |
NICHE_DATA = {
|
|
@@ -68,10 +73,6 @@ NICHE_DATA = {
|
|
| 68 |
"glp1": glp1.get_niche_data,
|
| 69 |
}
|
| 70 |
|
| 71 |
-
# Note: Frameworks are now loaded from data/frameworks.py
|
| 72 |
-
# This provides comprehensive framework data with hooks, visual styles, and niche-specific recommendations
|
| 73 |
-
|
| 74 |
-
|
| 75 |
# Age brackets for identity targeting (proven high-CTR pattern)
|
| 76 |
AGE_BRACKETS = [
|
| 77 |
{"label": "21-40", "color": "yellow/gold button"},
|
|
@@ -82,17 +83,7 @@ AGE_BRACKETS = [
|
|
| 82 |
{"label": "70+ years old", "color": "gray box"},
|
| 83 |
]
|
| 84 |
|
| 85 |
-
#
|
| 86 |
-
# whether to include prices and what specific amounts to use based on the ad format and strategy.
|
| 87 |
-
|
| 88 |
-
# Note: Containers are now loaded from data/containers.py
|
| 89 |
-
# This provides comprehensive container data with visual guidance, colors, fonts, and authenticity tips
|
| 90 |
-
# Native containers (iMessage, WhatsApp, SMS, etc.) are preferred for ad-blindness bypass
|
| 91 |
-
|
| 92 |
-
# Note: CAMERA_ANGLES, LIGHTING_STYLES, and COMPOSITIONS are now imported from data/visuals.py
|
| 93 |
-
# This eliminates duplication and ensures consistency across the codebase
|
| 94 |
-
|
| 95 |
-
# Old footage / vintage film styles
|
| 96 |
VINTAGE_FILM_STYLES = [
|
| 97 |
"grainy 8mm home movie footage from the 1970s, warm faded colors, light leaks",
|
| 98 |
"old VHS tape recording with tracking lines, fuzzy edges, dated look",
|
|
@@ -126,10 +117,16 @@ class AdGenerator:
|
|
| 126 |
Implements professional prompting techniques.
|
| 127 |
"""
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
def __init__(self):
|
| 130 |
"""Initialize the generator."""
|
| 131 |
self.output_dir = settings.output_dir
|
| 132 |
os.makedirs(self.output_dir, exist_ok=True)
|
|
|
|
|
|
|
| 133 |
|
| 134 |
def _should_save_locally(self) -> bool:
|
| 135 |
"""
|
|
@@ -168,11 +165,22 @@ class AdGenerator:
|
|
| 168 |
print(f"Warning: Failed to save image locally: {e}")
|
| 169 |
return None
|
| 170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
def _get_niche_data(self, niche: str) -> Dict[str, Any]:
|
| 172 |
-
"""Load data for a specific niche."""
|
| 173 |
if niche not in NICHE_DATA:
|
| 174 |
raise ValueError(f"Unsupported niche: {niche}. Supported: {list(NICHE_DATA.keys())}")
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
|
| 177 |
def _get_framework_strategy_compatibility(self, framework_key: str, strategy_name: str) -> float:
|
| 178 |
"""
|
|
@@ -289,11 +297,14 @@ class AdGenerator:
|
|
| 289 |
return [niche_data["strategies"][name] for name in strategy_names]
|
| 290 |
|
| 291 |
def _random_hooks(self, strategies: List[Dict], count: int = 3) -> List[str]:
|
| 292 |
-
"""Randomly select hooks from the chosen strategies."""
|
| 293 |
-
|
| 294 |
-
for strategy in strategies
|
| 295 |
-
|
| 296 |
-
|
|
|
|
|
|
|
|
|
|
| 297 |
|
| 298 |
def _get_visual_library_for_niche(self, niche: str) -> Dict[str, List[str]]:
|
| 299 |
"""
|
|
@@ -384,11 +395,9 @@ class AdGenerator:
|
|
| 384 |
if visuals:
|
| 385 |
selected_visuals.extend(random.sample(visuals, min(1, len(visuals))))
|
| 386 |
|
| 387 |
-
# If we need more, add random from any category
|
| 388 |
if len(selected_visuals) < count:
|
| 389 |
-
all_visuals = []
|
| 390 |
-
for visuals in visual_library.values():
|
| 391 |
-
all_visuals.extend(visuals)
|
| 392 |
remaining = count - len(selected_visuals)
|
| 393 |
if all_visuals:
|
| 394 |
selected_visuals.extend(random.sample(all_visuals, min(remaining, len(all_visuals))))
|
|
@@ -408,28 +417,26 @@ class AdGenerator:
|
|
| 408 |
niche: Niche name for visual library access
|
| 409 |
use_library: Whether to use comprehensive visual library (improvement)
|
| 410 |
"""
|
| 411 |
-
|
| 412 |
-
|
| 413 |
# Get strategy-specific visuals
|
| 414 |
-
for strategy in strategies
|
| 415 |
-
all_styles.extend(strategy.get("visual_styles", []))
|
| 416 |
|
| 417 |
# Add visual library visuals (improvement)
|
| 418 |
if use_library and niche:
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
seen = set()
|
| 425 |
-
unique_styles = []
|
| 426 |
-
for style in all_styles:
|
| 427 |
-
if style not in seen:
|
| 428 |
-
seen.add(style)
|
| 429 |
-
unique_styles.append(style)
|
| 430 |
|
|
|
|
|
|
|
| 431 |
return random.sample(unique_styles, min(count, len(unique_styles))) if unique_styles else []
|
| 432 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
def _get_niche_specific_guidance(self, niche: str) -> str:
|
| 434 |
"""Get niche-specific guidance for the prompt."""
|
| 435 |
if niche == "home_insurance":
|
|
@@ -531,6 +538,10 @@ NICHE-SPECIFIC REQUIREMENTS (GLP-1 / WEIGHT LOSS):
|
|
| 531 |
# Return compatible first, then others
|
| 532 |
return compatible + [c for c in all_containers if c not in compatible]
|
| 533 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
def _select_container(self, prefer_native: bool = True, strategy: str = "balanced", framework_key: Optional[str] = None) -> Dict[str, Any]:
|
| 535 |
"""
|
| 536 |
Select a container type for native-looking ad format.
|
|
@@ -578,6 +589,10 @@ NICHE-SPECIFIC REQUIREMENTS (GLP-1 / WEIGHT LOSS):
|
|
| 578 |
|
| 579 |
return container if container else get_random_container()
|
| 580 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
def _build_copy_prompt(
|
| 582 |
self,
|
| 583 |
niche: str,
|
|
@@ -1904,22 +1919,20 @@ CONCEPT: {concept['name']}
|
|
| 1904 |
if not researcher_output:
|
| 1905 |
raise ValueError("Researcher returned no results")
|
| 1906 |
|
| 1907 |
-
# Step 2: Retrieve knowledge (in parallel)
|
| 1908 |
print("📚 Step 2: Retrieving marketing knowledge...")
|
| 1909 |
-
from concurrent.futures import ThreadPoolExecutor
|
| 1910 |
|
| 1911 |
-
|
| 1912 |
-
|
|
|
|
| 1913 |
third_flow_service.retrieve_search,
|
| 1914 |
target_audience, offer, niche_display
|
| 1915 |
-
)
|
| 1916 |
-
|
| 1917 |
third_flow_service.retrieve_ads,
|
| 1918 |
target_audience, offer, niche_display
|
| 1919 |
)
|
| 1920 |
-
|
| 1921 |
-
book_knowledge = book_future.result()
|
| 1922 |
-
ads_knowledge = ads_future.result()
|
| 1923 |
|
| 1924 |
# Step 3: Creative Director
|
| 1925 |
print(f"🎨 Step 3: Creating {num_strategies} creative strategy/strategies...")
|
|
@@ -1941,22 +1954,19 @@ CONCEPT: {concept['name']}
|
|
| 1941 |
creative_strategies = creative_strategies[:num_strategies]
|
| 1942 |
print(f"📊 Using {len(creative_strategies)} strategy/strategies (requested: {num_strategies})")
|
| 1943 |
|
| 1944 |
-
# Step 4: Process strategies in parallel (designer + copywriter)
|
| 1945 |
print(f"⚡ Step 4: Processing {len(creative_strategies)} strategies in parallel...")
|
| 1946 |
-
from concurrent.futures import ThreadPoolExecutor as TPE
|
| 1947 |
-
from functools import partial
|
| 1948 |
-
|
| 1949 |
-
# Create a partial function that includes the niche parameter
|
| 1950 |
-
process_strategy_with_niche = partial(
|
| 1951 |
-
third_flow_service.process_strategy,
|
| 1952 |
-
niche=niche_display
|
| 1953 |
-
)
|
| 1954 |
|
| 1955 |
-
|
| 1956 |
-
|
| 1957 |
-
|
| 1958 |
-
|
| 1959 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1960 |
|
| 1961 |
# Step 5: Generate images for each strategy
|
| 1962 |
# Ensure we only process the requested number of strategies
|
|
@@ -2162,6 +2172,10 @@ CONCEPT: {concept['name']}
|
|
| 2162 |
else:
|
| 2163 |
raise ValueError("No ads generated from extensive")
|
| 2164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2165 |
def _build_matrix_ad_prompt(
|
| 2166 |
self,
|
| 2167 |
niche: str,
|
|
|
|
| 5 |
Saves ad creatives to Neon database with image URLs
|
| 6 |
"""
|
| 7 |
|
| 8 |
+
# ============================================================================
|
| 9 |
+
# IMPORTS
|
| 10 |
+
# ============================================================================
|
| 11 |
+
|
| 12 |
+
# Standard library
|
| 13 |
import os
|
| 14 |
import sys
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
import random
|
| 16 |
import uuid
|
| 17 |
import json
|
| 18 |
import asyncio
|
| 19 |
from datetime import datetime
|
| 20 |
from typing import Dict, Any, List, Optional
|
|
|
|
| 21 |
|
| 22 |
+
# Add parent directory to path for imports
|
| 23 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 24 |
+
|
| 25 |
+
# Local application imports
|
| 26 |
from config import settings
|
| 27 |
from services.llm import llm_service
|
| 28 |
from services.image import image_service
|
| 29 |
|
| 30 |
+
# Optional service imports
|
| 31 |
try:
|
| 32 |
from services.database import db_service
|
| 33 |
except ImportError:
|
|
|
|
| 34 |
db_service = None
|
| 35 |
print("Note: Database service not available (asyncpg not installed). Ads will be generated but not saved to database.")
|
| 36 |
|
|
|
|
| 37 |
try:
|
| 38 |
from services.r2_storage import get_r2_storage
|
| 39 |
r2_storage_available = True
|
|
|
|
| 41 |
r2_storage_available = False
|
| 42 |
print("Note: R2 storage not available. Images will only be saved locally.")
|
| 43 |
|
|
|
|
| 44 |
try:
|
| 45 |
from services.third_flow import third_flow_service
|
| 46 |
third_flow_available = True
|
|
|
|
| 48 |
third_flow_available = False
|
| 49 |
print("Note: Extensive service not available.")
|
| 50 |
|
| 51 |
+
# Data module imports
|
| 52 |
from data import home_insurance, glp1
|
| 53 |
from services.matrix import matrix_service
|
| 54 |
from data.frameworks import get_frameworks_for_niche, get_framework_hook_examples, get_all_frameworks
|
|
|
|
| 63 |
get_random_composition, get_random_mood, get_color_palette, get_niche_visual_guidance
|
| 64 |
)
|
| 65 |
|
| 66 |
+
# ============================================================================
|
| 67 |
+
# CONSTANTS
|
| 68 |
+
# ============================================================================
|
| 69 |
|
| 70 |
# Niche data loaders
|
| 71 |
NICHE_DATA = {
|
|
|
|
| 73 |
"glp1": glp1.get_niche_data,
|
| 74 |
}
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
# Age brackets for identity targeting (proven high-CTR pattern)
|
| 77 |
AGE_BRACKETS = [
|
| 78 |
{"label": "21-40", "color": "yellow/gold button"},
|
|
|
|
| 83 |
{"label": "70+ years old", "color": "gray box"},
|
| 84 |
]
|
| 85 |
|
| 86 |
+
# Vintage film visual styles
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
VINTAGE_FILM_STYLES = [
|
| 88 |
"grainy 8mm home movie footage from the 1970s, warm faded colors, light leaks",
|
| 89 |
"old VHS tape recording with tracking lines, fuzzy edges, dated look",
|
|
|
|
| 117 |
Implements professional prompting techniques.
|
| 118 |
"""
|
| 119 |
|
| 120 |
+
# ========================================================================
|
| 121 |
+
# INITIALIZATION & UTILITY METHODS
|
| 122 |
+
# ========================================================================
|
| 123 |
+
|
| 124 |
def __init__(self):
|
| 125 |
"""Initialize the generator."""
|
| 126 |
self.output_dir = settings.output_dir
|
| 127 |
os.makedirs(self.output_dir, exist_ok=True)
|
| 128 |
+
# Cache niche data to avoid repeated function calls
|
| 129 |
+
self._niche_data_cache: Dict[str, Dict[str, Any]] = {}
|
| 130 |
|
| 131 |
def _should_save_locally(self) -> bool:
|
| 132 |
"""
|
|
|
|
| 165 |
print(f"Warning: Failed to save image locally: {e}")
|
| 166 |
return None
|
| 167 |
|
| 168 |
+
# ========================================================================
|
| 169 |
+
# DATA RETRIEVAL & CACHING METHODS
|
| 170 |
+
# ========================================================================
|
| 171 |
+
|
| 172 |
def _get_niche_data(self, niche: str) -> Dict[str, Any]:
|
| 173 |
+
"""Load data for a specific niche (cached for performance)."""
|
| 174 |
if niche not in NICHE_DATA:
|
| 175 |
raise ValueError(f"Unsupported niche: {niche}. Supported: {list(NICHE_DATA.keys())}")
|
| 176 |
+
# Use cache to avoid repeated function calls
|
| 177 |
+
if niche not in self._niche_data_cache:
|
| 178 |
+
self._niche_data_cache[niche] = NICHE_DATA[niche]()
|
| 179 |
+
return self._niche_data_cache[niche]
|
| 180 |
+
|
| 181 |
+
# ========================================================================
|
| 182 |
+
# STRATEGY & COMPATIBILITY METHODS
|
| 183 |
+
# ========================================================================
|
| 184 |
|
| 185 |
def _get_framework_strategy_compatibility(self, framework_key: str, strategy_name: str) -> float:
|
| 186 |
"""
|
|
|
|
| 297 |
return [niche_data["strategies"][name] for name in strategy_names]
|
| 298 |
|
| 299 |
def _random_hooks(self, strategies: List[Dict], count: int = 3) -> List[str]:
|
| 300 |
+
"""Randomly select hooks from the chosen strategies (optimized list building)."""
|
| 301 |
+
# Optimized: use list comprehension instead of extend in loop
|
| 302 |
+
all_hooks = [hook for strategy in strategies for hook in strategy["hooks"]]
|
| 303 |
+
return random.sample(all_hooks, min(count, len(all_hooks))) if all_hooks else []
|
| 304 |
+
|
| 305 |
+
# ========================================================================
|
| 306 |
+
# VISUAL SELECTION METHODS
|
| 307 |
+
# ========================================================================
|
| 308 |
|
| 309 |
def _get_visual_library_for_niche(self, niche: str) -> Dict[str, List[str]]:
|
| 310 |
"""
|
|
|
|
| 395 |
if visuals:
|
| 396 |
selected_visuals.extend(random.sample(visuals, min(1, len(visuals))))
|
| 397 |
|
| 398 |
+
# If we need more, add random from any category (optimized: use list comprehension)
|
| 399 |
if len(selected_visuals) < count:
|
| 400 |
+
all_visuals = [v for visuals in visual_library.values() for v in visuals]
|
|
|
|
|
|
|
| 401 |
remaining = count - len(selected_visuals)
|
| 402 |
if all_visuals:
|
| 403 |
selected_visuals.extend(random.sample(all_visuals, min(remaining, len(all_visuals))))
|
|
|
|
| 417 |
niche: Niche name for visual library access
|
| 418 |
use_library: Whether to use comprehensive visual library (improvement)
|
| 419 |
"""
|
| 420 |
+
# Optimized: use list comprehension instead of extend in loops
|
|
|
|
| 421 |
# Get strategy-specific visuals
|
| 422 |
+
all_styles = [style for strategy in strategies for style in strategy.get("visual_styles", [])]
|
|
|
|
| 423 |
|
| 424 |
# Add visual library visuals (improvement)
|
| 425 |
if use_library and niche:
|
| 426 |
+
library_visuals = [
|
| 427 |
+
v for strategy in strategies
|
| 428 |
+
for v in self._select_visuals_from_library(niche, strategy.get("name", ""), count=1)
|
| 429 |
+
]
|
| 430 |
+
all_styles.extend(library_visuals)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
|
| 432 |
+
# Remove duplicates while preserving order (optimized using dict.fromkeys)
|
| 433 |
+
unique_styles = list(dict.fromkeys(all_styles))
|
| 434 |
return random.sample(unique_styles, min(count, len(unique_styles))) if unique_styles else []
|
| 435 |
|
| 436 |
+
# ========================================================================
|
| 437 |
+
# NICHE & CONTENT CONFIGURATION METHODS
|
| 438 |
+
# ========================================================================
|
| 439 |
+
|
| 440 |
def _get_niche_specific_guidance(self, niche: str) -> str:
|
| 441 |
"""Get niche-specific guidance for the prompt."""
|
| 442 |
if niche == "home_insurance":
|
|
|
|
| 538 |
# Return compatible first, then others
|
| 539 |
return compatible + [c for c in all_containers if c not in compatible]
|
| 540 |
|
| 541 |
+
# ========================================================================
|
| 542 |
+
# CONTAINER SELECTION METHODS
|
| 543 |
+
# ========================================================================
|
| 544 |
+
|
| 545 |
def _select_container(self, prefer_native: bool = True, strategy: str = "balanced", framework_key: Optional[str] = None) -> Dict[str, Any]:
|
| 546 |
"""
|
| 547 |
Select a container type for native-looking ad format.
|
|
|
|
| 589 |
|
| 590 |
return container if container else get_random_container()
|
| 591 |
|
| 592 |
+
# ========================================================================
|
| 593 |
+
# PROMPT BUILDING METHODS
|
| 594 |
+
# ========================================================================
|
| 595 |
+
|
| 596 |
def _build_copy_prompt(
|
| 597 |
self,
|
| 598 |
niche: str,
|
|
|
|
| 1919 |
if not researcher_output:
|
| 1920 |
raise ValueError("Researcher returned no results")
|
| 1921 |
|
| 1922 |
+
# Step 2: Retrieve knowledge (in parallel) - Optimized: use asyncio.to_thread instead of blocking .result()
|
| 1923 |
print("📚 Step 2: Retrieving marketing knowledge...")
|
|
|
|
| 1924 |
|
| 1925 |
+
# Run synchronous operations in thread pool without blocking event loop
|
| 1926 |
+
book_knowledge, ads_knowledge = await asyncio.gather(
|
| 1927 |
+
asyncio.to_thread(
|
| 1928 |
third_flow_service.retrieve_search,
|
| 1929 |
target_audience, offer, niche_display
|
| 1930 |
+
),
|
| 1931 |
+
asyncio.to_thread(
|
| 1932 |
third_flow_service.retrieve_ads,
|
| 1933 |
target_audience, offer, niche_display
|
| 1934 |
)
|
| 1935 |
+
)
|
|
|
|
|
|
|
| 1936 |
|
| 1937 |
# Step 3: Creative Director
|
| 1938 |
print(f"🎨 Step 3: Creating {num_strategies} creative strategy/strategies...")
|
|
|
|
| 1954 |
creative_strategies = creative_strategies[:num_strategies]
|
| 1955 |
print(f"📊 Using {len(creative_strategies)} strategy/strategies (requested: {num_strategies})")
|
| 1956 |
|
| 1957 |
+
# Step 4: Process strategies in parallel (designer + copywriter) - Optimized: use asyncio.to_thread instead of ThreadPoolExecutor
|
| 1958 |
print(f"⚡ Step 4: Processing {len(creative_strategies)} strategies in parallel...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1959 |
|
| 1960 |
+
# Process strategies concurrently using asyncio (non-blocking)
|
| 1961 |
+
strategy_tasks = [
|
| 1962 |
+
asyncio.to_thread(
|
| 1963 |
+
third_flow_service.process_strategy,
|
| 1964 |
+
strategy,
|
| 1965 |
+
niche=niche_display
|
| 1966 |
+
)
|
| 1967 |
+
for strategy in creative_strategies
|
| 1968 |
+
]
|
| 1969 |
+
strategy_results = await asyncio.gather(*strategy_tasks)
|
| 1970 |
|
| 1971 |
# Step 5: Generate images for each strategy
|
| 1972 |
# Ensure we only process the requested number of strategies
|
|
|
|
| 2172 |
else:
|
| 2173 |
raise ValueError("No ads generated from extensive")
|
| 2174 |
|
| 2175 |
+
# ========================================================================
|
| 2176 |
+
# MATRIX-SPECIFIC PROMPT METHODS
|
| 2177 |
+
# ========================================================================
|
| 2178 |
+
|
| 2179 |
def _build_matrix_ad_prompt(
|
| 2180 |
self,
|
| 2181 |
niche: str,
|