""" SEO Title Generator - Specialized service for generating SEO-optimized blog titles. Generates 5 premium SEO-optimized titles using research data and outline context. """ from typing import Dict, Any, List from loguru import logger from models.blog_models import BlogResearchResponse, BlogOutlineSection class SEOTitleGenerator: """Generates SEO-optimized blog titles using research and outline data.""" def __init__(self): """Initialize the SEO title generator.""" pass def build_title_prompt( self, research: BlogResearchResponse, outline: List[BlogOutlineSection], primary_keywords: List[str], secondary_keywords: List[str], content_angles: List[str], search_intent: str, word_count: int = 1500 ) -> str: """Build a specialized prompt for SEO title generation.""" # Extract key research insights keyword_analysis = research.keyword_analysis or {} competitor_analysis = research.competitor_analysis or {} primary_kw_text = ', '.join(primary_keywords) if primary_keywords else "the target topic" secondary_kw_text = ', '.join(secondary_keywords) if secondary_keywords else "None provided" long_tail_text = ', '.join(keyword_analysis.get('long_tail', [])) if keyword_analysis else "None discovered" semantic_text = ', '.join(keyword_analysis.get('semantic_keywords', [])) if keyword_analysis else "None discovered" trending_text = ', '.join(keyword_analysis.get('trending_terms', [])) if keyword_analysis else "None discovered" content_gap_text = ', '.join(keyword_analysis.get('content_gaps', [])) if keyword_analysis else "None identified" content_angle_text = ', '.join(content_angles) if content_angles else "No explicit angles provided" # Extract outline structure summary outline_summary = [] for i, section in enumerate(outline[:5], 1): # Limit to first 5 sections for context outline_summary.append(f"{i}. {section.heading}") if section.subheadings: outline_summary.append(f" Subtopics: {', '.join(section.subheadings[:3])}") outline_text = '\n'.join(outline_summary) if outline_summary else "No outline available" return f"""Generate exactly 5 SEO-optimized blog titles for: {primary_kw_text} RESEARCH CONTEXT: Primary Keywords: {primary_kw_text} Secondary Keywords: {secondary_kw_text} Long-tail Keywords: {long_tail_text} Semantic Keywords: {semantic_text} Trending Terms: {trending_text} Content Gaps: {content_gap_text} Search Intent: {search_intent} Content Angles: {content_angle_text} OUTLINE STRUCTURE: {outline_text} COMPETITIVE INTELLIGENCE: Top Competitors: {', '.join(competitor_analysis.get('top_competitors', [])) if competitor_analysis else 'Not available'} Market Opportunities: {', '.join(competitor_analysis.get('opportunities', [])) if competitor_analysis else 'Not available'} SEO REQUIREMENTS: - Each title must be 50-65 characters (optimal for search engine display) - Include the primary keyword within the first 55 characters - Highlight a unique value proposition from the research angles - Use power words that drive clicks (e.g., "Ultimate", "Complete", "Essential", "Proven") - Avoid generic phrasing - be specific and benefit-focused - Target the search intent: {search_intent} - Ensure titles are compelling and click-worthy Return ONLY a JSON array of exactly 5 titles: [ "Title 1 (50-65 chars)", "Title 2 (50-65 chars)", "Title 3 (50-65 chars)", "Title 4 (50-65 chars)", "Title 5 (50-65 chars)" ]""" def get_title_schema(self) -> Dict[str, Any]: """Get the JSON schema for title generation.""" return { "type": "array", "items": { "type": "string", "minLength": 50, "maxLength": 65 }, "minItems": 5, "maxItems": 5 } async def generate_seo_titles( self, research: BlogResearchResponse, outline: List[BlogOutlineSection], primary_keywords: List[str], secondary_keywords: List[str], content_angles: List[str], search_intent: str, word_count: int, user_id: str ) -> List[str]: """Generate SEO-optimized titles using research and outline data. Args: research: Research data with keywords and insights outline: Blog outline sections primary_keywords: Primary keywords for the blog secondary_keywords: Secondary keywords content_angles: Content angles from research search_intent: Search intent (informational, commercial, etc.) word_count: Target word count user_id: User ID for API calls Returns: List of 5 SEO-optimized titles """ from services.llm_providers.main_text_generation import llm_text_gen if not user_id: raise ValueError("user_id is required for title generation") # Build specialized prompt prompt = self.build_title_prompt( research=research, outline=outline, primary_keywords=primary_keywords, secondary_keywords=secondary_keywords, content_angles=content_angles, search_intent=search_intent, word_count=word_count ) # Get schema schema = self.get_title_schema() logger.info(f"Generating SEO-optimized titles for user {user_id}") try: # Generate titles using structured JSON response result = llm_text_gen( prompt=prompt, json_struct=schema, system_prompt="You are an expert SEO content strategist specializing in creating compelling, search-optimized blog titles.", user_id=user_id ) # Handle response - could be array directly or wrapped in dict if isinstance(result, list): titles = result elif isinstance(result, dict): # Try common keys titles = result.get('titles', result.get('title_options', result.get('options', []))) if not titles and isinstance(result.get('response'), list): titles = result['response'] else: logger.warning(f"Unexpected title generation result type: {type(result)}") titles = [] # Validate and clean titles cleaned_titles = [] for title in titles: if isinstance(title, str) and len(title.strip()) >= 30: # Minimum reasonable length cleaned = title.strip() # Ensure it's within reasonable bounds (allow slight overflow for quality) if len(cleaned) <= 70: # Allow slight overflow for quality cleaned_titles.append(cleaned) # Ensure we have exactly 5 titles if len(cleaned_titles) < 5: logger.warning(f"Generated only {len(cleaned_titles)} titles, expected 5") # Pad with placeholder if needed (shouldn't happen with proper schema) while len(cleaned_titles) < 5: cleaned_titles.append(f"{primary_keywords[0] if primary_keywords else 'Blog'} - Comprehensive Guide") # Return exactly 5 titles return cleaned_titles[:5] except Exception as e: logger.error(f"Failed to generate SEO titles: {e}") # Fallback: generate simple titles from keywords fallback_titles = [] primary = primary_keywords[0] if primary_keywords else "Blog Post" for i in range(5): fallback_titles.append(f"{primary}: Complete Guide {i+1}") return fallback_titles