| | """ |
| | 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 |
| | """ |
| | |
| | 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 |
| | |
| | |
| | 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) |
| | |
| | |
| | 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 |
| | |
| | |
| | trigger = angle.get("trigger", "") |
| | compatible = get_compatible_concepts(trigger) |
| | |
| | |
| | if len(compatible) < concept_count: |
| | extra = get_random_concepts(concept_count - len(compatible), diverse=True) |
| | compatible.extend(extra) |
| | |
| | |
| | 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. |
| | """ |
| | |
| | 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) |
| | |
| | |
| | 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. |
| | """ |
| | |
| | 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 |
| | |
| | |
| | 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 |
| | |
| | |
| | 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 |
| | |
| | |
| | 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 |
| | |
| | |
| | angle_cat = angle.get("category_key") |
| | concept_cat = concept.get("category_key") |
| | |
| | |
| | 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), |
| | } |
| |
|
| |
|
| | |
| | matrix_service = AngleConceptMatrix() |
| |
|
| |
|