""" Title Generator - Handles title generation and formatting for blog outlines. Extracts content angles from research data and combines them with AI-generated titles. """ from typing import List from loguru import logger class TitleGenerator: """Handles title generation, formatting, and combination logic.""" def __init__(self): """Initialize the title generator.""" pass def extract_content_angle_titles(self, research) -> List[str]: """ Extract content angles from research data and convert them to blog titles. Args: research: BlogResearchResponse object containing suggested_angles Returns: List of title-formatted content angles """ if not research or not hasattr(research, 'suggested_angles'): return [] content_angles = research.suggested_angles or [] if not content_angles: return [] # Convert content angles to title format title_formatted_angles = [] for angle in content_angles: if isinstance(angle, str) and angle.strip(): # Clean and format the angle as a title formatted_angle = self._format_angle_as_title(angle.strip()) if formatted_angle and formatted_angle not in title_formatted_angles: title_formatted_angles.append(formatted_angle) logger.info(f"Extracted {len(title_formatted_angles)} content angle titles from research data") return title_formatted_angles def _format_angle_as_title(self, angle: str) -> str: """ Format a content angle as a proper blog title. Args: angle: Raw content angle string Returns: Formatted title string """ if not angle or len(angle.strip()) < 10: return "" cleaned_angle = angle.strip() # Use sentence case: capitalize first letter, rest as-is if cleaned_angle: cleaned_angle = cleaned_angle[0].upper() + cleaned_angle[1:] # Limit length to reasonable blog title size if len(cleaned_angle) > 120: cleaned_angle = cleaned_angle[:117] + "..." return cleaned_angle def combine_title_options(self, ai_titles: List[str], content_angle_titles: List[str], primary_keywords: List[str], research_topic: str = "") -> List[str]: """ Combine AI-generated titles with content angle titles, ensuring variety and quality. AI titles (proper SEO titles generated by LLM) take priority. Content angle titles (long-format descriptions) are used as fallback. The research topic is the last resort when nothing else exists. Args: ai_titles: AI-generated title options (proper blog titles, 50-65 chars) content_angle_titles: Titles derived from content angles (longer, descriptive) primary_keywords: Primary keywords for fallback generation research_topic: Original user research topic as ultimate fallback Returns: Combined list of title options (max 6 total) """ all_titles = [] # 1. AI-generated titles first (proper SEO titles from LLM) for title in ai_titles: if title and title not in all_titles: all_titles.append(title) # 2. Content angle titles as fallback (research-based, but verbose) for title in content_angle_titles[:3]: if title and title not in all_titles: all_titles.append(title) # 3. Research topic as last resort when nothing was generated if not all_titles and research_topic: all_titles.append(research_topic) # 4. Primary keyword fallback as absolute last resort if not all_titles and primary_keywords: kw = primary_keywords[0] all_titles.append(kw) # Limit to 6 titles maximum for UI usability final_titles = all_titles[:6] logger.info(f"Combined title options: {len(final_titles)} total (AI: {len(ai_titles)}, Content angles: {len(content_angle_titles)})") return final_titles def generate_fallback_titles(self, primary_keywords: List[str]) -> List[str]: """Generate fallback titles when AI generation fails.""" from datetime import datetime primary_keyword = primary_keywords[0] if primary_keywords else "Topic" return [ f"The Complete Guide to {primary_keyword}", f"{primary_keyword}: Everything You Need to Know", f"How to Master {primary_keyword} in {datetime.now().year}" ]