318 lines
12 KiB
Python
318 lines
12 KiB
Python
"""
|
|
Enhanced Blog Outline Generator
|
|
|
|
This module provides a sophisticated outline generation system that creates detailed,
|
|
well-structured outlines for blog posts based on user preferences and content requirements.
|
|
"""
|
|
|
|
import sys
|
|
from typing import Dict, List, Optional
|
|
from enum import Enum
|
|
from dataclasses import dataclass
|
|
from loguru import logger
|
|
import json
|
|
|
|
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
|
from lib.gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
|
|
|
|
logger.remove()
|
|
logger.add(sys.stdout,
|
|
colorize=True,
|
|
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}")
|
|
|
|
class ContentType(Enum):
|
|
"""Types of content that can be generated."""
|
|
HOW_TO = "how-to"
|
|
TUTORIAL = "tutorial"
|
|
LISTICLE = "listicle"
|
|
COMPARISON = "comparison"
|
|
CASE_STUDY = "case-study"
|
|
OPINION = "opinion"
|
|
NEWS = "news"
|
|
REVIEW = "review"
|
|
GUIDE = "guide"
|
|
|
|
class ContentDepth(Enum):
|
|
"""Depth levels for content coverage."""
|
|
BASIC = "basic"
|
|
INTERMEDIATE = "intermediate"
|
|
ADVANCED = "advanced"
|
|
EXPERT = "expert"
|
|
|
|
class OutlineStyle(Enum):
|
|
"""Styles for outline structure."""
|
|
TRADITIONAL = "traditional"
|
|
MODERN = "modern"
|
|
CONVERSATIONAL = "conversational"
|
|
ACADEMIC = "academic"
|
|
SEO_OPTIMIZED = "seo-optimized"
|
|
|
|
@dataclass
|
|
class OutlineConfig:
|
|
"""Configuration for outline generation."""
|
|
content_type: ContentType = ContentType.GUIDE
|
|
content_depth: ContentDepth = ContentDepth.INTERMEDIATE
|
|
outline_style: OutlineStyle = OutlineStyle.MODERN
|
|
target_word_count: int = 2000
|
|
num_main_sections: int = 5
|
|
num_subsections_per_section: int = 3
|
|
include_introduction: bool = True
|
|
include_conclusion: bool = True
|
|
include_faqs: bool = True
|
|
include_resources: bool = True
|
|
target_audience: str = "general"
|
|
language: str = "English"
|
|
keywords: List[str] = None
|
|
exclude_topics: List[str] = None
|
|
include_images: bool = True
|
|
image_style: str = "realistic"
|
|
image_engine: str = "Gemini-AI"
|
|
|
|
@dataclass
|
|
class SectionContent:
|
|
"""Content for a section including text and image."""
|
|
title: str
|
|
content: str
|
|
image_prompt: Optional[str] = None
|
|
image_path: Optional[str] = None
|
|
|
|
class BlogOutlineGenerator:
|
|
"""Enhanced blog outline generator with comprehensive controls."""
|
|
|
|
def __init__(self, config: Optional[OutlineConfig] = None):
|
|
"""Initialize the outline generator with optional configuration."""
|
|
self.config = config or OutlineConfig()
|
|
self.outline = {}
|
|
self.section_contents = {}
|
|
|
|
def generate_outline(self, topic: str) -> Dict[str, List[str]]:
|
|
"""Generate a blog outline based on the topic and configuration."""
|
|
try:
|
|
# Create a focused prompt for outline generation
|
|
prompt = f"""Generate a blog outline for topic: {topic}
|
|
|
|
Content Type: {self.config.content_type.value}
|
|
Target Audience: {self.config.target_audience}
|
|
Content Depth: {self.config.content_depth.value}
|
|
Style: {self.config.outline_style.value}
|
|
Word Count Target: {self.config.target_word_count}
|
|
Main Sections: {self.config.num_main_sections}
|
|
Subsections per Section: {self.config.num_subsections_per_section}
|
|
|
|
Requirements:
|
|
- Create exactly {self.config.num_main_sections} main sections
|
|
- Each section should have exactly {self.config.num_subsections_per_section} subsections
|
|
- Focus on {self.config.content_type.value} content style
|
|
- Target {self.config.target_audience} audience
|
|
- Maintain {self.config.content_depth.value} depth
|
|
- Follow {self.config.outline_style.value} style
|
|
- Optimize for {self.config.target_word_count} words total
|
|
|
|
IMPORTANT: You must return a valid JSON object with main sections as keys and lists of subsections as values.
|
|
Example format: {{"Section 1": ["Subsection 1.1", "Subsection 1.2"], "Section 2": ["Subsection 2.1", "Subsection 2.2"]}}
|
|
Do not include any additional text or explanations, only the JSON object."""
|
|
|
|
# Get outline from LLM
|
|
outline_json = llm_text_gen(prompt)
|
|
|
|
# Clean the response to ensure it's valid JSON
|
|
outline_json = outline_json.strip()
|
|
if not outline_json.startswith('{'):
|
|
outline_json = outline_json[outline_json.find('{'):]
|
|
if not outline_json.endswith('}'):
|
|
outline_json = outline_json[:outline_json.rfind('}')+1]
|
|
|
|
# Parse the outline
|
|
try:
|
|
outline = json.loads(outline_json)
|
|
except json.JSONDecodeError as e:
|
|
logger.error(f"JSON parsing error: {str(e)}")
|
|
logger.error(f"Raw response: {outline_json}")
|
|
# Fallback to a basic outline structure
|
|
outline = {
|
|
f"Section {i+1}": [f"Subsection {i+1}.{j+1}" for j in range(self.config.num_subsections_per_section)]
|
|
for i in range(self.config.num_main_sections)
|
|
}
|
|
|
|
# Add introduction and conclusion if configured
|
|
if self.config.include_introduction:
|
|
outline = {"Introduction": ["Overview", "Importance", "What to Expect"]} | outline
|
|
|
|
if self.config.include_conclusion:
|
|
outline["Conclusion"] = ["Summary", "Key Takeaways", "Next Steps"]
|
|
|
|
# Add FAQs if configured
|
|
if self.config.include_faqs:
|
|
# Generate topic-specific FAQs
|
|
faq_prompt = f"""Generate 3 specific and relevant FAQ questions for a blog post about: {topic}
|
|
|
|
Content Type: {self.config.content_type.value}
|
|
Target Audience: {self.config.target_audience}
|
|
Content Depth: {self.config.content_depth.value}
|
|
|
|
Requirements:
|
|
- Questions should be specific to the topic
|
|
- Cover common concerns and important aspects
|
|
- Be relevant to the target audience
|
|
- Include both basic and advanced questions
|
|
|
|
Format: Return only a JSON array of 3 questions.
|
|
Example format: ["Question 1?", "Question 2?", "Question 3?"]"""
|
|
|
|
try:
|
|
faq_json = llm_text_gen(faq_prompt)
|
|
faq_json = faq_json.strip()
|
|
if not faq_json.startswith('['):
|
|
faq_json = faq_json[faq_json.find('['):]
|
|
if not faq_json.endswith(']'):
|
|
faq_json = faq_json[:faq_json.rfind(']')+1]
|
|
|
|
faqs = json.loads(faq_json)
|
|
outline["Frequently Asked Questions"] = faqs
|
|
except Exception as e:
|
|
logger.error(f"Error generating FAQs: {str(e)}")
|
|
outline["Frequently Asked Questions"] = [
|
|
f"Common Question about {topic} 1",
|
|
f"Common Question about {topic} 2",
|
|
f"Common Question about {topic} 3"
|
|
]
|
|
|
|
# Add resources if configured
|
|
if self.config.include_resources:
|
|
outline["Additional Resources"] = [
|
|
"Further Reading",
|
|
"Tools and References",
|
|
"Related Topics"
|
|
]
|
|
|
|
return outline
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error generating outline: {str(e)}")
|
|
return {}
|
|
|
|
def generate_section_content(self, section: str, subsections: List[str]) -> Optional[SectionContent]:
|
|
"""Generate content for a section."""
|
|
try:
|
|
# Create a focused prompt for content generation
|
|
prompt = f"""Generate content for section: {section}
|
|
|
|
Subsections: {', '.join(subsections)}
|
|
Content Type: {self.config.content_type.value}
|
|
Target Audience: {self.config.target_audience}
|
|
Content Depth: {self.config.content_depth.value}
|
|
Style: {self.config.outline_style.value}
|
|
Word Count Target: {self.config.target_word_count // self.config.num_main_sections}
|
|
|
|
Requirements:
|
|
- Write content for each subsection
|
|
- Maintain {self.config.content_depth.value} depth
|
|
- Target {self.config.target_audience} audience
|
|
- Follow {self.config.outline_style.value} style
|
|
- Optimize for {self.config.target_word_count // self.config.num_main_sections} words
|
|
- Include relevant examples and data points
|
|
- Use clear, engaging language
|
|
|
|
Format: Return only a JSON object with 'content' and 'image_prompt' fields.
|
|
Example format: {{"content": "Section content here...", "image_prompt": "Image description here..."}}"""
|
|
|
|
# Get content from LLM
|
|
content_json = llm_text_gen(prompt)
|
|
content_data = json.loads(content_json)
|
|
|
|
# Generate image if configured
|
|
image_path = None
|
|
if self.config.include_images:
|
|
image_path = self.generate_section_image(section)
|
|
|
|
return SectionContent(
|
|
title=section,
|
|
content=content_data["content"],
|
|
image_prompt=content_data.get("image_prompt"),
|
|
image_path=image_path
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error generating content for section {section}: {str(e)}")
|
|
return None
|
|
|
|
def generate_section_image(self, section: str) -> Optional[str]:
|
|
"""Generate an image for a section."""
|
|
try:
|
|
# Create a focused prompt for image generation
|
|
prompt = f"""Generate an image prompt for section: {section}
|
|
|
|
Style: {self.config.image_style}
|
|
Engine: {self.config.image_engine}
|
|
Content Type: {self.config.content_type.value}
|
|
Target Audience: {self.config.target_audience}
|
|
|
|
Requirements:
|
|
- Create a {self.config.image_style} style image
|
|
- Optimize for {self.config.image_engine} engine
|
|
- Match {self.config.content_type.value} content type
|
|
- Appeal to {self.config.target_audience} audience
|
|
- Be visually engaging and relevant
|
|
|
|
Format: Return only a JSON object with an 'image_prompt' field.
|
|
Example format: {{"image_prompt": "Detailed image description here..."}}"""
|
|
|
|
# Get image prompt from LLM
|
|
prompt_json = llm_text_gen(prompt)
|
|
prompt_data = json.loads(prompt_json)
|
|
|
|
# Generate image using the specified engine
|
|
if self.config.image_engine == "Gemini-AI":
|
|
image_path = generate_gemini_image(prompt_data["image_prompt"])
|
|
elif self.config.image_engine == "Dalle3":
|
|
image_path = generate_dalle_image(prompt_data["image_prompt"])
|
|
else: # Stability-AI
|
|
image_path = generate_stability_image(prompt_data["image_prompt"])
|
|
|
|
return image_path
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error generating image for section {section}: {str(e)}")
|
|
return None
|
|
|
|
def to_markdown(self) -> str:
|
|
"""Convert outline to markdown format with content and images."""
|
|
markdown = f"# {self.outline.get('Introduction', [''])[0]}\n\n"
|
|
|
|
for section, subsections in self.outline.items():
|
|
if section not in ["Introduction", "Conclusion", "FAQs", "Additional Resources"]:
|
|
markdown += f"## {section}\n\n"
|
|
|
|
# Add section content if available
|
|
if section in self.section_contents:
|
|
content = self.section_contents[section]
|
|
markdown += f"{content.content}\n\n"
|
|
|
|
# Add image if available
|
|
if content.image_path:
|
|
markdown += f"\n\n"
|
|
|
|
# Add subsections
|
|
for subsection in subsections:
|
|
markdown += f"- {subsection}\n"
|
|
markdown += "\n"
|
|
|
|
if "Conclusion" in self.outline:
|
|
markdown += "## Conclusion\n\n"
|
|
for subsection in self.outline["Conclusion"]:
|
|
markdown += f"- {subsection}\n"
|
|
markdown += "\n"
|
|
|
|
if "FAQs" in self.outline:
|
|
markdown += "## Frequently Asked Questions\n\n"
|
|
for faq in self.outline["FAQs"]:
|
|
markdown += f"- {faq}\n"
|
|
markdown += "\n"
|
|
|
|
if "Additional Resources" in self.outline:
|
|
markdown += "## Additional Resources\n\n"
|
|
for resource in self.outline["Additional Resources"]:
|
|
markdown += f"- {resource}\n"
|
|
|
|
return markdown
|