125 lines
4.8 KiB
Python
125 lines
4.8 KiB
Python
"""
|
|
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}"
|
|
]
|