sushilideaclan01's picture
add new things in the extensive flow
026f283
"""
Angle × Concept Matrix Service
Implements the scaling formula:
1 Offer → 5-8 Angles → 3-5 Concepts per angle → Kill fast, scale hard
This creates systematic ad testing by generating all possible
angle × concept combinations with compatibility scoring.
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from typing import Dict, List, Any, Optional
import random
from data.angles import (
get_all_angles,
get_random_angles,
get_angles_for_niche,
get_top_angles,
get_angle_by_key,
AngleCategory,
)
try:
from data.ecom_verticals import get_random_vertical, get_angle_keys_for_vertical
except ImportError:
get_random_vertical = None
get_angle_keys_for_vertical = None
from data.concepts import (
get_all_concepts,
get_random_concepts,
get_top_concepts,
get_concept_by_key,
get_compatible_concepts,
ConceptCategory,
)
class AngleConceptMatrix:
"""
Service for generating angle × concept combinations.
Implements the scaling formula:
- Initial testing: 6 angles × 5 concepts = 30 ad variations
- Scale winners: 3 winning angles × 5-10 new concepts
"""
def __init__(self):
"""Initialize with all angles and concepts."""
self.all_angles = get_all_angles()
self.all_concepts = get_all_concepts()
def generate_testing_matrix(
self,
niche: Optional[str] = None,
angle_count: int = 6,
concept_count: int = 5,
strategy: str = "balanced",
unrestricted: bool = True,
) -> List[Dict[str, Any]]:
"""
Generate initial testing matrix.
Default: 6 angles × 5 concepts = 30 combinations
Args:
niche: Target niche for filtering (ignored for angles when unrestricted=True)
angle_count: Number of angles to test
concept_count: Number of concepts per angle
strategy: Selection strategy (balanced, top_performers, diverse)
unrestricted: If True, use full angle pool (all angles) for maximum diversity
Returns:
List of angle × concept combinations
"""
# Select angles: when unrestricted, use full/diverse pool; otherwise niche-filtered
if unrestricted:
if strategy == "top_performers":
angles = get_top_angles()[:angle_count]
else:
angles = get_random_angles(angle_count, diverse=True)
if len(angles) < angle_count:
angles.extend(get_random_angles(angle_count - len(angles), diverse=True))
elif strategy == "top_performers":
angles = get_top_angles()[:angle_count]
elif strategy == "diverse":
angles = get_random_angles(angle_count, diverse=True)
elif niche:
angles = get_angles_for_niche(niche)[:angle_count]
if len(angles) < angle_count:
extra = get_random_angles(angle_count - len(angles), diverse=True)
angles.extend(extra)
else:
top = get_top_angles()[:angle_count // 2]
diverse = get_random_angles(angle_count - len(top), diverse=True)
angles = top + diverse
# Select concepts
if strategy == "top_performers":
concepts = get_top_concepts()
if len(concepts) < concept_count:
concepts.extend(get_random_concepts(concept_count - len(concepts)))
else:
concepts = get_random_concepts(concept_count, diverse=True)
# Generate combinations
combinations = []
for angle in angles[:angle_count]:
for concept in concepts[:concept_count]:
combo = self._create_combination(angle, concept)
combinations.append(combo)
return combinations
def generate_scaling_matrix(
self,
winning_angle_keys: List[str],
concept_count: int = 5
) -> List[Dict[str, Any]]:
"""
Generate scaling matrix for winning angles.
After initial testing, scale the winning angles with new concepts.
Args:
winning_angle_keys: List of winning angle keys
concept_count: Number of new concepts per angle
Returns:
List of angle × concept combinations for scaling
"""
combinations = []
for angle_key in winning_angle_keys:
angle = get_angle_by_key(angle_key)
if not angle:
continue
# Get compatible concepts based on psychological trigger
trigger = angle.get("trigger", "")
compatible = get_compatible_concepts(trigger)
# If not enough compatible, add diverse ones
if len(compatible) < concept_count:
extra = get_random_concepts(concept_count - len(compatible), diverse=True)
compatible.extend(extra)
# Create combinations
for concept in compatible[:concept_count]:
combo = self._create_combination(angle, concept)
combinations.append(combo)
return combinations
def generate_single_combination(
self,
niche: Optional[str] = None,
unrestricted: bool = True,
) -> Dict[str, Any]:
"""
Generate a single random angle × concept combination.
Good for generating one-off ads with variety.
When unrestricted=True (default), use full angle and concept pool for maximum diversity.
When unrestricted=False and niche provided, filter angles by niche.
"""
# Get random angle: unrestricted = all angles or vertical-biased for variety; otherwise niche-filtered
if unrestricted or not niche:
if get_random_vertical and get_angle_keys_for_vertical and random.random() < 0.4:
v = get_random_vertical()
keys = get_angle_keys_for_vertical(v.get("key", ""))
vertical_angles = [get_angle_by_key(k) for k in keys if get_angle_by_key(k)]
angle = random.choice(vertical_angles) if vertical_angles else random.choice(self.all_angles)
else:
angle = random.choice(self.all_angles)
else:
angles = get_angles_for_niche(niche)
angle = random.choice(angles) if angles else random.choice(self.all_angles)
# Get concept: unrestricted = any concept for max diversity; otherwise trigger-compatible
if unrestricted:
concept = random.choice(self.all_concepts)
else:
trigger = angle.get("trigger", "")
compatible = get_compatible_concepts(trigger)
if compatible:
concept = random.choice(compatible)
else:
concept = random.choice(self.all_concepts)
return self._create_combination(angle, concept)
def generate_all_permutations(
self,
angle_keys: Optional[List[str]] = None,
concept_keys: Optional[List[str]] = None,
max_combinations: int = 100
) -> List[Dict[str, Any]]:
"""
Generate all possible permutations.
100 angles × 100 concepts = 10,000 possible combinations.
Limited by max_combinations for performance.
"""
# Get angles
if angle_keys:
angles = [get_angle_by_key(k) for k in angle_keys if get_angle_by_key(k)]
else:
angles = self.all_angles
# Get concepts
if concept_keys:
concepts = [get_concept_by_key(k) for k in concept_keys if get_concept_by_key(k)]
else:
concepts = self.all_concepts
# Generate combinations
combinations = []
for angle in angles:
for concept in concepts:
if len(combinations) >= max_combinations:
break
combo = self._create_combination(angle, concept)
combinations.append(combo)
if len(combinations) >= max_combinations:
break
return combinations
def _create_combination(
self,
angle: Dict[str, Any],
concept: Dict[str, Any]
) -> Dict[str, Any]:
"""Create an angle × concept combination with metadata."""
compatibility = self._calculate_compatibility(angle, concept)
return {
"combination_id": f"{angle.get('key')}_{concept.get('key')}",
"angle": {
"key": angle.get("key"),
"name": angle.get("name"),
"trigger": angle.get("trigger"),
"example": angle.get("example"),
"category": angle.get("category"),
},
"concept": {
"key": concept.get("key"),
"name": concept.get("name"),
"structure": concept.get("structure"),
"visual": concept.get("visual"),
"category": concept.get("category"),
},
"compatibility_score": compatibility,
"prompt_guidance": self._build_prompt_guidance(angle, concept),
}
def _calculate_compatibility(
self,
angle: Dict[str, Any],
concept: Dict[str, Any]
) -> float:
"""
Calculate compatibility score between angle and concept.
Higher score = better match.
"""
score = 0.5 # Base score
# Check trigger-concept compatibility
trigger = angle.get("trigger", "")
compatible_concepts = get_compatible_concepts(trigger)
if any(c.get("key") == concept.get("key") for c in compatible_concepts):
score += 0.3
# Check category compatibility
angle_cat = angle.get("category_key")
concept_cat = concept.get("category_key")
# Good pairs
good_pairs = [
(AngleCategory.FINANCIAL, ConceptCategory.COMPARISON),
(AngleCategory.EMOTIONAL, ConceptCategory.STORYTELLING),
(AngleCategory.SOCIAL_PROOF, ConceptCategory.SOCIAL_PROOF),
(AngleCategory.AUTHORITY, ConceptCategory.AUTHORITY),
(AngleCategory.URGENCY, ConceptCategory.SCROLL_STOPPING),
(AngleCategory.CURIOSITY, ConceptCategory.SCROLL_STOPPING),
(AngleCategory.CONVENIENCE, ConceptCategory.EDUCATIONAL),
(AngleCategory.PROBLEM_SOLUTION, ConceptCategory.STORYTELLING),
]
if (angle_cat, concept_cat) in good_pairs:
score += 0.2
return min(score, 1.0)
def _build_prompt_guidance(
self,
angle: Dict[str, Any],
concept: Dict[str, Any]
) -> str:
"""Build prompt guidance for ad generation."""
return f"""
ANGLE: {angle.get('name')}
- Psychological trigger: {angle.get('trigger')}
- Example hook: "{angle.get('example')}"
- Why it works: Appeals to {angle.get('trigger').lower()}
CONCEPT: {concept.get('name')}
- Visual structure: {concept.get('structure')}
- Visual guidance: {concept.get('visual')}
COMBINED APPROACH:
Create an ad that uses the "{angle.get('name')}" angle with a "{concept.get('name')}" visual concept.
The headline should trigger {angle.get('trigger').lower()} while the image follows the {concept.get('structure').lower()} structure.
""".strip()
def get_matrix_summary(
self,
combinations: List[Dict[str, Any]]
) -> Dict[str, Any]:
"""Get summary statistics for a matrix."""
if not combinations:
return {
"total_combinations": 0,
"unique_angles": 0,
"unique_concepts": 0,
"average_compatibility": 0.0,
}
unique_angles = set(c["angle"]["key"] for c in combinations)
unique_concepts = set(c["concept"]["key"] for c in combinations)
avg_compat = sum(c.get("compatibility_score", 0) for c in combinations) / len(combinations)
return {
"total_combinations": len(combinations),
"unique_angles": len(unique_angles),
"unique_concepts": len(unique_concepts),
"average_compatibility": round(avg_compat, 2),
"angles_used": list(unique_angles),
"concepts_used": list(unique_concepts),
}
# Global instance
matrix_service = AngleConceptMatrix()