"""Extensive ad generation: researcher → creative director → designer → copywriter (OpenAI + file search). Supports optional Creative Inventor to generate new angles, concepts, visuals, and triggers by itself.""" import os import sys import time from typing import List, Optional from pydantic import BaseModel # Add parent directory to path for imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from openai import OpenAI from config import settings # Pydantic models for structured outputs class ImageAdEssentials(BaseModel): phsychologyTriggers: str angles: list[str] concepts: list[str] class ImageAdEssentialsOutput(BaseModel): output: list[ImageAdEssentials] class Text(BaseModel): textToBeWrittern: str color: str placement: str class CreativeStrategies(BaseModel): phsychologyTrigger: str angle: str concept: str text: Text cta: str visualDirection: str titleIdeas: str captionIdeas: str bodyIdeas: str class CreativeStrategiesOutput(BaseModel): output: list[CreativeStrategies] class AdImagePrompt(BaseModel): prompt: str class CopyWriterOutput(BaseModel): title: str body: str description: str class ThirdFlowService: """Extensive ad generation (researcher → creative director → designer → copywriter).""" def __init__(self): """Set up OpenAI client and vector store IDs.""" self.client = OpenAI(api_key=settings.openai_api_key) self.search_vector_store_id = "vs_691afcc4f8688191b01487b4a8439607" self.ads_vector_store_id = "vs_69609db487048191a1e6b7ba0997ee39" self.gpt_model = getattr(settings, 'third_flow_model', 'gpt-4o') def researcher( self, target_audience: str, offer: str, niche: str = "" ) -> List[ImageAdEssentials]: """Return psychology triggers, angles, and concepts for niche/offer/audience.""" messages = [ { "role": "system", "content": [ { "type": "text", "text": """You are the researcher with 20 years of experience for the affiliate marketing company which does research on trending angles, concepts and psychology triggers based on the user input. Affiliate marketing is a performance-based model where you promote someone else's product or service and earn a commission for each qualified action (click, lead, or sale). A psychology trigger is an emotional or cognitive stimulus that pushes someone toward action—clicking, signing up, or buying—before logic kicks in. An ad angle is the reason someone should care right now. Same product → different reasons to click → different angles. An ad concept is the creative execution style or storyline you use to deliver an angle. In affiliate marketing 'Low-production, realistic often outperform studio creatives' runs most. Invent psychology triggers, angles, and concepts without limiting to the given niche. Suggest diverse and hyperrealistic audiences including outside the niche. Prioritize novelty and variety. Provide different angles and concepts we can try based on psychology triggers for the image ads; use the user input as a springboard, not a constraint. User will provide you the category on which he needs to run the ads, what is the offer he is providing and what is target audience.""" } ] }, { "role": "user", "content": [ { "type": "text", "text": f"""Following are the inputs: Niche: {niche} Offer to run: {offer} Target Audience: {target_audience} Provide the different psychology triggers, angles and concept based on the given input.""" } ] } ] try: completion = self.client.beta.chat.completions.parse( model=self.gpt_model, messages=messages, response_format=ImageAdEssentialsOutput, ) response = completion.choices[0].message if response.parsed: return response.parsed.output else: print(f"Warning: Researcher refusal: {response.refusal}") # Return empty list if refused return [] except Exception as e: print(f"Error in researcher: {e}") return [] def get_essentials_via_inventor( self, niche: str, offer: str, n: int = 5, *, target_audience_hint: Optional[str] = None, existing_reference: Optional[str] = None, trend_context: Optional[str] = None, competitor_insights: Optional[str] = None, ) -> tuple[List[ImageAdEssentials], List[str]]: """ Use the Creative Inventor to generate new angles, concepts, visuals, psychological triggers, and hyper-specific target audiences. Returns (essentials for creative_director, list of target_audience per essential). """ try: from services.creative_inventor import creative_inventor_service except ImportError: from creative_inventor import creative_inventor_service # noqa: F401 invented = creative_inventor_service.invent( niche=niche, offer=offer, n=n, target_audience_hint=target_audience_hint, existing_reference=existing_reference, trend_context=trend_context, competitor_insights=competitor_insights, ) essentials = self._invented_to_essentials(invented) target_audiences = [getattr(e, "target_audience", "") or f"Audience {i+1}" for i, e in enumerate(invented)] return (essentials, target_audiences) def _invented_to_essentials(self, invented: list) -> List[ImageAdEssentials]: """Convert InventedEssential list to ImageAdEssentials for creative_director.""" out: List[ImageAdEssentials] = [] for e in invented: # Fold visual_directions into concepts so creative_director gets visual hints concepts = list(getattr(e, "concepts", [])) + list(getattr(e, "visual_directions", [])) out.append(ImageAdEssentials( phsychologyTriggers=getattr(e, "psychology_trigger", ""), angles=list(getattr(e, "angles", [])), concepts=concepts, )) return out def retrieve_search( self, target_audience: str, offer: str, niche: str = "" ) -> str: """Retrieve marketing knowledge from vector store (file search).""" try: # Method 1: Try using responses.create if hasattr(self.client, 'responses') and hasattr(self.client.responses, 'create'): try: search = self.client.responses.create( model="gpt-4o", input=f"Find {niche} creative strategies relevant to ad images related to target audience: {target_audience} and offer: {offer}. The image ads are associated with performance marketing and affiliate marketing ads. If there is nothing related to this category then take reference from everything and make strategies. Make sure you go through each and every document present and from each file you should give results.", tools=[ { "type": "file_search", "vector_store_ids": [self.search_vector_store_id] } ] ) result = search.output[1].content[0].text print("✓ Used responses.create API for search retrieval") return result except Exception as custom_api_error: error_str = str(custom_api_error) print(f"⚠️ responses.create API error: {error_str[:100]}...") raise Exception(f"Failed to retrieve search knowledge via responses.create: {error_str}") from custom_api_error # Method 2: Try using Assistants API (official OpenAI method) query = f"Find {niche} creative strategies relevant to ad images related to target audience: {target_audience} and offer: {offer}. The image ads are associated with performance marketing and affiliate marketing ads. If there is nothing related to this category then take reference from everything and make strategies. Make sure you go through each and every document present and from each file you should give results." try: # Create assistant with vector store assistant = self.client.beta.assistants.create( name="Marketing Knowledge Assistant", instructions="You are a marketing research assistant with 20 years of experience. Search through the provided documents and extract relevant creative strategies and knowledge.", model="gpt-4o", tools=[{"type": "file_search"}], tool_resources={ "file_search": { "vector_store_ids": [self.search_vector_store_id] } } ) # Create a thread and run thread = self.client.beta.threads.create() message = self.client.beta.threads.messages.create( thread_id=thread.id, role="user", content=query ) # Run the assistant run = self.client.beta.threads.runs.create( thread_id=thread.id, assistant_id=assistant.id ) # Wait for completion import time while run.status in ['queued', 'in_progress']: time.sleep(1) run = self.client.beta.threads.runs.retrieve( thread_id=thread.id, run_id=run.id ) if run.status == 'completed': # Get the messages messages = self.client.beta.threads.messages.list( thread_id=thread.id ) # Get the assistant's response for msg in messages.data: if msg.role == 'assistant': if msg.content[0].type == 'text': result = msg.content[0].text.value # Clean up self.client.beta.assistants.delete(assistant.id) return result # Clean up self.client.beta.assistants.delete(assistant.id) except Exception as api_error: error_str = str(api_error) print(f"⚠️ Assistants API error: {error_str[:100]}...") raise Exception(f"Failed to retrieve search knowledge: {error_str}") from api_error # If we reach here, both methods failed raise Exception("Failed to retrieve search knowledge: Both API methods failed") except Exception as e: print(f"Error in retrieve_search: {e}") raise def retrieve_ads( self, target_audience: str, offer: str, niche: str = "" ) -> str: """Retrieve ads knowledge from vector store (file search).""" try: # Method 1: Try using responses.create if hasattr(self.client, 'responses') and hasattr(self.client.responses, 'create'): try: search = self.client.responses.create( model="gpt-4o", input=f"Find {niche} creative ad ideas relevant to ad images related to target audience: {target_audience} and offer: {offer}. The image ads are associated with performance marketing and affiliate marketing ads.", tools=[ { "type": "file_search", "vector_store_ids": [self.ads_vector_store_id] } ] ) result = search.output[1].content[0].text print("✓ Used responses.create API for ads retrieval") return result except Exception as custom_api_error: error_str = str(custom_api_error) print(f"⚠️ responses.create API error: {error_str[:100]}...") raise Exception(f"Failed to retrieve search knowledge via responses.create: {error_str}") from custom_api_error # Method 2: Try using Assistants API (official OpenAI method) query = f"Find {niche} creative ad ideas relevant to ad images related to target audience: {target_audience} and offer: {offer}. The image ads are associated with performance marketing and affiliate marketing ads." try: # Create assistant with vector store assistant = self.client.beta.assistants.create( name="Ads Knowledge Assistant", instructions="You are a marketing research assistant with 20 years of experience. Search through the provided ad examples and extract relevant creative ideas and patterns.", model="gpt-4o", tools=[{"type": "file_search"}], tool_resources={ "file_search": { "vector_store_ids": [self.ads_vector_store_id] } } ) # Create a thread and run thread = self.client.beta.threads.create() message = self.client.beta.threads.messages.create( thread_id=thread.id, role="user", content=query ) # Run the assistant run = self.client.beta.threads.runs.create( thread_id=thread.id, assistant_id=assistant.id ) # Wait for completion import time while run.status in ['queued', 'in_progress']: time.sleep(1) run = self.client.beta.threads.runs.retrieve( thread_id=thread.id, run_id=run.id ) if run.status == 'completed': # Get the messages messages = self.client.beta.threads.messages.list( thread_id=thread.id ) # Get the assistant's response for msg in messages.data: if msg.role == 'assistant': if msg.content[0].type == 'text': result = msg.content[0].text.value # Clean up self.client.beta.assistants.delete(assistant.id) return result # Clean up self.client.beta.assistants.delete(assistant.id) except Exception as api_error: error_str = str(api_error) print(f"⚠️ Assistants API error: {error_str[:100]}...") raise Exception(f"Failed to retrieve ads knowledge: {error_str}") from api_error # If we reach here, both methods failed raise Exception("Failed to retrieve ads knowledge: Both API methods failed") except Exception as e: print(f"Error in retrieve_ads: {e}") raise def creative_director( self, researcher_output: List[ImageAdEssentials], book_knowledge: str, ads_knowledge: str, target_audience: str, offer: str, niche: str = "", n: int = 5, target_audiences: Optional[List[str]] = None, ) -> List[CreativeStrategies]: """Create creative strategies from research, book knowledge, and ads knowledge. When target_audiences is provided (one per strategy), each strategy is tailored to that hyper-specific audience.""" # Convert researcher_output to string for prompt researcher_str = "\n".join([ f"Psychology Triggers: {item.phsychologyTriggers}\n" f"Angles: {', '.join(item.angles)}\n" f"Concepts: {', '.join(item.concepts)}" for item in researcher_output ]) messages = [ { "role": "system", "content": [ { "type": "text", "text": f"""You are the Creative Director with 20 years of experience for the affiliate marketing company which make creative strategies for the image ads on the basis of the research given and user's input. The research work includes the psychology triggers, angles and different concepts. Your work is to finalise the {n} strategies based on the research. There will also be researched content from the different marketing books. Along with these there will information about the old ads information which are winner. Make the strongest patterns for the image ads, which should include about what types of visual should be their, colors, what should be the text, what should be the tone of the text with it's placement and CTA. Strategies can target any audience and use any visual or concept—research is a springboard, not a constraint. When the user provides per-strategy target audiences, you MUST tailor each strategy (angle, concept, visual, title, body) to that specific audience. Along with this provide the title ideas and description/caption which should be added with the image ad. It will complete the full ad copy. If the image should include only visuals then text field must return None or NA. What information you should give make sure you give in brief and well defined. Affiliate marketing is a performance-based model where you promote someone else's product or service and earn a commission for each qualified action (click, lead, or sale). In affiliate marketing 'Low-production, realistic images often outperform studio creatives' runs most. Role of the Title: Stop the scroll and trigger emotion. Role of Body: The body is the main paragraph text which should Explain just enough, Reduce anxiety, and Push to the next step. Role of Description: Reduce friction and justify the click. Keeping in mind all this, make sure you provide different creative strategies for the image ads for the given input based on affiliate marketing. User will provide you the category on which he needs to run the ads, what is the offer he is providing and what is target audience, along with the research.""" } ] }, { "role": "user", "content": [ { "type": "text", "text": self._creative_director_user_message( researcher_str, book_knowledge, ads_knowledge, niche, offer, target_audience, n, target_audiences, ) } ] } ] try: completion = self.client.beta.chat.completions.parse( model=self.gpt_model, messages=messages, response_format=CreativeStrategiesOutput, ) response = completion.choices[0].message if response.parsed: return response.parsed.output else: print(f"Warning: Creative director refusal: {response.refusal}") return [] except Exception as e: print(f"Error in creative_director: {e}") return [] def _creative_director_user_message( self, researcher_str: str, book_knowledge: str, ads_knowledge: str, niche: str, offer: str, target_audience: str, n: int, target_audiences: Optional[List[str]] = None, ) -> str: """Build user message for creative_director; inject per-strategy audiences when provided.""" audience_block = "" if target_audiences and len(target_audiences) >= n: per = "\n".join([f"Strategy {i+1} must target: {aud}" for i, aud in enumerate(target_audiences[:n])]) audience_block = f"\n\nCRITICAL - Each strategy must speak to this hyper-specific audience:\n{per}\n\nTailor angle, concept, visual, title, and body to that audience." return f"""Following are the inputs: Researched Content: {researcher_str} Researched Content from marketing books: {book_knowledge} Old Ads Data: {ads_knowledge} Niche: {niche} Offer to run: {offer} Target Audience (overall): {target_audience} {audience_block} Provide the different creative strategies based on the given input.""" def creative_designer(self, creative_strategy: CreativeStrategies, niche: str = "") -> str: """Generate image prompt from a creative strategy.""" niche_lower = niche.lower().replace(" ", "_").replace("-", "_") if niche else "" niche_guidance = f"\nThe image must be appropriate for the {niche} niche." if niche else "" text_overlay = creative_strategy.text.textToBeWrittern if creative_strategy.text and creative_strategy.text.textToBeWrittern not in (None, "None", "NA", "") else "No text overlay" strategy_str = f"""Psychology Trigger: {creative_strategy.phsychologyTrigger} Angle: {creative_strategy.angle} Concept: {creative_strategy.concept} Text: {text_overlay} CTA: {creative_strategy.cta} Visual Direction: {creative_strategy.visualDirection} """ messages = [ { "role": "system", "content": [ { "type": "text", "text": f"""You are the Creative Designer with 20 years of experience for the affiliate marketing company which makes the prompt from creative strategy given for the ad images in the affiliate marketing. Nano Banana image model will be used to generate the images Affiliate marketing is a performance-based model where you promote someone else's product or service and earn a commission for each qualified action (click, lead, or sale). In affiliate marketing 'Low-production, realistic images often outperform studio creatives' runs most. If the image looks like it belongs on a stock website, it has failed. The psychology trigger and angle from the strategy are the emotional driver. Structure the prompt so the image directly conveys that emotional truth—the scene, expressions, props, and composition must all serve it. A generic scene is not enough; the strategy must differentiate this creative. For image model here's structure for the prompt: [The Hook - emotion from psychology trigger/angle] + [The Subject] + [The Context/Setting] + [The Technical Polish] {niche_guidance} CRITICAL - TEXT IN IMAGE: The strategy includes "Text" (title/caption to show). Your prompt MUST describe where and how this text appears in the image as visible, readable copy—e.g. on a sign, document, phone screen, poster, note, or surface in the scene. The image must contain that exact text (or the main phrase) so viewers can read it. Do not omit text from the prompt. CRITICAL: If the image includes people or faces, ensure they look like real, original people with: - Photorealistic faces with natural skin texture, visible pores, and realistic skin imperfections - Natural facial asymmetry (no perfectly symmetrical faces) - Unique, individual facial features (not generic or model-like) - Natural expressions with authentic micro-expressions - Realistic skin tones with natural variations - Faces that look like real photographs of real people, NOT AI-generated portraits - Avoid any faces that look synthetic, fake, or obviously computer-generated - Avoid overly smooth or plastic-looking skin - Avoid perfectly symmetrical faces or generic model-like features""" } ] }, { "role": "user", "content": [ { "type": "text", "text": f"""Following is the creative strategy: {strategy_str} Provide the image prompt. The prompt MUST lead with the strategy's emotional hook (psychology trigger and angle)—describe a scene that makes that feeling unmistakable. Then add subject, setting, and technical polish. You MUST include in the prompt a clear description of where the Text from the strategy appears in the scene (e.g. "with the text '[exact phrase]' visible on a sign/phone/document") so the generated image contains readable copy.""" } ] } ] try: completion = self.client.beta.chat.completions.parse( model=self.gpt_model, messages=messages, response_format=AdImagePrompt, ) response = completion.choices[0].message if response.parsed: # Refine the prompt for affiliate marketing raw_prompt = response.parsed.prompt refined_prompt = self._refine_prompt_for_affiliate(raw_prompt, niche_lower) return refined_prompt else: print(f"Warning: Creative designer refusal: {response.refusal}") return "" except Exception as e: print(f"Error in creative_designer: {e}") return "" def _refine_prompt_for_affiliate(self, prompt: str, niche: str) -> str: """Refine prompt for affiliate creatives: fix stock/corporate wording, ensure authenticity.""" import re if not prompt: return prompt prompt_lower = prompt.lower() # ================================================================= # REMOVE STOCK PHOTO / CORPORATE AESTHETICS # ================================================================= stock_replacements = [ (r'\bstock photo\b', 'authentic photo'), (r'\bprofessional studio shot\b', 'natural candid shot'), (r'\bcorporate headshot\b', 'casual portrait'), (r'\bgeneric model\b', 'real person'), (r'\bperfect lighting\b', 'natural lighting'), (r'\bshutterstock\b', 'authentic'), (r'\bistock\b', 'documentary style'), ] for pattern, replacement in stock_replacements: prompt = re.sub(pattern, replacement, prompt, flags=re.IGNORECASE) # ================================================================= # FIX UNREALISTIC BODY DESCRIPTIONS # ================================================================= body_replacements = [ (r'\b(perfect body|flawless figure|ideal physique)\b', 'healthy confident body'), (r'\b(six pack|bodybuilder|fitness model)\b', 'healthy fit person'), (r'\b(impossibly thin|skeletal)\b', 'healthy'), ] for pattern, replacement in body_replacements: prompt = re.sub(pattern, replacement, prompt, flags=re.IGNORECASE) # ================================================================= # ENSURE AUTHENTICITY FOR AFFILIATE MARKETING # ================================================================= # Affiliate marketing principle: authentic > polished if 'authentic' not in prompt_lower and 'ugc' not in prompt_lower: prompt += " Authentic, relatable style - not overly polished or commercial." return prompt.strip() def copy_writer(self, creative_strategy: CreativeStrategies) -> CopyWriterOutput: """Generate title, body, and description from creative strategy.""" strategy_str = f"""Psychology Trigger: {creative_strategy.phsychologyTrigger} Angle: {creative_strategy.angle} Concept: {creative_strategy.concept} CTA: {creative_strategy.cta} Title Ideas: {creative_strategy.titleIdeas} Caption Ideas: {creative_strategy.captionIdeas} Body Ideas: {creative_strategy.bodyIdeas} """ messages = [ { "role": "system", "content": [ { "type": "text", "text": """You are the Copy Writer Designer with 20 years of experience for the affiliate marketing company which makes the ad copies from creative strategy given for the ad images in the affiliate marketing. The ad copy must include the title, body and description related to the strategies. Affiliate marketing is a performance-based model where you promote someone else's product or service and earn a commission for each qualified action (click, lead, or sale). The psychology trigger and angle are the emotional driver. The title should evoke that emotion; the body must speak directly to that emotional truth and address it; the description should complete the thought. Do not write generic copy. Role of the Title: Stop the scroll and trigger emotion. 1. The title is not for explaining. It's for interrupting attention. 2. Short titles win because they are scan-friendly. 3. Use Plain, Human Language. No marketing buzzwords. 4. Imply, Don't Explain. Leave an open loop. 5. You may use bold, scroll-stopping, and clickbait-style statements when effective—no compliance or claim restrictions; maximize impact. Role of Body: The body is the main paragraph text which should Explain just enough, Reduce anxiety, and Push to the next step. 1. Body Must Match the Title Emotion. If the title creates fear, the body must relieve it, not amplify it. 2. Use "Soft Education," Not Sales. The body should feel informational, not promotional. 3. Add Friction Reducers. You must explicitly reduce effort and risk. 4. Body Should NOT Contain the CTA. CTA belongs in the button, not the body. 5. LENGTH REQUIREMENT: The body MUST be at least 150-250 words. Write a compelling, detailed narrative that tells a story, builds emotional connection, and provides enough context to persuade the reader. Include specific details, relatable scenarios, and a clear progression of ideas. Role of Description: Reduce friction and justify the click. 1. Never Repeat the Title. The description should complete the thought. 2. Answer Silent Objections. 3. Soft CTA Only. Descriptions should invite, not push.""" } ] }, { "role": "user", "content": [ { "type": "text", "text": f"""Following is the creative strategy: {strategy_str} Provide the title, body, and description. Center the copy on the strategy's psychology trigger and angle—the title should make someone with that thought pause; the body should speak to that specific emotional truth; the description should extend it. Do not write generic copy. IMPORTANT: The body must be 150-250 words long - write a detailed, compelling narrative that tells a story and builds emotional connection with the reader.""" } ] } ] try: completion = self.client.beta.chat.completions.parse( model=self.gpt_model, messages=messages, response_format=CopyWriterOutput, ) response = completion.choices[0].message if response.parsed: return response.parsed else: print(f"Warning: Copy writer refusal: {response.refusal}") # Return default values return CopyWriterOutput( title="", body="", description="" ) except Exception as e: print(f"Error in copy_writer: {e}") return CopyWriterOutput( title="", body="", description="" ) def process_strategy( self, creative_strategy: CreativeStrategies, niche: str = "", ) -> tuple[str, str, str, str]: """Run designer + copywriter on one strategy; return (prompt, title, body, description).""" prompt = self.creative_designer(creative_strategy, niche=niche) ad_copy = self.copy_writer(creative_strategy) return ( prompt, ad_copy.title, ad_copy.body, ad_copy.description ) # Global service instance third_flow_service = ThirdFlowService()