alwrity chatbot assistant, content scheduler, and content repurposing
This commit is contained in:
@@ -9,7 +9,7 @@ parent_dir = str(Path(__file__).parent.parent.parent.parent)
|
||||
if parent_dir not in sys.path:
|
||||
sys.path.append(parent_dir)
|
||||
|
||||
from lib.ai_seo_tools.content_calendar.models.calendar import ContentType, ContentItem, Platform
|
||||
from lib.database.models import ContentType, ContentItem, Platform
|
||||
from lib.ai_seo_tools.content_calendar.utils.error_handling import handle_calendar_error
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
from lib.ai_seo_tools.content_gap_analysis.main import ContentGapAnalysis
|
||||
@@ -570,22 +570,6 @@ class AIGenerator:
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Generate AI content suggestions based on input parameters.
|
||||
|
||||
Args:
|
||||
content_type: Type of content to generate
|
||||
topic: Main topic or subject
|
||||
audience: Target audience
|
||||
goals: List of content goals
|
||||
tone: Desired tone
|
||||
length: Content length
|
||||
model_settings: AI model settings
|
||||
style_preferences: Style preferences
|
||||
seo_preferences: SEO preferences
|
||||
platform_settings: Platform-specific settings
|
||||
platform: Target platform
|
||||
|
||||
Returns:
|
||||
List of generated content suggestions
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Generating AI suggestions for topic: {topic}")
|
||||
@@ -601,14 +585,14 @@ Tone: {tone}
|
||||
Length: {length}
|
||||
|
||||
Style Preferences:
|
||||
- Creativity Level: {model_settings['Creativity Level']}
|
||||
- Formality Level: {model_settings['Formality Level']}
|
||||
- Creativity Level: {model_settings.get('Creativity Level', 'medium')}
|
||||
- Formality Level: {model_settings.get('Formality Level', 'professional')}
|
||||
- Style Elements: {', '.join(style_preferences)}
|
||||
|
||||
SEO Preferences:
|
||||
- Keyword Density: {seo_preferences['Keyword Density']}%
|
||||
- Internal Linking: {'Enabled' if seo_preferences['Internal Linking'] else 'Disabled'}
|
||||
- External Linking: {'Enabled' if seo_preferences['External Linking'] else 'Disabled'}
|
||||
- Keyword Density: {seo_preferences.get('Keyword Density', 2)}%
|
||||
- Internal Linking: {'Enabled' if seo_preferences.get('Internal Linking', True) else 'Disabled'}
|
||||
- External Linking: {'Enabled' if seo_preferences.get('External Linking', True) else 'Disabled'}
|
||||
|
||||
Platform Settings:
|
||||
- Platform: {platform}
|
||||
@@ -645,55 +629,20 @@ Please generate 3 different content suggestions. Format your response as a valid
|
||||
|
||||
IMPORTANT: Your response must be a valid JSON object. Do not include any text before or after the JSON object."""
|
||||
|
||||
# Define JSON structure for validation
|
||||
json_struct = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"suggestions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"},
|
||||
"introduction": {"type": "string"},
|
||||
"key_points": {"type": "array", "items": {"type": "string"}},
|
||||
"main_sections": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"},
|
||||
"content": {"type": "string"},
|
||||
"engagement_elements": {"type": "array", "items": {"type": "string"}},
|
||||
"seo_elements": {"type": "array", "items": {"type": "string"}}
|
||||
}
|
||||
}
|
||||
},
|
||||
"conclusion": {"type": "string"},
|
||||
"seo_elements": {"type": "array", "items": {"type": "string"}},
|
||||
"platform_optimizations": {"type": "array", "items": {"type": "string"}},
|
||||
"engagement_strategies": {"type": "array", "items": {"type": "string"}},
|
||||
"content_metrics": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"estimated_read_time": {"type": "string"},
|
||||
"word_count": {"type": "number"},
|
||||
"keyword_density": {"type": "number"},
|
||||
"engagement_score": {"type": "number"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Generate content using llm_text_gen with JSON structure
|
||||
generated_content = llm_text_gen(prompt, json_struct=json_struct)
|
||||
# Generate content using llm_text_gen
|
||||
generated_content = llm_text_gen(
|
||||
prompt=prompt,
|
||||
max_tokens=1000,
|
||||
temperature=0.7,
|
||||
top_p=0.9,
|
||||
frequency_penalty=0.5,
|
||||
presence_penalty=0.5
|
||||
)
|
||||
|
||||
if not generated_content:
|
||||
raise ValueError("Failed to generate content suggestions")
|
||||
|
||||
self.logger.error("No content generated from AI model")
|
||||
return []
|
||||
|
||||
# Parse the generated content
|
||||
try:
|
||||
# If generated_content is already a dict, use it directly
|
||||
@@ -703,6 +652,10 @@ IMPORTANT: Your response must be a valid JSON object. Do not include any text be
|
||||
# Try to parse as JSON string
|
||||
content_data = json.loads(generated_content)
|
||||
|
||||
if not content_data or 'suggestions' not in content_data:
|
||||
self.logger.error("Invalid content structure in AI response")
|
||||
return []
|
||||
|
||||
return self._format_suggestions(
|
||||
content_data,
|
||||
content_type,
|
||||
@@ -725,6 +678,9 @@ IMPORTANT: Your response must be a valid JSON object. Do not include any text be
|
||||
if start >= 0 and end > start:
|
||||
json_str = generated_content[start:end]
|
||||
content_data = json.loads(json_str)
|
||||
if not content_data or 'suggestions' not in content_data:
|
||||
self.logger.error("Invalid content structure in extracted JSON")
|
||||
return []
|
||||
return self._format_suggestions(
|
||||
content_data,
|
||||
content_type,
|
||||
@@ -738,11 +694,11 @@ IMPORTANT: Your response must be a valid JSON object. Do not include any text be
|
||||
)
|
||||
except Exception as e2:
|
||||
self.logger.error(f"Error extracting JSON from response: {str(e2)}")
|
||||
raise ValueError("Failed to parse generated content")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error generating AI suggestions: {str(e)}", exc_info=True)
|
||||
raise
|
||||
return []
|
||||
|
||||
def _format_suggestions(
|
||||
self,
|
||||
|
||||
@@ -4,10 +4,9 @@ import logging
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
|
||||
from lib.database.models import ContentItem, ContentType, Platform, get_engine, get_session, init_db
|
||||
from ..integrations.seo_tools import SEOToolsIntegration
|
||||
from ..integrations.gap_analyzer import GapAnalyzerIntegration
|
||||
from ..models.calendar import Calendar, ContentItem
|
||||
from ..utils.date_utils import calculate_publish_dates
|
||||
from ..utils.error_handling import handle_calendar_error
|
||||
|
||||
@@ -21,24 +20,22 @@ logging.basicConfig(
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CALENDAR_JSON_PATH = "calendar_data.json"
|
||||
engine = get_engine()
|
||||
init_db(engine)
|
||||
session = get_session(engine)
|
||||
|
||||
class CalendarManager:
|
||||
"""
|
||||
Main calendar management system that coordinates content planning,
|
||||
scheduling, and optimization.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize calendar manager."""
|
||||
self.logger = logging.getLogger('content_calendar.manager')
|
||||
self.logger.info("Initializing CalendarManager")
|
||||
|
||||
self.seo_tools = SEOToolsIntegration()
|
||||
self.gap_analyzer = GapAnalyzerIntegration()
|
||||
self._calendar: Optional[Calendar] = None
|
||||
self.logger.info("CalendarManager initialized successfully")
|
||||
|
||||
|
||||
@handle_calendar_error
|
||||
def create_calendar(
|
||||
self,
|
||||
@@ -46,136 +43,107 @@ class CalendarManager:
|
||||
duration: str, # 'weekly', 'monthly', 'quarterly'
|
||||
platforms: List[str],
|
||||
website_url: str
|
||||
) -> Calendar:
|
||||
"""
|
||||
Create a new content calendar based on content gap analysis and SEO requirements.
|
||||
|
||||
Args:
|
||||
start_date: When the calendar should begin
|
||||
duration: How long the calendar should span
|
||||
platforms: List of platforms to create content for
|
||||
website_url: URL of the website to analyze
|
||||
|
||||
Returns:
|
||||
Calendar object containing the content schedule
|
||||
"""
|
||||
) -> List[ContentItem]:
|
||||
self.logger.info(f"Creating new calendar for {website_url}")
|
||||
self.logger.debug(f"Parameters: start_date={start_date}, duration={duration}, platforms={platforms}")
|
||||
|
||||
try:
|
||||
# 1. Analyze content gaps
|
||||
self.logger.info("Analyzing content gaps")
|
||||
gap_analysis = self.gap_analyzer.analyze_gaps(website_url)
|
||||
|
||||
# 2. Generate topics based on gaps
|
||||
self.logger.info("Generating topics from gap analysis")
|
||||
topics = self._generate_topics(gap_analysis, platforms)
|
||||
|
||||
# 3. Calculate publish dates
|
||||
self.logger.info("Calculating publish dates")
|
||||
schedule = calculate_publish_dates(
|
||||
topics=topics,
|
||||
start_date=start_date,
|
||||
duration=duration
|
||||
)
|
||||
|
||||
# 4. Create calendar
|
||||
self.logger.info("Creating calendar object")
|
||||
self._calendar = Calendar(
|
||||
start_date=start_date,
|
||||
duration=duration,
|
||||
platforms=platforms,
|
||||
schedule=schedule
|
||||
)
|
||||
|
||||
self.logger.info("Calendar created successfully")
|
||||
return self._calendar
|
||||
|
||||
# Add to DB
|
||||
for topic in schedule:
|
||||
session.add(topic)
|
||||
session.commit()
|
||||
self.logger.info("Calendar created and content scheduled in DB successfully")
|
||||
return schedule
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating calendar: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
def _generate_topics(
|
||||
self,
|
||||
gap_analysis: Dict[str, Any],
|
||||
platforms: List[str]
|
||||
) -> List[ContentItem]:
|
||||
"""
|
||||
Generate content topics based on gap analysis and platform requirements.
|
||||
"""
|
||||
topics = []
|
||||
|
||||
for gap in gap_analysis['gaps']:
|
||||
# Generate topic using AI
|
||||
topic = self._generate_topic_from_gap(gap, platforms)
|
||||
|
||||
# Optimize for SEO
|
||||
optimized_topic = self._optimize_topic(topic)
|
||||
|
||||
topics.append(optimized_topic)
|
||||
|
||||
return topics
|
||||
|
||||
|
||||
def _generate_topic_from_gap(
|
||||
self,
|
||||
gap: Dict[str, Any],
|
||||
platforms: List[str]
|
||||
) -> ContentItem:
|
||||
"""
|
||||
Generate a specific topic based on a content gap.
|
||||
"""
|
||||
# Use existing AI tools to generate topic
|
||||
topic_data = {
|
||||
'title': self._generate_title(gap),
|
||||
'description': self._generate_description(gap),
|
||||
'keywords': gap.get('keywords', []),
|
||||
'platforms': platforms,
|
||||
'content_type': self._determine_content_type(gap, platforms)
|
||||
'content_type': self._determine_content_type(gap, platforms),
|
||||
'publish_date': datetime.now(),
|
||||
'status': 'Draft',
|
||||
'author': None,
|
||||
'tags': [],
|
||||
'notes': None,
|
||||
'seo_data': {}
|
||||
}
|
||||
|
||||
return ContentItem(**topic_data)
|
||||
|
||||
|
||||
def _optimize_topic(self, topic: ContentItem) -> ContentItem:
|
||||
"""
|
||||
Optimize a topic for SEO using existing tools.
|
||||
"""
|
||||
# Optimize title
|
||||
topic.title = self.seo_tools.optimize_title(topic.title)
|
||||
|
||||
# Generate meta description
|
||||
topic.meta_description = self.seo_tools.generate_meta_description(
|
||||
topic.description
|
||||
)
|
||||
|
||||
# Add structured data
|
||||
topic.structured_data = self.seo_tools.generate_structured_data(
|
||||
topic.content_type
|
||||
)
|
||||
|
||||
topic.seo_data['meta_description'] = self.seo_tools.generate_meta_description(topic.description)
|
||||
topic.seo_data['structured_data'] = self.seo_tools.generate_structured_data(topic.content_type)
|
||||
return topic
|
||||
|
||||
def get_calendar(self) -> Optional[Calendar]:
|
||||
|
||||
def get_all_content(self) -> List[ContentItem]:
|
||||
return session.query(ContentItem).all()
|
||||
|
||||
def remove_content(self, content_id):
|
||||
content = session.query(ContentItem).get(content_id)
|
||||
if content:
|
||||
session.delete(content)
|
||||
session.commit()
|
||||
|
||||
def update_content(self, content_id, **kwargs):
|
||||
content = session.query(ContentItem).get(content_id)
|
||||
if content:
|
||||
for key, value in kwargs.items():
|
||||
setattr(content, key, value)
|
||||
session.commit()
|
||||
|
||||
def get_calendar(self) -> Optional[List[ContentItem]]:
|
||||
"""
|
||||
Get the current calendar.
|
||||
"""
|
||||
self.logger.debug("Getting current calendar")
|
||||
return self._calendar
|
||||
return self.get_all_content()
|
||||
|
||||
def update_calendar(self, calendar: Calendar) -> None:
|
||||
def update_calendar(self, calendar: List[ContentItem]) -> None:
|
||||
"""
|
||||
Update the current calendar.
|
||||
"""
|
||||
self._calendar = calendar
|
||||
self.get_all_content()
|
||||
for content in calendar:
|
||||
session.add(content)
|
||||
session.commit()
|
||||
|
||||
def export_calendar(self) -> Optional[Dict[str, Any]]:
|
||||
"""Export the current calendar."""
|
||||
self.logger.info("Exporting calendar")
|
||||
if not self._calendar:
|
||||
calendar = self.get_calendar()
|
||||
if not calendar:
|
||||
self.logger.warning("No calendar to export")
|
||||
return None
|
||||
|
||||
try:
|
||||
calendar_data = self._calendar.export()
|
||||
calendar_data = [content.to_dict() for content in calendar]
|
||||
self.logger.info("Calendar exported successfully")
|
||||
return calendar_data
|
||||
except Exception as e:
|
||||
@@ -185,12 +153,11 @@ class CalendarManager:
|
||||
def save_calendar_to_json(self):
|
||||
calendar = self.get_calendar()
|
||||
if calendar:
|
||||
with open(CALENDAR_JSON_PATH, "w") as f:
|
||||
json.dump(calendar.to_dict(), f, indent=2, default=str)
|
||||
with open("calendar_data.json", "w") as f:
|
||||
json.dump(calendar, f, indent=2, default=str)
|
||||
|
||||
def load_calendar_from_json(self):
|
||||
from lib.ai_seo_tools.content_calendar.models.calendar import Calendar
|
||||
if os.path.exists(CALENDAR_JSON_PATH):
|
||||
with open(CALENDAR_JSON_PATH, "r") as f:
|
||||
if os.path.exists("calendar_data.json"):
|
||||
with open("calendar_data.json", "r") as f:
|
||||
data = json.load(f)
|
||||
self._calendar = Calendar.from_dict(data)
|
||||
self.update_calendar(data)
|
||||
@@ -8,7 +8,7 @@ parent_dir = str(Path(__file__).parent.parent.parent.parent)
|
||||
if parent_dir not in sys.path:
|
||||
sys.path.append(parent_dir)
|
||||
|
||||
from lib.ai_seo_tools.content_calendar.models.calendar import ContentType, ContentItem, Platform
|
||||
from lib.database.models import ContentType, ContentItem, Platform
|
||||
from lib.ai_seo_tools.content_calendar.utils.error_handling import handle_calendar_error
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
from lib.ai_seo_tools.content_gap_analysis.main import ContentGapAnalysis
|
||||
|
||||
@@ -2,23 +2,25 @@ from typing import Dict, List, Any, Optional
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Add parent directory to path to import existing tools
|
||||
parent_dir = str(Path(__file__).parent.parent.parent.parent)
|
||||
if parent_dir not in sys.path:
|
||||
sys.path.append(parent_dir)
|
||||
|
||||
from ..models.calendar import ContentItem, ContentType
|
||||
from lib.database.models import ContentItem, ContentType, Platform
|
||||
from ..utils.error_handling import handle_calendar_error
|
||||
from lib.ai_seo_tools.content_gap_analysis.main import ContentGapAnalysis
|
||||
from lib.ai_seo_tools.content_title_generator import ai_title_generator
|
||||
from lib.ai_seo_tools.meta_desc_generator import metadesc_generator_main
|
||||
from lib.ai_seo_tools.content_calendar.core.content_repurposer import SmartContentRepurposingEngine
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ContentGenerator:
|
||||
"""
|
||||
AI-powered content generation for content briefs.
|
||||
Enhanced content generator with smart repurposing capabilities.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
@@ -26,6 +28,8 @@ class ContentGenerator:
|
||||
self.logger.info("Initializing ContentGenerator")
|
||||
self._setup_logging()
|
||||
self._load_ai_tools()
|
||||
# Initialize the Smart Content Repurposing Engine
|
||||
self.repurposing_engine = SmartContentRepurposingEngine()
|
||||
|
||||
def _setup_logging(self):
|
||||
"""Configure logging for content generator."""
|
||||
@@ -320,4 +324,303 @@ class ContentGenerator:
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error generating variation: {str(e)}")
|
||||
return {}
|
||||
return {}
|
||||
|
||||
@handle_calendar_error
|
||||
def repurpose_content_for_platforms(
|
||||
self,
|
||||
content_item: ContentItem,
|
||||
target_platforms: List[Platform],
|
||||
strategy: str = 'adaptive'
|
||||
) -> List[ContentItem]:
|
||||
"""
|
||||
Repurpose existing content for multiple platforms using the Smart Content Repurposing Engine.
|
||||
|
||||
Args:
|
||||
content_item: Original content to repurpose
|
||||
target_platforms: List of platforms to create content for
|
||||
strategy: Repurposing strategy ('adaptive', 'atomic', 'series')
|
||||
|
||||
Returns:
|
||||
List of repurposed content items optimized for each platform
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Repurposing content '{content_item.title}' for {len(target_platforms)} platforms")
|
||||
|
||||
# Use the repurposing engine to create platform-specific content
|
||||
repurposed_content = self.repurposing_engine.repurpose_single_content(
|
||||
content=content_item,
|
||||
target_platforms=target_platforms,
|
||||
strategy=strategy
|
||||
)
|
||||
|
||||
self.logger.info(f"Successfully created {len(repurposed_content)} repurposed content pieces")
|
||||
return repurposed_content
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error repurposing content: {str(e)}")
|
||||
return []
|
||||
|
||||
@handle_calendar_error
|
||||
def create_content_series_across_platforms(
|
||||
self,
|
||||
source_content: ContentItem,
|
||||
platforms: List[Platform],
|
||||
series_type: str = 'progressive_disclosure'
|
||||
) -> Dict[str, List[ContentItem]]:
|
||||
"""
|
||||
Create a cross-platform content series with progressive disclosure strategy.
|
||||
|
||||
Args:
|
||||
source_content: Original comprehensive content
|
||||
platforms: Target platforms for the series
|
||||
series_type: Type of series ('progressive_disclosure', 'platform_native')
|
||||
|
||||
Returns:
|
||||
Dictionary mapping platforms to their content pieces
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Creating cross-platform series for '{source_content.title}'")
|
||||
|
||||
# Use the repurposing engine to create a content series
|
||||
series_content = self.repurposing_engine.create_content_series(
|
||||
content=source_content,
|
||||
platforms=platforms,
|
||||
series_type=series_type
|
||||
)
|
||||
|
||||
total_pieces = sum(len(pieces) for pieces in series_content.values())
|
||||
self.logger.info(f"Successfully created series with {total_pieces} pieces across {len(series_content)} platforms")
|
||||
|
||||
return series_content
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating content series: {str(e)}")
|
||||
return {}
|
||||
|
||||
@handle_calendar_error
|
||||
def analyze_content_for_repurposing(
|
||||
self,
|
||||
content_item: ContentItem,
|
||||
available_platforms: List[Platform]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyze content and get AI-powered repurposing suggestions.
|
||||
|
||||
Args:
|
||||
content_item: Content to analyze
|
||||
available_platforms: Available platforms for repurposing
|
||||
|
||||
Returns:
|
||||
Dictionary containing repurposing suggestions and analysis
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Analyzing content '{content_item.title}' for repurposing opportunities")
|
||||
|
||||
# Get repurposing suggestions from the engine
|
||||
suggestions = self.repurposing_engine.get_repurposing_suggestions(
|
||||
content=content_item,
|
||||
available_platforms=available_platforms
|
||||
)
|
||||
|
||||
# Add content analysis
|
||||
content_text = content_item.description or content_item.notes or ""
|
||||
content_atoms = self.repurposing_engine.analyze_content_atoms(
|
||||
content=content_text,
|
||||
title=content_item.title
|
||||
)
|
||||
|
||||
analysis = {
|
||||
'content_analysis': {
|
||||
'word_count': len(content_text.split()) if content_text else 0,
|
||||
'content_richness': self._assess_content_richness(content_atoms),
|
||||
'repurposing_potential': self._assess_repurposing_potential(content_atoms),
|
||||
'content_atoms': content_atoms
|
||||
},
|
||||
'platform_suggestions': suggestions['recommended_platforms'],
|
||||
'strategy_suggestions': suggestions['repurposing_strategies'],
|
||||
'estimated_output': {
|
||||
'total_pieces': suggestions['estimated_pieces'],
|
||||
'time_savings': f"{suggestions['estimated_pieces'] * 2} hours",
|
||||
'content_multiplication': f"{suggestions['estimated_pieces']}x"
|
||||
}
|
||||
}
|
||||
|
||||
return analysis
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error analyzing content for repurposing: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _assess_content_richness(self, content_atoms: Dict[str, List[str]]) -> str:
|
||||
"""Assess the richness of content based on extracted atoms."""
|
||||
total_atoms = sum(len(atoms) for atoms in content_atoms.values())
|
||||
|
||||
if total_atoms >= 15:
|
||||
return "High"
|
||||
elif total_atoms >= 8:
|
||||
return "Medium"
|
||||
else:
|
||||
return "Low"
|
||||
|
||||
def _assess_repurposing_potential(self, content_atoms: Dict[str, List[str]]) -> str:
|
||||
"""Assess the repurposing potential based on content atoms."""
|
||||
# Check for diverse content types
|
||||
atom_types_with_content = sum(1 for atoms in content_atoms.values() if atoms)
|
||||
|
||||
if atom_types_with_content >= 4:
|
||||
return "Excellent"
|
||||
elif atom_types_with_content >= 3:
|
||||
return "Good"
|
||||
elif atom_types_with_content >= 2:
|
||||
return "Fair"
|
||||
else:
|
||||
return "Limited"
|
||||
|
||||
@handle_calendar_error
|
||||
def generate_content_with_repurposing_plan(
|
||||
self,
|
||||
content_item: ContentItem,
|
||||
context: Dict[str, Any],
|
||||
target_platforms: List[Platform] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate content along with a comprehensive repurposing plan.
|
||||
|
||||
Args:
|
||||
content_item: Content item to generate
|
||||
context: Content context from gap analysis
|
||||
target_platforms: Platforms to include in repurposing plan
|
||||
|
||||
Returns:
|
||||
Dictionary containing generated content and repurposing plan
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Generating content with repurposing plan for '{content_item.title}'")
|
||||
|
||||
# Generate the main content structure
|
||||
headings = self.generate_headings(content_item, context)
|
||||
subheadings = self.generate_subheadings(content_item, headings, context)
|
||||
key_points = self.generate_key_points(content_item, context)
|
||||
|
||||
outline = {
|
||||
'headings': headings,
|
||||
'subheadings': subheadings,
|
||||
'key_points': key_points
|
||||
}
|
||||
|
||||
content_flow = self.generate_content_flow(content_item, outline)
|
||||
|
||||
# Create repurposing plan if platforms are specified
|
||||
repurposing_plan = {}
|
||||
if target_platforms:
|
||||
# Analyze repurposing potential
|
||||
analysis = self.analyze_content_for_repurposing(content_item, target_platforms)
|
||||
|
||||
# Generate repurposing suggestions
|
||||
repurposing_plan = {
|
||||
'analysis': analysis,
|
||||
'recommended_strategy': self._recommend_repurposing_strategy(analysis),
|
||||
'platform_roadmap': self._create_platform_roadmap(content_item, target_platforms),
|
||||
'content_calendar_integration': self._suggest_calendar_integration(content_item, target_platforms)
|
||||
}
|
||||
|
||||
return {
|
||||
'content': {
|
||||
'outline': outline,
|
||||
'content_flow': content_flow,
|
||||
'metadata': {
|
||||
'generated_at': str(datetime.now()),
|
||||
'content_type': content_item.content_type.name,
|
||||
'platforms': [p.name for p in content_item.platforms] if content_item.platforms else []
|
||||
}
|
||||
},
|
||||
'repurposing_plan': repurposing_plan
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error generating content with repurposing plan: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _recommend_repurposing_strategy(self, analysis: Dict[str, Any]) -> str:
|
||||
"""Recommend the best repurposing strategy based on content analysis."""
|
||||
content_richness = analysis.get('content_analysis', {}).get('content_richness', 'Low')
|
||||
repurposing_potential = analysis.get('content_analysis', {}).get('repurposing_potential', 'Limited')
|
||||
|
||||
if content_richness == 'High' and repurposing_potential in ['Excellent', 'Good']:
|
||||
return 'progressive_disclosure'
|
||||
elif content_richness in ['Medium', 'High']:
|
||||
return 'adaptive'
|
||||
else:
|
||||
return 'atomic'
|
||||
|
||||
def _create_platform_roadmap(
|
||||
self,
|
||||
content_item: ContentItem,
|
||||
target_platforms: List[Platform]
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a roadmap for content distribution across platforms."""
|
||||
roadmap = {
|
||||
'timeline': {},
|
||||
'platform_sequence': [],
|
||||
'cross_promotion_opportunities': []
|
||||
}
|
||||
|
||||
# Create a timeline for content release
|
||||
base_date = content_item.publish_date or datetime.now()
|
||||
|
||||
for i, platform in enumerate(target_platforms):
|
||||
release_date = base_date + timedelta(days=i)
|
||||
roadmap['timeline'][platform.name] = {
|
||||
'release_date': release_date.strftime('%Y-%m-%d'),
|
||||
'content_type': self._get_optimal_content_type_for_platform(platform),
|
||||
'engagement_strategy': self._get_engagement_strategy_for_platform(platform)
|
||||
}
|
||||
roadmap['platform_sequence'].append(platform.name)
|
||||
|
||||
return roadmap
|
||||
|
||||
def _suggest_calendar_integration(
|
||||
self,
|
||||
content_item: ContentItem,
|
||||
target_platforms: List[Platform]
|
||||
) -> Dict[str, Any]:
|
||||
"""Suggest how to integrate repurposed content into the content calendar."""
|
||||
return {
|
||||
'scheduling_recommendations': {
|
||||
'primary_content': 'Schedule as main content piece',
|
||||
'repurposed_content': 'Schedule 1-2 days after primary content',
|
||||
'series_content': 'Schedule weekly releases for maximum impact'
|
||||
},
|
||||
'calendar_tags': [
|
||||
'repurposed_content',
|
||||
f'source_{content_item.id}',
|
||||
'multi_platform_series'
|
||||
],
|
||||
'performance_tracking': {
|
||||
'metrics_to_track': ['engagement_rate', 'cross_platform_traffic', 'conversion_rate'],
|
||||
'comparison_baseline': 'Compare against single-platform content performance'
|
||||
}
|
||||
}
|
||||
|
||||
def _get_optimal_content_type_for_platform(self, platform: Platform) -> str:
|
||||
"""Get the optimal content type for a specific platform."""
|
||||
platform_content_types = {
|
||||
Platform.TWITTER: 'Thread or single tweet',
|
||||
Platform.LINKEDIN: 'Professional post or article',
|
||||
Platform.INSTAGRAM: 'Visual post with caption',
|
||||
Platform.FACEBOOK: 'Engaging post with discussion starter',
|
||||
Platform.WEBSITE: 'Full blog post or article'
|
||||
}
|
||||
return platform_content_types.get(platform, 'Standard post')
|
||||
|
||||
def _get_engagement_strategy_for_platform(self, platform: Platform) -> str:
|
||||
"""Get the engagement strategy for a specific platform."""
|
||||
engagement_strategies = {
|
||||
Platform.TWITTER: 'Use hashtags, engage in conversations, create polls',
|
||||
Platform.LINKEDIN: 'Professional networking, thought leadership, industry discussions',
|
||||
Platform.INSTAGRAM: 'Visual storytelling, user-generated content, stories',
|
||||
Platform.FACEBOOK: 'Community building, discussions, live interactions',
|
||||
Platform.WEBSITE: 'SEO optimization, internal linking, lead magnets'
|
||||
}
|
||||
return engagement_strategies.get(platform, 'Standard engagement tactics')
|
||||
599
lib/ai_seo_tools/content_calendar/core/content_repurposer.py
Normal file
599
lib/ai_seo_tools/content_calendar/core/content_repurposer.py
Normal file
@@ -0,0 +1,599 @@
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import json
|
||||
|
||||
# Add parent directory to path to import existing tools
|
||||
parent_dir = str(Path(__file__).parent.parent.parent.parent)
|
||||
if parent_dir not in sys.path:
|
||||
sys.path.append(parent_dir)
|
||||
|
||||
from lib.database.models import ContentItem, ContentType, Platform, SEOData
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
from ..utils.error_handling import handle_calendar_error
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ContentAtomizer:
|
||||
"""
|
||||
Break down content into atomic pieces that can be recombined
|
||||
for different platforms and purposes.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('content_calendar.atomizer')
|
||||
|
||||
def atomize_content(self, content: str, title: str = "") -> Dict[str, List[str]]:
|
||||
"""
|
||||
Extract key quotes, statistics, tips, and examples from content.
|
||||
|
||||
Args:
|
||||
content: The content text to atomize
|
||||
title: The content title for context
|
||||
|
||||
Returns:
|
||||
Dictionary containing different types of content atoms
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Atomizing content: {title[:50]}...")
|
||||
|
||||
# Use AI to extract content atoms
|
||||
prompt = f"""
|
||||
Analyze the following content and extract key elements that can be repurposed:
|
||||
|
||||
Title: {title}
|
||||
Content: {content[:3000]}...
|
||||
|
||||
Extract and categorize the following elements:
|
||||
1. Key Statistics (numbers, percentages, data points)
|
||||
2. Quotable Insights (memorable quotes or key insights)
|
||||
3. Actionable Tips (practical advice or steps)
|
||||
4. Examples/Case Studies (real examples or stories)
|
||||
5. Key Questions (thought-provoking questions)
|
||||
6. Main Arguments (core points or arguments)
|
||||
|
||||
Format your response as JSON:
|
||||
{{
|
||||
"statistics": ["stat1", "stat2", ...],
|
||||
"quotes": ["quote1", "quote2", ...],
|
||||
"tips": ["tip1", "tip2", ...],
|
||||
"examples": ["example1", "example2", ...],
|
||||
"questions": ["question1", "question2", ...],
|
||||
"arguments": ["argument1", "argument2", ...]
|
||||
}}
|
||||
"""
|
||||
|
||||
response = llm_text_gen(
|
||||
prompt=prompt,
|
||||
system_prompt="You are an expert content analyst. Extract key elements that can be repurposed across different platforms.",
|
||||
json_struct={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"statistics": {"type": "array", "items": {"type": "string"}},
|
||||
"quotes": {"type": "array", "items": {"type": "string"}},
|
||||
"tips": {"type": "array", "items": {"type": "string"}},
|
||||
"examples": {"type": "array", "items": {"type": "string"}},
|
||||
"questions": {"type": "array", "items": {"type": "string"}},
|
||||
"arguments": {"type": "array", "items": {"type": "string"}}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if response:
|
||||
return response
|
||||
else:
|
||||
# Fallback to basic extraction
|
||||
return self._basic_content_extraction(content)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error atomizing content: {str(e)}")
|
||||
return self._basic_content_extraction(content)
|
||||
|
||||
def _basic_content_extraction(self, content: str) -> Dict[str, List[str]]:
|
||||
"""Fallback method for basic content extraction."""
|
||||
atoms = {
|
||||
"statistics": [],
|
||||
"quotes": [],
|
||||
"tips": [],
|
||||
"examples": [],
|
||||
"questions": [],
|
||||
"arguments": []
|
||||
}
|
||||
|
||||
# Extract statistics (numbers with %)
|
||||
stats = re.findall(r'\d+%|\d+\.\d+%|\d+,\d+|\d+ percent', content)
|
||||
atoms["statistics"] = stats[:5] # Limit to 5
|
||||
|
||||
# Extract questions
|
||||
questions = re.findall(r'[A-Z][^.!?]*\?', content)
|
||||
atoms["questions"] = questions[:3] # Limit to 3
|
||||
|
||||
# Extract sentences that might be tips (containing words like "should", "must", "need to")
|
||||
tip_patterns = r'[^.!?]*(?:should|must|need to|important to|remember to)[^.!?]*[.!?]'
|
||||
tips = re.findall(tip_patterns, content, re.IGNORECASE)
|
||||
atoms["tips"] = tips[:5] # Limit to 5
|
||||
|
||||
return atoms
|
||||
|
||||
class ContentRepurposer:
|
||||
"""
|
||||
Main content repurposing engine that transforms content for different platforms.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('content_calendar.repurposer')
|
||||
self.atomizer = ContentAtomizer()
|
||||
|
||||
# Platform-specific content specifications
|
||||
self.platform_specs = {
|
||||
Platform.TWITTER: {
|
||||
'max_length': 280,
|
||||
'optimal_length': 240,
|
||||
'format': 'concise',
|
||||
'tone': 'engaging',
|
||||
'hashtags': True,
|
||||
'mentions': True
|
||||
},
|
||||
Platform.LINKEDIN: {
|
||||
'max_length': 3000,
|
||||
'optimal_length': 1500,
|
||||
'format': 'professional',
|
||||
'tone': 'authoritative',
|
||||
'hashtags': True,
|
||||
'mentions': False
|
||||
},
|
||||
Platform.INSTAGRAM: {
|
||||
'max_length': 2200,
|
||||
'optimal_length': 1000,
|
||||
'format': 'visual-focused',
|
||||
'tone': 'casual',
|
||||
'hashtags': True,
|
||||
'mentions': True
|
||||
},
|
||||
Platform.FACEBOOK: {
|
||||
'max_length': 63206,
|
||||
'optimal_length': 500,
|
||||
'format': 'engaging',
|
||||
'tone': 'conversational',
|
||||
'hashtags': False,
|
||||
'mentions': True
|
||||
},
|
||||
Platform.WEBSITE: {
|
||||
'max_length': None,
|
||||
'optimal_length': 2000,
|
||||
'format': 'comprehensive',
|
||||
'tone': 'informative',
|
||||
'hashtags': False,
|
||||
'mentions': False
|
||||
}
|
||||
}
|
||||
|
||||
@handle_calendar_error
|
||||
def repurpose_content(
|
||||
self,
|
||||
source_content: ContentItem,
|
||||
target_platforms: List[Platform],
|
||||
repurpose_strategy: str = 'adaptive'
|
||||
) -> List[ContentItem]:
|
||||
"""
|
||||
Repurpose content for multiple platforms.
|
||||
|
||||
Args:
|
||||
source_content: Original content to repurpose
|
||||
target_platforms: List of platforms to create content for
|
||||
repurpose_strategy: Strategy for repurposing ('adaptive', 'atomic', 'series')
|
||||
|
||||
Returns:
|
||||
List of repurposed content items
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Repurposing content '{source_content.title}' for {len(target_platforms)} platforms")
|
||||
|
||||
repurposed_content = []
|
||||
|
||||
# Get content text (assuming it's in description or notes)
|
||||
content_text = source_content.description or source_content.notes or ""
|
||||
|
||||
if not content_text:
|
||||
self.logger.warning("No content text found for repurposing")
|
||||
return []
|
||||
|
||||
# Atomize the content
|
||||
atoms = self.atomizer.atomize_content(content_text, source_content.title)
|
||||
|
||||
# Generate repurposed content for each platform
|
||||
for platform in target_platforms:
|
||||
if platform == source_content.platforms[0] if source_content.platforms else None:
|
||||
continue # Skip the original platform
|
||||
|
||||
repurposed_item = self._create_platform_specific_content(
|
||||
source_content=source_content,
|
||||
target_platform=platform,
|
||||
atoms=atoms,
|
||||
strategy=repurpose_strategy
|
||||
)
|
||||
|
||||
if repurposed_item:
|
||||
repurposed_content.append(repurposed_item)
|
||||
|
||||
self.logger.info(f"Successfully repurposed content into {len(repurposed_content)} variations")
|
||||
return repurposed_content
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error repurposing content: {str(e)}")
|
||||
return []
|
||||
|
||||
def _create_platform_specific_content(
|
||||
self,
|
||||
source_content: ContentItem,
|
||||
target_platform: Platform,
|
||||
atoms: Dict[str, List[str]],
|
||||
strategy: str
|
||||
) -> Optional[ContentItem]:
|
||||
"""Create platform-specific content variation."""
|
||||
try:
|
||||
platform_spec = self.platform_specs.get(target_platform, {})
|
||||
|
||||
# Generate platform-specific content using AI
|
||||
repurposed_text = self._generate_platform_content(
|
||||
source_content=source_content,
|
||||
target_platform=target_platform,
|
||||
atoms=atoms,
|
||||
platform_spec=platform_spec,
|
||||
strategy=strategy
|
||||
)
|
||||
|
||||
if not repurposed_text:
|
||||
return None
|
||||
|
||||
# Create new content item
|
||||
repurposed_item = ContentItem(
|
||||
title=self._adapt_title_for_platform(source_content.title, target_platform),
|
||||
description=repurposed_text,
|
||||
content_type=self._determine_content_type_for_platform(target_platform),
|
||||
platforms=[target_platform],
|
||||
publish_date=source_content.publish_date + timedelta(days=1), # Schedule for next day
|
||||
status="draft",
|
||||
author=source_content.author,
|
||||
tags=source_content.tags + [f"repurposed_from_{source_content.id}"],
|
||||
notes=f"Repurposed from: {source_content.title}",
|
||||
seo_data=self._adapt_seo_data_for_platform(source_content.seo_data, target_platform)
|
||||
)
|
||||
|
||||
return repurposed_item
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating platform-specific content: {str(e)}")
|
||||
return None
|
||||
|
||||
def _generate_platform_content(
|
||||
self,
|
||||
source_content: ContentItem,
|
||||
target_platform: Platform,
|
||||
atoms: Dict[str, List[str]],
|
||||
platform_spec: Dict[str, Any],
|
||||
strategy: str
|
||||
) -> str:
|
||||
"""Generate content optimized for specific platform."""
|
||||
try:
|
||||
# Prepare content elements
|
||||
title = source_content.title
|
||||
original_content = source_content.description or ""
|
||||
|
||||
# Create platform-specific prompt
|
||||
prompt = self._create_repurposing_prompt(
|
||||
title=title,
|
||||
original_content=original_content,
|
||||
target_platform=target_platform,
|
||||
atoms=atoms,
|
||||
platform_spec=platform_spec,
|
||||
strategy=strategy
|
||||
)
|
||||
|
||||
# Generate content using AI
|
||||
repurposed_content = llm_text_gen(prompt)
|
||||
|
||||
return repurposed_content or ""
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error generating platform content: {str(e)}")
|
||||
return ""
|
||||
|
||||
def _create_repurposing_prompt(
|
||||
self,
|
||||
title: str,
|
||||
original_content: str,
|
||||
target_platform: Platform,
|
||||
atoms: Dict[str, List[str]],
|
||||
platform_spec: Dict[str, Any],
|
||||
strategy: str
|
||||
) -> str:
|
||||
"""Create AI prompt for content repurposing."""
|
||||
|
||||
platform_guidelines = {
|
||||
Platform.TWITTER: "Create engaging tweets that drive conversation. Use threads for complex topics. Include relevant hashtags.",
|
||||
Platform.LINKEDIN: "Write professional content that provides value to business professionals. Focus on insights and actionable advice.",
|
||||
Platform.INSTAGRAM: "Create visually-oriented content with engaging captions. Use storytelling and include relevant hashtags.",
|
||||
Platform.FACEBOOK: "Write conversational content that encourages engagement. Ask questions and create community discussion.",
|
||||
Platform.WEBSITE: "Create comprehensive, SEO-optimized content with clear structure and valuable information."
|
||||
}
|
||||
|
||||
atoms_text = ""
|
||||
for atom_type, atom_list in atoms.items():
|
||||
if atom_list:
|
||||
atoms_text += f"\n{atom_type.title()}: {', '.join(atom_list[:3])}"
|
||||
|
||||
prompt = f"""
|
||||
Repurpose the following content for {target_platform.name}:
|
||||
|
||||
Original Title: {title}
|
||||
Original Content: {original_content[:1500]}...
|
||||
|
||||
Key Content Elements:{atoms_text}
|
||||
|
||||
Platform Guidelines: {platform_guidelines.get(target_platform, '')}
|
||||
|
||||
Platform Specifications:
|
||||
- Optimal Length: {platform_spec.get('optimal_length', 'flexible')} characters
|
||||
- Format: {platform_spec.get('format', 'standard')}
|
||||
- Tone: {platform_spec.get('tone', 'professional')}
|
||||
- Include Hashtags: {platform_spec.get('hashtags', False)}
|
||||
|
||||
Requirements:
|
||||
1. Adapt the content to fit {target_platform.name}'s format and audience
|
||||
2. Maintain the core message and value
|
||||
3. Optimize for {target_platform.name} engagement
|
||||
4. Include platform-appropriate calls to action
|
||||
5. Use the extracted content elements effectively
|
||||
|
||||
Create compelling, platform-optimized content that will perform well on {target_platform.name}.
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
def _adapt_title_for_platform(self, original_title: str, platform: Platform) -> str:
|
||||
"""Adapt title for specific platform."""
|
||||
platform_prefixes = {
|
||||
Platform.TWITTER: "🧵 ",
|
||||
Platform.LINKEDIN: "💼 ",
|
||||
Platform.INSTAGRAM: "📸 ",
|
||||
Platform.FACEBOOK: "💬 ",
|
||||
Platform.WEBSITE: ""
|
||||
}
|
||||
|
||||
prefix = platform_prefixes.get(platform, "")
|
||||
return f"{prefix}{original_title}"
|
||||
|
||||
def _determine_content_type_for_platform(self, platform: Platform) -> ContentType:
|
||||
"""Determine appropriate content type for platform."""
|
||||
platform_content_types = {
|
||||
Platform.TWITTER: ContentType.SOCIAL_MEDIA,
|
||||
Platform.LINKEDIN: ContentType.SOCIAL_MEDIA,
|
||||
Platform.INSTAGRAM: ContentType.SOCIAL_MEDIA,
|
||||
Platform.FACEBOOK: ContentType.SOCIAL_MEDIA,
|
||||
Platform.WEBSITE: ContentType.BLOG_POST
|
||||
}
|
||||
|
||||
return platform_content_types.get(platform, ContentType.SOCIAL_MEDIA)
|
||||
|
||||
def _adapt_seo_data_for_platform(self, original_seo: SEOData, platform: Platform) -> SEOData:
|
||||
"""Adapt SEO data for specific platform."""
|
||||
if platform == Platform.WEBSITE:
|
||||
return original_seo
|
||||
|
||||
# For social media platforms, create simplified SEO data
|
||||
return SEOData(
|
||||
title=original_seo.title,
|
||||
meta_description=original_seo.meta_description[:160] if original_seo.meta_description else "",
|
||||
keywords=original_seo.keywords[:5] if original_seo.keywords else [],
|
||||
structured_data={}
|
||||
)
|
||||
|
||||
class ContentSeriesRepurposer:
|
||||
"""
|
||||
Create cross-platform content series with progressive disclosure strategy.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('content_calendar.series_repurposer')
|
||||
self.repurposer = ContentRepurposer()
|
||||
|
||||
def create_cross_platform_series(
|
||||
self,
|
||||
source_content: ContentItem,
|
||||
platforms: List[Platform],
|
||||
series_strategy: str = 'progressive_disclosure'
|
||||
) -> Dict[str, List[ContentItem]]:
|
||||
"""
|
||||
Create a content series that progressively reveals information
|
||||
across different platforms, driving traffic between them.
|
||||
|
||||
Args:
|
||||
source_content: Original comprehensive content
|
||||
platforms: Target platforms for the series
|
||||
series_strategy: Strategy for content distribution
|
||||
|
||||
Returns:
|
||||
Dictionary mapping platforms to their content pieces
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Creating cross-platform series for: {source_content.title}")
|
||||
|
||||
series_content = {}
|
||||
|
||||
if series_strategy == 'progressive_disclosure':
|
||||
series_content = self._create_progressive_disclosure_series(
|
||||
source_content, platforms
|
||||
)
|
||||
elif series_strategy == 'platform_native':
|
||||
series_content = self._create_platform_native_series(
|
||||
source_content, platforms
|
||||
)
|
||||
else:
|
||||
# Default to simple repurposing
|
||||
repurposed = self.repurposer.repurpose_content(
|
||||
source_content, platforms
|
||||
)
|
||||
for item in repurposed:
|
||||
platform = item.platforms[0]
|
||||
if platform not in series_content:
|
||||
series_content[platform] = []
|
||||
series_content[platform].append(item)
|
||||
|
||||
return series_content
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating cross-platform series: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _create_progressive_disclosure_series(
|
||||
self,
|
||||
source_content: ContentItem,
|
||||
platforms: List[Platform]
|
||||
) -> Dict[str, List[ContentItem]]:
|
||||
"""Create series with progressive information disclosure."""
|
||||
series_content = {}
|
||||
|
||||
# Define disclosure strategy
|
||||
disclosure_strategy = {
|
||||
Platform.TWITTER: "teaser", # Hook with key stat/question
|
||||
Platform.INSTAGRAM: "visual", # Visual summary with key points
|
||||
Platform.LINKEDIN: "insight", # Professional insight/analysis
|
||||
Platform.FACEBOOK: "discussion", # Community discussion starter
|
||||
Platform.WEBSITE: "complete" # Full detailed content
|
||||
}
|
||||
|
||||
for platform in platforms:
|
||||
strategy = disclosure_strategy.get(platform, "summary")
|
||||
content_piece = self._create_disclosure_content(
|
||||
source_content, platform, strategy
|
||||
)
|
||||
if content_piece:
|
||||
series_content[platform] = [content_piece]
|
||||
|
||||
return series_content
|
||||
|
||||
def _create_disclosure_content(
|
||||
self,
|
||||
source_content: ContentItem,
|
||||
platform: Platform,
|
||||
disclosure_type: str
|
||||
) -> Optional[ContentItem]:
|
||||
"""Create content piece for specific disclosure strategy."""
|
||||
try:
|
||||
# This would use the repurposer with specific instructions
|
||||
# for the disclosure type
|
||||
repurposed = self.repurposer._create_platform_specific_content(
|
||||
source_content=source_content,
|
||||
target_platform=platform,
|
||||
atoms=self.repurposer.atomizer.atomize_content(
|
||||
source_content.description or "",
|
||||
source_content.title
|
||||
),
|
||||
strategy=disclosure_type
|
||||
)
|
||||
|
||||
return repurposed
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating disclosure content: {str(e)}")
|
||||
return None
|
||||
|
||||
def _create_platform_native_series(
|
||||
self,
|
||||
source_content: ContentItem,
|
||||
platforms: List[Platform]
|
||||
) -> Dict[str, List[ContentItem]]:
|
||||
"""Create series optimized for each platform's native format."""
|
||||
# Implementation for platform-native series
|
||||
# This would create multiple pieces per platform
|
||||
# optimized for that platform's specific characteristics
|
||||
return {}
|
||||
|
||||
# Main repurposing interface
|
||||
class SmartContentRepurposingEngine:
|
||||
"""
|
||||
Main interface for the Smart Content Repurposing Engine.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('content_calendar.repurposing_engine')
|
||||
self.repurposer = ContentRepurposer()
|
||||
self.series_repurposer = ContentSeriesRepurposer()
|
||||
self.atomizer = ContentAtomizer()
|
||||
|
||||
def repurpose_single_content(
|
||||
self,
|
||||
content: ContentItem,
|
||||
target_platforms: List[Platform],
|
||||
strategy: str = 'adaptive'
|
||||
) -> List[ContentItem]:
|
||||
"""Repurpose a single piece of content."""
|
||||
return self.repurposer.repurpose_content(content, target_platforms, strategy)
|
||||
|
||||
def create_content_series(
|
||||
self,
|
||||
content: ContentItem,
|
||||
platforms: List[Platform],
|
||||
series_type: str = 'progressive_disclosure'
|
||||
) -> Dict[str, List[ContentItem]]:
|
||||
"""Create a cross-platform content series."""
|
||||
return self.series_repurposer.create_cross_platform_series(
|
||||
content, platforms, series_type
|
||||
)
|
||||
|
||||
def analyze_content_atoms(self, content: str, title: str = "") -> Dict[str, List[str]]:
|
||||
"""Analyze content and extract reusable atoms."""
|
||||
return self.atomizer.atomize_content(content, title)
|
||||
|
||||
def get_repurposing_suggestions(
|
||||
self,
|
||||
content: ContentItem,
|
||||
available_platforms: List[Platform]
|
||||
) -> Dict[str, Any]:
|
||||
"""Get AI-powered suggestions for content repurposing."""
|
||||
try:
|
||||
# Analyze content to suggest best repurposing strategies
|
||||
content_text = content.description or content.notes or ""
|
||||
atoms = self.atomizer.atomize_content(content_text, content.title)
|
||||
|
||||
suggestions = {
|
||||
'recommended_platforms': [],
|
||||
'repurposing_strategies': [],
|
||||
'content_atoms': atoms,
|
||||
'estimated_pieces': 0
|
||||
}
|
||||
|
||||
# Analyze content type and suggest platforms
|
||||
if content.content_type == ContentType.BLOG_POST:
|
||||
suggestions['recommended_platforms'] = [
|
||||
Platform.TWITTER, Platform.LINKEDIN, Platform.INSTAGRAM
|
||||
]
|
||||
suggestions['estimated_pieces'] = len(available_platforms) * 2
|
||||
elif content.content_type == ContentType.VIDEO:
|
||||
suggestions['recommended_platforms'] = [
|
||||
Platform.TWITTER, Platform.INSTAGRAM, Platform.FACEBOOK
|
||||
]
|
||||
suggestions['estimated_pieces'] = len(available_platforms) * 3
|
||||
|
||||
# Suggest strategies based on content richness
|
||||
if len(atoms.get('statistics', [])) > 3:
|
||||
suggestions['repurposing_strategies'].append('data_driven')
|
||||
if len(atoms.get('tips', [])) > 5:
|
||||
suggestions['repurposing_strategies'].append('tip_series')
|
||||
if len(atoms.get('examples', [])) > 2:
|
||||
suggestions['repurposing_strategies'].append('case_study_series')
|
||||
|
||||
return suggestions
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting repurposing suggestions: {str(e)}")
|
||||
return {
|
||||
'recommended_platforms': [],
|
||||
'repurposing_strategies': [],
|
||||
'content_atoms': {},
|
||||
'estimated_pieces': 0
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from ..core.calendar_manager import CalendarManager
|
||||
from ..models.calendar import ContentType, Platform
|
||||
|
||||
def create_content_calendar(
|
||||
website_url: str,
|
||||
start_date: datetime,
|
||||
duration: str,
|
||||
platforms: List[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Example of creating a content calendar.
|
||||
|
||||
Args:
|
||||
website_url: URL of the website to analyze
|
||||
start_date: When to start the calendar
|
||||
duration: How long the calendar should span
|
||||
platforms: List of platforms to create content for
|
||||
|
||||
Returns:
|
||||
Dictionary containing the calendar data
|
||||
"""
|
||||
# Initialize calendar manager
|
||||
calendar_manager = CalendarManager()
|
||||
|
||||
# Create calendar
|
||||
calendar = calendar_manager.create_calendar(
|
||||
start_date=start_date,
|
||||
duration=duration,
|
||||
platforms=platforms,
|
||||
website_url=website_url
|
||||
)
|
||||
|
||||
# Export calendar
|
||||
calendar_data = calendar_manager.export_calendar()
|
||||
|
||||
return calendar_data
|
||||
|
||||
def main():
|
||||
"""Example usage of the content calendar system."""
|
||||
# Example parameters
|
||||
website_url = "https://example.com"
|
||||
start_date = datetime.now()
|
||||
duration = "monthly"
|
||||
platforms = [
|
||||
Platform.WEBSITE.value,
|
||||
Platform.FACEBOOK.value,
|
||||
Platform.TWITTER.value,
|
||||
Platform.LINKEDIN.value
|
||||
]
|
||||
|
||||
try:
|
||||
# Create calendar
|
||||
calendar_data = create_content_calendar(
|
||||
website_url=website_url,
|
||||
start_date=start_date,
|
||||
duration=duration,
|
||||
platforms=platforms
|
||||
)
|
||||
|
||||
# Print calendar summary
|
||||
print("\nContent Calendar Summary:")
|
||||
print(f"Duration: {calendar_data['duration']}")
|
||||
print(f"Platforms: {', '.join(calendar_data['platforms'])}")
|
||||
print("\nScheduled Content:")
|
||||
|
||||
for date, items in calendar_data['schedule'].items():
|
||||
print(f"\n{date}:")
|
||||
for item in items:
|
||||
print(f"- {item['title']} ({item['content_type']})")
|
||||
print(f" Platforms: {', '.join(item['platforms'])}")
|
||||
print(f" Status: {item['status']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating calendar: {str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,138 +0,0 @@
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
|
||||
from ..models.calendar import ContentItem, ContentType, Platform, SEOData
|
||||
from ..core.content_brief import ContentBriefGenerator
|
||||
|
||||
def create_content_brief(
|
||||
title: str,
|
||||
content_type: ContentType,
|
||||
platforms: list[Platform],
|
||||
website_url: str,
|
||||
target_audience: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a content brief for the given content.
|
||||
|
||||
Args:
|
||||
title: Content title
|
||||
content_type: Type of content
|
||||
platforms: List of platforms to publish on
|
||||
website_url: Website URL for context
|
||||
target_audience: Target audience information
|
||||
|
||||
Returns:
|
||||
Dictionary containing the content brief
|
||||
"""
|
||||
# Create content item
|
||||
content_item = ContentItem(
|
||||
id=f"content-{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
title=title,
|
||||
description=f"Content brief for {title}",
|
||||
content_type=content_type,
|
||||
platforms=platforms,
|
||||
publish_date=datetime.now(),
|
||||
seo_data=SEOData(
|
||||
keywords=[], # Will be generated by SEO tools
|
||||
meta_description="", # Will be generated by SEO tools
|
||||
structured_data={}
|
||||
),
|
||||
platform_specs={}, # Will be generated based on platforms
|
||||
context={
|
||||
"website_url": website_url,
|
||||
"target_audience": target_audience.get("demographics", {}).get("profession", ""),
|
||||
"content_goals": ["educate", "generate leads"]
|
||||
}
|
||||
)
|
||||
|
||||
# Initialize content brief generator
|
||||
generator = ContentBriefGenerator()
|
||||
|
||||
# Generate brief
|
||||
brief = generator.generate_brief(
|
||||
content_item=content_item,
|
||||
target_audience=target_audience
|
||||
)
|
||||
|
||||
return brief
|
||||
|
||||
def main():
|
||||
"""Example usage of content brief generation."""
|
||||
# Example content details
|
||||
title = "10 Ways to Improve Your SEO Strategy"
|
||||
content_type = ContentType.BLOG_POST
|
||||
platforms = [Platform.WEBSITE, Platform.LINKEDIN]
|
||||
website_url = "https://example.com"
|
||||
|
||||
# Example target audience
|
||||
target_audience = {
|
||||
"demographics": {
|
||||
"age_range": "25-45",
|
||||
"profession": "digital marketers",
|
||||
"experience_level": "intermediate"
|
||||
},
|
||||
"interests": [
|
||||
"SEO",
|
||||
"content marketing",
|
||||
"digital strategy",
|
||||
"search engine optimization"
|
||||
],
|
||||
"pain_points": [
|
||||
"low search rankings",
|
||||
"poor content performance",
|
||||
"lack of organic traffic",
|
||||
"difficulty in keyword research"
|
||||
],
|
||||
"goals": [
|
||||
"improve search rankings",
|
||||
"increase organic traffic",
|
||||
"generate more leads",
|
||||
"build brand authority"
|
||||
]
|
||||
}
|
||||
|
||||
try:
|
||||
# Generate content brief
|
||||
brief = create_content_brief(
|
||||
title=title,
|
||||
content_type=content_type,
|
||||
platforms=platforms,
|
||||
website_url=website_url,
|
||||
target_audience=target_audience
|
||||
)
|
||||
|
||||
# Print brief summary
|
||||
print("\nContent Brief Summary:")
|
||||
print(f"Title: {brief['title']}")
|
||||
print(f"Content Type: {brief['content_type']}")
|
||||
|
||||
print("\nOutline:")
|
||||
for heading in brief['outline']['main_headings']:
|
||||
print(f"\n- {heading['title']}")
|
||||
print(f" Keywords: {', '.join(heading['keywords'])}")
|
||||
print(f" Summary: {heading['summary']}")
|
||||
|
||||
# Print subheadings
|
||||
subheadings = brief['outline']['subheadings'].get(heading['title'], [])
|
||||
for subheading in subheadings:
|
||||
print(f" - {subheading['title']}")
|
||||
print(f" Keywords: {', '.join(subheading['keywords'])}")
|
||||
|
||||
print("\nKey Points:")
|
||||
for point in brief['key_points']:
|
||||
print(f"\n- {point['point']}")
|
||||
print(f" Importance: {point['importance']}")
|
||||
print(f" Evidence: {', '.join(point['supporting_evidence'])}")
|
||||
|
||||
print("\nContent Flow:")
|
||||
flow = brief['content_flow']
|
||||
print(f"Introduction: {flow['introduction'].get('summary', '')}")
|
||||
print(f"Main Sections: {len(flow['main_sections'])} sections")
|
||||
print(f"Conclusion: {flow['conclusion'].get('summary', '')}")
|
||||
print(f"Transitions: {len(flow['transitions'])} transition points")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error generating content brief: {str(e)}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,196 +0,0 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from ..integrations.integration_manager import IntegrationManager
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def create_cross_platform_content(
|
||||
title: str,
|
||||
content: str,
|
||||
platforms: List[str],
|
||||
content_type: str,
|
||||
target_audience: Dict[str, Any],
|
||||
industry: str,
|
||||
keywords: List[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""Create and optimize content for multiple platforms."""
|
||||
try:
|
||||
# Initialize integration manager
|
||||
integration_manager = IntegrationManager()
|
||||
|
||||
# Prepare content item
|
||||
content_item = {
|
||||
'title': title,
|
||||
'content': content,
|
||||
'content_type': content_type,
|
||||
'keywords': keywords,
|
||||
'target_audience': target_audience,
|
||||
'industry': industry
|
||||
}
|
||||
|
||||
# Get platform suggestions
|
||||
suggestions = integration_manager.get_platform_suggestions(
|
||||
content=content_item,
|
||||
platforms=platforms
|
||||
)
|
||||
|
||||
# Validate content for each platform
|
||||
validation_results = {}
|
||||
for platform in platforms:
|
||||
validation = integration_manager.validate_platform_content(
|
||||
content=content_item,
|
||||
platform=platform
|
||||
)
|
||||
validation_results[platform] = validation
|
||||
|
||||
# Optimize content for each platform
|
||||
optimized_content = integration_manager.optimize_cross_platform_content(
|
||||
content=content_item,
|
||||
platforms=platforms
|
||||
)
|
||||
|
||||
return {
|
||||
'original_content': content_item,
|
||||
'platform_suggestions': suggestions,
|
||||
'validation_results': validation_results,
|
||||
'optimized_content': optimized_content
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating cross-platform content: {str(e)}")
|
||||
return {
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def create_content_calendar(
|
||||
start_date: datetime,
|
||||
end_date: datetime,
|
||||
platforms: List[str],
|
||||
content_types: List[str],
|
||||
target_audience: Dict[str, Any],
|
||||
industry: str,
|
||||
keywords: List[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a cross-platform content calendar."""
|
||||
try:
|
||||
# Initialize integration manager
|
||||
integration_manager = IntegrationManager()
|
||||
|
||||
# Create calendar
|
||||
calendar = integration_manager.create_cross_platform_calendar(
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
platforms=platforms,
|
||||
content_types=content_types,
|
||||
target_audience=target_audience,
|
||||
industry=industry,
|
||||
keywords=keywords
|
||||
)
|
||||
|
||||
return calendar
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating content calendar: {str(e)}")
|
||||
return {
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def main():
|
||||
"""Main function to demonstrate integration manager usage."""
|
||||
# Example content details
|
||||
title = "The Future of AI in Content Marketing"
|
||||
content = """
|
||||
Artificial Intelligence is revolutionizing the way we approach content marketing.
|
||||
From automated content generation to personalized recommendations, AI tools are
|
||||
helping marketers create more engaging and effective content strategies.
|
||||
|
||||
Key points:
|
||||
1. AI-powered content generation
|
||||
2. Personalized content recommendations
|
||||
3. Automated content optimization
|
||||
4. Data-driven content strategy
|
||||
5. Future trends in AI marketing
|
||||
"""
|
||||
|
||||
# Platform and content settings
|
||||
platforms = ['instagram', 'twitter', 'linkedin', 'blog', 'facebook']
|
||||
content_type = 'article'
|
||||
target_audience = {
|
||||
'age_range': '25-34',
|
||||
'interests': ['technology', 'marketing', 'AI'],
|
||||
'location': 'global',
|
||||
'profession': 'marketing professionals'
|
||||
}
|
||||
industry = 'technology'
|
||||
keywords = ['AI', 'content marketing', 'automation', 'personalization']
|
||||
|
||||
# Create cross-platform content
|
||||
logger.info("Creating cross-platform content...")
|
||||
content_result = create_cross_platform_content(
|
||||
title=title,
|
||||
content=content,
|
||||
platforms=platforms,
|
||||
content_type=content_type,
|
||||
target_audience=target_audience,
|
||||
industry=industry,
|
||||
keywords=keywords
|
||||
)
|
||||
|
||||
# Print content results
|
||||
logger.info("\nCross-Platform Content Results:")
|
||||
logger.info("===============================")
|
||||
|
||||
# Print platform suggestions
|
||||
logger.info("\nPlatform Suggestions:")
|
||||
for platform, suggestions in content_result['platform_suggestions'].items():
|
||||
logger.info(f"\n{platform.upper()}:")
|
||||
for key, value in suggestions.items():
|
||||
logger.info(f" {key}: {value}")
|
||||
|
||||
# Print validation results
|
||||
logger.info("\nValidation Results:")
|
||||
for platform, validation in content_result['validation_results'].items():
|
||||
logger.info(f"\n{platform.upper()}:")
|
||||
logger.info(f" Valid: {validation['is_valid']}")
|
||||
if not validation['is_valid']:
|
||||
logger.info(f" Error: {validation.get('error', 'N/A')}")
|
||||
|
||||
# Print optimized content
|
||||
logger.info("\nOptimized Content:")
|
||||
for platform, optimized in content_result['optimized_content'].items():
|
||||
logger.info(f"\n{platform.upper()}:")
|
||||
for key, value in optimized.items():
|
||||
logger.info(f" {key}: {value}")
|
||||
|
||||
# Create content calendar
|
||||
logger.info("\nCreating content calendar...")
|
||||
start_date = datetime.now()
|
||||
end_date = start_date + timedelta(days=30)
|
||||
calendar_result = create_content_calendar(
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
platforms=platforms,
|
||||
content_types=[content_type],
|
||||
target_audience=target_audience,
|
||||
industry=industry,
|
||||
keywords=keywords
|
||||
)
|
||||
|
||||
# Print calendar results
|
||||
logger.info("\nContent Calendar Results:")
|
||||
logger.info("========================")
|
||||
|
||||
# Print platform calendars
|
||||
logger.info("\nPlatform Calendars:")
|
||||
for platform, calendar in calendar_result['platform_calendars'].items():
|
||||
logger.info(f"\n{platform.upper()}:")
|
||||
logger.info(f" Content Items: {len(calendar['content_items'])}")
|
||||
for item in calendar['content_items']:
|
||||
logger.info(f" - {item['original_item']['title']}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,142 +0,0 @@
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from ..integrations.platform_adapters import UnifiedPlatformAdapter
|
||||
|
||||
def create_platform_content(
|
||||
title: str,
|
||||
content: str,
|
||||
platforms: list,
|
||||
context: Dict[str, Any] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create platform-specific content using the UnifiedPlatformAdapter.
|
||||
|
||||
Args:
|
||||
title: The title of the content
|
||||
content: The main content to be adapted
|
||||
platforms: List of platforms to adapt content for
|
||||
context: Additional context for content adaptation
|
||||
|
||||
Returns:
|
||||
Dict containing adapted content for each platform
|
||||
"""
|
||||
# Initialize the platform adapter
|
||||
adapter = UnifiedPlatformAdapter()
|
||||
|
||||
# Prepare base content
|
||||
base_content = {
|
||||
'title': title,
|
||||
'content': content,
|
||||
'keywords': ['content', 'marketing', 'social media'],
|
||||
'tone': 'professional',
|
||||
'cta': 'Learn More',
|
||||
'audience': 'For All',
|
||||
'language': 'English',
|
||||
'industry': 'technology',
|
||||
'word_count': 1000
|
||||
}
|
||||
|
||||
# Adapt content for each platform
|
||||
adapted_content = {}
|
||||
for platform in platforms:
|
||||
try:
|
||||
platform_content = adapter.adapt_content(
|
||||
content=base_content,
|
||||
platform=platform,
|
||||
context=context
|
||||
)
|
||||
adapted_content[platform] = platform_content
|
||||
except Exception as e:
|
||||
print(f"Error adapting content for {platform}: {str(e)}")
|
||||
adapted_content[platform] = {'error': str(e)}
|
||||
|
||||
return adapted_content
|
||||
|
||||
def main():
|
||||
"""Example usage of platform content adaptation."""
|
||||
# Example content
|
||||
title = "The Future of AI in Content Marketing"
|
||||
content = """
|
||||
Artificial Intelligence is revolutionizing content marketing in unprecedented ways.
|
||||
From automated content generation to personalized user experiences, AI is becoming
|
||||
an indispensable tool for marketers. This article explores the latest trends and
|
||||
innovations in AI-powered content marketing.
|
||||
"""
|
||||
|
||||
# Example context
|
||||
context = {
|
||||
'target_audience': 'marketing professionals',
|
||||
'campaign_goals': ['awareness', 'engagement', 'lead generation'],
|
||||
'brand_voice': 'authoritative yet approachable',
|
||||
'content_theme': 'technology and innovation'
|
||||
}
|
||||
|
||||
# Platforms to adapt content for
|
||||
platforms = ['instagram', 'twitter', 'linkedin', 'blog', 'facebook']
|
||||
|
||||
# Generate platform-specific content
|
||||
adapted_content = create_platform_content(
|
||||
title=title,
|
||||
content=content,
|
||||
platforms=platforms,
|
||||
context=context
|
||||
)
|
||||
|
||||
# Print results
|
||||
print("\nPlatform-Specific Content Adaptation Results:")
|
||||
print("=" * 50)
|
||||
|
||||
for platform, content in adapted_content.items():
|
||||
print(f"\n{platform.upper()} Content:")
|
||||
print("-" * 30)
|
||||
|
||||
if 'error' in content:
|
||||
print(f"Error: {content['error']}")
|
||||
continue
|
||||
|
||||
# Print platform-specific content
|
||||
if platform == 'instagram':
|
||||
print("\nCaptions:")
|
||||
for caption in content['captions']:
|
||||
print(f"- {caption}")
|
||||
print("\nHashtags:")
|
||||
print(content['hashtags'])
|
||||
|
||||
elif platform == 'twitter':
|
||||
print("\nTweets:")
|
||||
for tweet in content['tweets']:
|
||||
print(f"- {tweet}")
|
||||
print("\nThread Structure:")
|
||||
print(content['thread_structure'])
|
||||
|
||||
elif platform == 'linkedin':
|
||||
print("\nPost:")
|
||||
print(content['post'])
|
||||
print("\nEngagement Optimization:")
|
||||
print(content['engagement_optimization'])
|
||||
|
||||
elif platform == 'blog':
|
||||
print("\nPost:")
|
||||
print(content['post'])
|
||||
print("\nSEO Optimization:")
|
||||
print(content['seo_optimization'])
|
||||
|
||||
elif platform == 'facebook':
|
||||
print("\nPost:")
|
||||
print(content['post'])
|
||||
print("\nEngagement Optimization:")
|
||||
print(content['engagement_optimization'])
|
||||
|
||||
# Print media suggestions
|
||||
print("\nMedia Suggestions:")
|
||||
for media in content['media_suggestions']:
|
||||
print(f"- {media['type']}: {media['description']}")
|
||||
|
||||
# Print platform-specific recommendations
|
||||
print("\nPlatform-Specific Recommendations:")
|
||||
for key, value in content['platform_specific'].items():
|
||||
print(f"- {key}: {value}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,37 +1,30 @@
|
||||
"""
|
||||
Platform adapters for content calendar.
|
||||
Unified platform adapter for content adaptation across different platforms.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
from typing import Dict, Any, List, Optional
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional, TypedDict
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
|
||||
from lib.utils.website_analyzer.analyzer import WebsiteAnalyzer
|
||||
from lib.ai_seo_tools.content_gap_analysis.main import ContentGapAnalysis
|
||||
from lib.ai_seo_tools.content_title_generator import ai_title_generator
|
||||
from lib.ai_seo_tools.meta_desc_generator import metadesc_generator_main
|
||||
from lib.ai_seo_tools.seo_structured_data import ai_structured_data
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
|
||||
# Configure logger
|
||||
logger.remove() # Remove default handler
|
||||
logger.add(
|
||||
"logs/platform_adapters.log",
|
||||
rotation="50 MB",
|
||||
retention="10 days",
|
||||
level="DEBUG",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
|
||||
)
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
level="INFO",
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>"
|
||||
)
|
||||
|
||||
# Ensure logs directory exists
|
||||
os.makedirs("logs", exist_ok=True)
|
||||
class ContentItem(TypedDict):
|
||||
"""Type definition for content items."""
|
||||
id: str
|
||||
title: str
|
||||
content: str
|
||||
platforms: List[str]
|
||||
status: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
published_at: Optional[datetime]
|
||||
metadata: Dict[str, Any]
|
||||
analytics: Optional[Dict[str, Any]]
|
||||
|
||||
class UnifiedPlatformAdapter:
|
||||
"""Unified adapter for different social media platforms."""
|
||||
@@ -72,14 +65,76 @@ class UnifiedPlatformAdapter:
|
||||
'content': None
|
||||
}
|
||||
|
||||
def get_content_performance(self, content_item: ContentItem) -> Dict[str, Any]:
|
||||
"""Get performance metrics for content across platforms."""
|
||||
try:
|
||||
logger.info(f"Getting performance metrics for content: {getattr(content_item, 'title', 'Untitled')}")
|
||||
|
||||
# Get platform from content item
|
||||
platforms = getattr(content_item, 'platforms', None)
|
||||
if platforms and len(platforms) > 0:
|
||||
platform = platforms[0].name if hasattr(platforms[0], 'name') else str(platforms[0])
|
||||
else:
|
||||
platform = 'Unknown'
|
||||
|
||||
# Initialize performance metrics
|
||||
performance = {
|
||||
'engagement_metrics': {
|
||||
'likes': 0,
|
||||
'comments': 0,
|
||||
'shares': 0,
|
||||
'reach': 0
|
||||
},
|
||||
'seo_metrics': {
|
||||
'impressions': 0,
|
||||
'clicks': 0,
|
||||
'ctr': 0,
|
||||
'position': 0
|
||||
},
|
||||
'conversion_metrics': {
|
||||
'conversions': 0,
|
||||
'conversion_rate': 0,
|
||||
'revenue': 0
|
||||
},
|
||||
'platform_specific': {},
|
||||
'performance_trends': [],
|
||||
'recommendations': []
|
||||
}
|
||||
|
||||
# Add platform-specific metrics
|
||||
if platform.upper() == 'WEBSITE':
|
||||
performance['platform_specific'] = {
|
||||
'bounce_rate': 0,
|
||||
'time_on_page': 0,
|
||||
'page_views': 0
|
||||
}
|
||||
|
||||
return performance
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error getting content performance: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {
|
||||
'error': error_msg,
|
||||
'metrics': {},
|
||||
'trends': {},
|
||||
'recommendations': []
|
||||
}
|
||||
|
||||
def _handle_instagram(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Handle Instagram content generation."""
|
||||
try:
|
||||
# Use content title generator for Instagram captions
|
||||
caption = ai_title_generator(data)
|
||||
# Generate Instagram-specific content
|
||||
caption = metadesc_generator_main(data)
|
||||
hashtags = self._generate_hashtags(data)
|
||||
|
||||
return {
|
||||
'platform': 'instagram',
|
||||
'content': caption
|
||||
'content': {
|
||||
'caption': caption,
|
||||
'hashtags': hashtags,
|
||||
'media_suggestions': self._get_media_suggestions(data)
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating Instagram content: {str(e)}")
|
||||
@@ -91,11 +146,16 @@ class UnifiedPlatformAdapter:
|
||||
def _handle_linkedin(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Handle LinkedIn content generation."""
|
||||
try:
|
||||
# Use meta description generator for LinkedIn posts
|
||||
# Generate LinkedIn-specific content
|
||||
post = metadesc_generator_main(data)
|
||||
|
||||
return {
|
||||
'platform': 'linkedin',
|
||||
'content': post
|
||||
'content': {
|
||||
'post': post,
|
||||
'engagement_optimization': self._get_engagement_suggestions(data),
|
||||
'media_suggestions': self._get_media_suggestions(data)
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating LinkedIn content: {str(e)}")
|
||||
@@ -107,11 +167,18 @@ class UnifiedPlatformAdapter:
|
||||
def _handle_twitter(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Handle Twitter content generation."""
|
||||
try:
|
||||
# Use content title generator for tweets
|
||||
tweet = ai_title_generator(data)
|
||||
# Generate Twitter-specific content
|
||||
tweet = metadesc_generator_main(data)
|
||||
hashtags = self._generate_hashtags(data)
|
||||
|
||||
return {
|
||||
'platform': 'twitter',
|
||||
'content': tweet
|
||||
'content': {
|
||||
'tweet': tweet,
|
||||
'hashtags': hashtags,
|
||||
'thread_structure': self._get_thread_structure(data),
|
||||
'media_suggestions': self._get_media_suggestions(data)
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating Twitter content: {str(e)}")
|
||||
@@ -123,15 +190,118 @@ class UnifiedPlatformAdapter:
|
||||
def _handle_facebook(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Handle Facebook content generation."""
|
||||
try:
|
||||
# Use meta description generator for Facebook posts
|
||||
# Generate Facebook-specific content
|
||||
post = metadesc_generator_main(data)
|
||||
|
||||
return {
|
||||
'platform': 'facebook',
|
||||
'content': post
|
||||
'content': {
|
||||
'post': post,
|
||||
'engagement_optimization': self._get_engagement_suggestions(data),
|
||||
'media_suggestions': self._get_media_suggestions(data)
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating Facebook content: {str(e)}")
|
||||
return {
|
||||
'platform': 'facebook',
|
||||
'error': str(e)
|
||||
}
|
||||
}
|
||||
|
||||
def _generate_hashtags(self, data: Dict[str, Any]) -> List[str]:
|
||||
"""Generate relevant hashtags for content."""
|
||||
try:
|
||||
# Extract keywords from content
|
||||
keywords = data.get('keywords', [])
|
||||
|
||||
# Add platform-specific hashtags
|
||||
platform = data.get('platform', '').lower()
|
||||
platform_hashtags = {
|
||||
'instagram': ['#instagood', '#photooftheday'],
|
||||
'twitter': ['#trending', '#followme'],
|
||||
'linkedin': ['#business', '#professional'],
|
||||
'facebook': ['#social', '#community']
|
||||
}.get(platform, [])
|
||||
|
||||
return keywords + platform_hashtags
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating hashtags: {str(e)}")
|
||||
return []
|
||||
|
||||
def _get_media_suggestions(self, data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Get media suggestions for content."""
|
||||
try:
|
||||
# Generate media suggestions based on content type
|
||||
content_type = data.get('type', 'post')
|
||||
|
||||
suggestions = []
|
||||
if content_type == 'blog':
|
||||
suggestions.append({
|
||||
'type': 'featured_image',
|
||||
'description': 'Main blog post image',
|
||||
'dimensions': '1200x630'
|
||||
})
|
||||
elif content_type == 'social':
|
||||
suggestions.append({
|
||||
'type': 'post_image',
|
||||
'description': 'Social media post image',
|
||||
'dimensions': '1080x1080'
|
||||
})
|
||||
|
||||
return suggestions
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting media suggestions: {str(e)}")
|
||||
return []
|
||||
|
||||
def _get_engagement_suggestions(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get engagement optimization suggestions."""
|
||||
try:
|
||||
return {
|
||||
'best_posting_times': ['9:00 AM', '5:00 PM'],
|
||||
'engagement_tips': [
|
||||
'Ask questions to encourage comments',
|
||||
'Use relevant hashtags',
|
||||
'Include a clear call-to-action'
|
||||
],
|
||||
'content_length': {
|
||||
'optimal': '150-200 characters',
|
||||
'maximum': '300 characters'
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting engagement suggestions: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _get_thread_structure(self, data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Get thread structure for Twitter threads."""
|
||||
try:
|
||||
content = data.get('content', '')
|
||||
sentences = content.split('.')
|
||||
|
||||
thread = []
|
||||
current_tweet = ''
|
||||
|
||||
for sentence in sentences:
|
||||
if len(current_tweet + sentence) <= 280:
|
||||
current_tweet += sentence + '.'
|
||||
else:
|
||||
if current_tweet:
|
||||
thread.append({
|
||||
'content': current_tweet.strip(),
|
||||
'type': 'tweet'
|
||||
})
|
||||
current_tweet = sentence + '.'
|
||||
|
||||
if current_tweet:
|
||||
thread.append({
|
||||
'content': current_tweet.strip(),
|
||||
'type': 'tweet'
|
||||
})
|
||||
|
||||
return thread
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating thread structure: {str(e)}")
|
||||
return []
|
||||
@@ -1,237 +0,0 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout),
|
||||
logging.FileHandler('content_calendar_debug.log', mode='a')
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
import pandas as pd
|
||||
|
||||
class ContentType(Enum):
|
||||
"""Types of content that can be scheduled."""
|
||||
BLOG_POST = "blog_post"
|
||||
SOCIAL_MEDIA = "social_media"
|
||||
VIDEO = "video"
|
||||
PODCAST = "podcast"
|
||||
NEWSLETTER = "newsletter"
|
||||
LANDING_PAGE = "landing_page"
|
||||
|
||||
class Platform(Enum):
|
||||
"""Supported content platforms."""
|
||||
WEBSITE = "website"
|
||||
FACEBOOK = "facebook"
|
||||
TWITTER = "twitter"
|
||||
LINKEDIN = "linkedin"
|
||||
INSTAGRAM = "instagram"
|
||||
YOUTUBE = "youtube"
|
||||
MEDIUM = "medium"
|
||||
|
||||
@dataclass
|
||||
class SEOData:
|
||||
"""SEO-related data for content."""
|
||||
title: str
|
||||
meta_description: str
|
||||
keywords: List[str]
|
||||
structured_data: Dict[str, Any]
|
||||
canonical_url: Optional[str] = None
|
||||
og_tags: Optional[Dict[str, str]] = None
|
||||
twitter_cards: Optional[Dict[str, str]] = None
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data):
|
||||
return SEOData(
|
||||
title=data.get('title', ''),
|
||||
meta_description=data.get('meta_description', ''),
|
||||
keywords=data.get('keywords', []),
|
||||
structured_data=data.get('structured_data', {}),
|
||||
canonical_url=data.get('canonical_url'),
|
||||
og_tags=data.get('og_tags'),
|
||||
twitter_cards=data.get('twitter_cards')
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class ContentItem:
|
||||
"""Represents a single content item in the calendar."""
|
||||
title: str
|
||||
description: str
|
||||
content_type: ContentType
|
||||
platforms: List[Platform]
|
||||
publish_date: datetime
|
||||
seo_data: SEOData
|
||||
status: str = "draft"
|
||||
author: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
notes: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert content item to dictionary."""
|
||||
return {
|
||||
'title': self.title,
|
||||
'description': self.description,
|
||||
'content_type': self.content_type.value,
|
||||
'platforms': [p.value for p in self.platforms],
|
||||
'publish_date': self.publish_date.isoformat(),
|
||||
'seo_data': {
|
||||
'title': self.seo_data.title,
|
||||
'meta_description': self.seo_data.meta_description,
|
||||
'keywords': self.seo_data.keywords,
|
||||
'structured_data': self.seo_data.structured_data,
|
||||
'canonical_url': self.seo_data.canonical_url,
|
||||
'og_tags': self.seo_data.og_tags,
|
||||
'twitter_cards': self.seo_data.twitter_cards
|
||||
},
|
||||
'status': self.status,
|
||||
'author': self.author,
|
||||
'tags': self.tags,
|
||||
'notes': self.notes
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data):
|
||||
from .calendar import ContentType, Platform, SEOData
|
||||
return ContentItem(
|
||||
title=data['title'],
|
||||
description=data.get('description', ''),
|
||||
content_type=ContentType(data['content_type']),
|
||||
platforms=[Platform(p) for p in data['platforms']],
|
||||
publish_date=pd.to_datetime(data['publish_date']),
|
||||
seo_data=SEOData.from_dict(data.get('seo_data', {})),
|
||||
status=data.get('status', 'draft'),
|
||||
author=data.get('author'),
|
||||
tags=data.get('tags', []),
|
||||
notes=data.get('notes')
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class Calendar:
|
||||
"""Represents a content calendar."""
|
||||
start_date: datetime
|
||||
duration: str # 'weekly', 'monthly', 'quarterly'
|
||||
platforms: List[Platform]
|
||||
schedule: Dict[str, List[ContentItem]]
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
def __init__(self, start_date: datetime, duration: str, platforms: List[Platform],
|
||||
schedule: Dict[str, List[ContentItem]], name: Optional[str] = None,
|
||||
description: Optional[str] = None):
|
||||
"""Initialize a new calendar.
|
||||
|
||||
Args:
|
||||
start_date: Start date of the calendar
|
||||
duration: Duration of the calendar ('weekly', 'monthly', 'quarterly')
|
||||
platforms: List of platforms to schedule content for
|
||||
schedule: Dictionary mapping dates to content items
|
||||
name: Optional name for the calendar
|
||||
description: Optional description of the calendar
|
||||
"""
|
||||
self.start_date = start_date
|
||||
self.duration = duration
|
||||
self.platforms = platforms
|
||||
self.schedule = schedule
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.content_items: List[ContentItem] = []
|
||||
self.logger = logging.getLogger('content_calendar.calendar')
|
||||
|
||||
# Initialize content_items from schedule
|
||||
for items in self.schedule.values():
|
||||
self.content_items.extend(items)
|
||||
|
||||
def get_all_content(self) -> List[ContentItem]:
|
||||
"""Get all content items in the calendar.
|
||||
|
||||
Returns:
|
||||
List of all ContentItem objects in the calendar
|
||||
"""
|
||||
try:
|
||||
self.logger.debug(f"Getting all content items. Count: {len(self.content_items)}")
|
||||
return self.content_items
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting all content: {str(e)}")
|
||||
return []
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert calendar to dictionary."""
|
||||
return {
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'start_date': self.start_date.isoformat(),
|
||||
'duration': self.duration,
|
||||
'platforms': [p.value for p in self.platforms],
|
||||
'schedule': {
|
||||
date: [item.to_dict() for item in items]
|
||||
for date, items in self.schedule.items()
|
||||
}
|
||||
}
|
||||
|
||||
def export(self, format: str = 'json') -> Dict[str, Any]:
|
||||
"""
|
||||
Export calendar in specified format.
|
||||
Currently only supports JSON format.
|
||||
"""
|
||||
if format.lower() != 'json':
|
||||
raise ValueError(f"Unsupported export format: {format}")
|
||||
|
||||
return self.to_dict()
|
||||
|
||||
def get_content_for_date(self, date: datetime) -> List[ContentItem]:
|
||||
"""Get all content items scheduled for a specific date."""
|
||||
date_str = date.strftime('%Y-%m-%d')
|
||||
return self.schedule.get(date_str, [])
|
||||
|
||||
def get_content_for_platform(
|
||||
self,
|
||||
platform: Platform
|
||||
) -> List[ContentItem]:
|
||||
"""Get all content items for a specific platform."""
|
||||
all_content = []
|
||||
for items in self.schedule.values():
|
||||
platform_content = [
|
||||
item for item in items
|
||||
if platform in item.platforms
|
||||
]
|
||||
all_content.extend(platform_content)
|
||||
return all_content
|
||||
|
||||
def add_content(self, content: ContentItem) -> None:
|
||||
"""Add a new content item to the calendar."""
|
||||
date_str = content.publish_date.strftime('%Y-%m-%d')
|
||||
if date_str not in self.schedule:
|
||||
self.schedule[date_str] = []
|
||||
self.schedule[date_str].append(content)
|
||||
|
||||
def remove_content(self, content: ContentItem) -> None:
|
||||
"""Remove a content item from the calendar."""
|
||||
date_str = content.publish_date.strftime('%Y-%m-%d')
|
||||
if date_str in self.schedule:
|
||||
self.schedule[date_str] = [
|
||||
item for item in self.schedule[date_str]
|
||||
if item != content
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data):
|
||||
from .calendar import ContentItem, Platform
|
||||
schedule = {
|
||||
date: [ContentItem.from_dict(item) for item in items]
|
||||
for date, items in data.get('schedule', {}).items()
|
||||
}
|
||||
return Calendar(
|
||||
start_date=pd.to_datetime(data['start_date']),
|
||||
duration=data['duration'],
|
||||
platforms=[Platform(p) for p in data['platforms']],
|
||||
schedule=schedule,
|
||||
name=data.get('name'),
|
||||
description=data.get('description')
|
||||
)
|
||||
@@ -1,185 +0,0 @@
|
||||
import unittest
|
||||
from typing import Dict, Any
|
||||
|
||||
from ..models.calendar import ContentType
|
||||
from ..core.ai_generator import AIContentGenerator
|
||||
|
||||
class TestAIContentGenerator(unittest.TestCase):
|
||||
"""Test cases for AIContentGenerator."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test cases."""
|
||||
self.generator = AIContentGenerator()
|
||||
self.test_title = "10 Ways to Improve Your SEO Strategy"
|
||||
self.test_content_type = ContentType.BLOG_POST
|
||||
self.test_context = {
|
||||
"website_url": "https://example.com",
|
||||
"target_audience": "digital marketers",
|
||||
"content_goals": ["educate", "generate leads"]
|
||||
}
|
||||
|
||||
def test_generate_headings(self):
|
||||
"""Test heading generation."""
|
||||
headings = self.generator.generate_headings(
|
||||
title=self.test_title,
|
||||
content_type=self.test_content_type,
|
||||
context=self.test_context
|
||||
)
|
||||
|
||||
self.assertIsInstance(headings, list)
|
||||
for heading in headings:
|
||||
self.assertIn('title', heading)
|
||||
self.assertIn('level', heading)
|
||||
self.assertIn('keywords', heading)
|
||||
self.assertIn('summary', heading)
|
||||
|
||||
# Verify heading level
|
||||
self.assertEqual(heading['level'], 1)
|
||||
|
||||
# Verify heading content
|
||||
self.assertIsInstance(heading['title'], str)
|
||||
self.assertIsInstance(heading['keywords'], list)
|
||||
self.assertIsInstance(heading['summary'], str)
|
||||
|
||||
def test_generate_subheadings(self):
|
||||
"""Test subheading generation."""
|
||||
main_heading = {
|
||||
'title': 'Understanding SEO Basics',
|
||||
'level': 1,
|
||||
'keywords': ['SEO', 'basics', 'fundamentals'],
|
||||
'summary': 'Introduction to core SEO concepts'
|
||||
}
|
||||
|
||||
subheadings = self.generator.generate_subheadings(
|
||||
main_heading=main_heading,
|
||||
content_type=self.test_content_type,
|
||||
context=self.test_context
|
||||
)
|
||||
|
||||
self.assertIsInstance(subheadings, list)
|
||||
for subheading in subheadings:
|
||||
self.assertIn('title', subheading)
|
||||
self.assertIn('level', subheading)
|
||||
self.assertIn('keywords', subheading)
|
||||
self.assertIn('summary', subheading)
|
||||
|
||||
# Verify subheading level
|
||||
self.assertEqual(subheading['level'], 2)
|
||||
|
||||
# Verify subheading content
|
||||
self.assertIsInstance(subheading['title'], str)
|
||||
self.assertIsInstance(subheading['keywords'], list)
|
||||
self.assertIsInstance(subheading['summary'], str)
|
||||
|
||||
def test_generate_key_points(self):
|
||||
"""Test key points generation."""
|
||||
key_points = self.generator.generate_key_points(
|
||||
title=self.test_title,
|
||||
content_type=self.test_content_type,
|
||||
context=self.test_context
|
||||
)
|
||||
|
||||
self.assertIsInstance(key_points, list)
|
||||
for point in key_points:
|
||||
self.assertIn('point', point)
|
||||
self.assertIn('importance', point)
|
||||
self.assertIn('supporting_evidence', point)
|
||||
self.assertIn('related_keywords', point)
|
||||
|
||||
# Verify point content
|
||||
self.assertIsInstance(point['point'], str)
|
||||
self.assertIn(point['importance'], ['high', 'medium', 'low'])
|
||||
self.assertIsInstance(point['supporting_evidence'], list)
|
||||
self.assertIsInstance(point['related_keywords'], list)
|
||||
|
||||
def test_generate_content_flow(self):
|
||||
"""Test content flow generation."""
|
||||
outline = {
|
||||
'main_headings': [
|
||||
{
|
||||
'title': 'Introduction',
|
||||
'level': 1,
|
||||
'keywords': ['SEO', 'introduction'],
|
||||
'summary': 'Overview of SEO importance'
|
||||
}
|
||||
],
|
||||
'subheadings': {
|
||||
'Introduction': [
|
||||
{
|
||||
'title': 'What is SEO?',
|
||||
'level': 2,
|
||||
'keywords': ['definition', 'basics'],
|
||||
'summary': 'Basic definition of SEO'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
flow = self.generator.generate_content_flow(
|
||||
title=self.test_title,
|
||||
content_type=self.test_content_type,
|
||||
outline=outline
|
||||
)
|
||||
|
||||
self.assertIsInstance(flow, dict)
|
||||
self.assertIn('introduction', flow)
|
||||
self.assertIn('main_sections', flow)
|
||||
self.assertIn('conclusion', flow)
|
||||
self.assertIn('transitions', flow)
|
||||
self.assertIn('content_pacing', flow)
|
||||
|
||||
# Verify flow content
|
||||
self.assertIsInstance(flow['introduction'], dict)
|
||||
self.assertIsInstance(flow['main_sections'], list)
|
||||
self.assertIsInstance(flow['conclusion'], dict)
|
||||
self.assertIsInstance(flow['transitions'], list)
|
||||
self.assertIsInstance(flow['content_pacing'], dict)
|
||||
|
||||
def test_prompt_creation(self):
|
||||
"""Test prompt creation methods."""
|
||||
# Test heading prompt
|
||||
heading_prompt = self.generator._create_heading_prompt(
|
||||
title=self.test_title,
|
||||
content_type=self.test_content_type,
|
||||
gaps={'opportunities': ['keyword research', 'content optimization']}
|
||||
)
|
||||
self.assertIsInstance(heading_prompt, str)
|
||||
self.assertIn(self.test_title, heading_prompt)
|
||||
self.assertIn(self.test_content_type.value, heading_prompt)
|
||||
|
||||
# Test subheading prompt
|
||||
main_heading = {
|
||||
'title': 'Understanding SEO Basics',
|
||||
'level': 1,
|
||||
'keywords': ['SEO', 'basics'],
|
||||
'summary': 'Introduction to SEO'
|
||||
}
|
||||
subheading_prompt = self.generator._create_subheading_prompt(
|
||||
main_heading=main_heading,
|
||||
content_type=self.test_content_type,
|
||||
context=self.test_context
|
||||
)
|
||||
self.assertIsInstance(subheading_prompt, str)
|
||||
self.assertIn(main_heading['title'], subheading_prompt)
|
||||
|
||||
# Test key points prompt
|
||||
key_points_prompt = self.generator._create_key_points_prompt(
|
||||
title=self.test_title,
|
||||
content_type=self.test_content_type,
|
||||
seo_data={'keywords': ['SEO', 'strategy']},
|
||||
context=self.test_context
|
||||
)
|
||||
self.assertIsInstance(key_points_prompt, str)
|
||||
self.assertIn(self.test_title, key_points_prompt)
|
||||
|
||||
# Test flow prompt
|
||||
flow_prompt = self.generator._create_flow_prompt(
|
||||
title=self.test_title,
|
||||
content_type=self.test_content_type,
|
||||
outline={'main_headings': []}
|
||||
)
|
||||
self.assertIsInstance(flow_prompt, str)
|
||||
self.assertIn(self.test_title, flow_prompt)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,132 +0,0 @@
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
|
||||
from ..models.calendar import ContentItem, ContentType, Platform, SEOData
|
||||
from ..core.content_brief import ContentBriefGenerator
|
||||
|
||||
class TestContentBriefGenerator(unittest.TestCase):
|
||||
"""Test cases for ContentBriefGenerator."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test cases."""
|
||||
self.generator = ContentBriefGenerator()
|
||||
self.test_content_item = self._create_test_content_item()
|
||||
|
||||
def _create_test_content_item(self) -> ContentItem:
|
||||
"""Create a test content item."""
|
||||
return ContentItem(
|
||||
id="test-001",
|
||||
title="10 Ways to Improve Your SEO Strategy",
|
||||
description="A comprehensive guide to enhancing your website's SEO performance",
|
||||
content_type=ContentType.BLOG_POST,
|
||||
platforms=[Platform.WEBSITE, Platform.LINKEDIN],
|
||||
publish_date=datetime.now(),
|
||||
seo_data=SEOData(
|
||||
keywords=["SEO", "search engine optimization", "digital marketing"],
|
||||
meta_description="Learn effective SEO strategies to boost your website's visibility",
|
||||
structured_data={}
|
||||
),
|
||||
platform_specs={
|
||||
"website": {
|
||||
"format": "blog post",
|
||||
"min_length": 1500
|
||||
},
|
||||
"linkedin": {
|
||||
"format": "article",
|
||||
"min_length": 800
|
||||
}
|
||||
},
|
||||
context={
|
||||
"website_url": "https://example.com",
|
||||
"target_audience": "digital marketers",
|
||||
"content_goals": ["educate", "generate leads"]
|
||||
}
|
||||
)
|
||||
|
||||
def test_generate_brief(self):
|
||||
"""Test content brief generation."""
|
||||
# Generate brief
|
||||
brief = self.generator.generate_brief(
|
||||
content_item=self.test_content_item,
|
||||
target_audience={
|
||||
"demographics": {
|
||||
"age_range": "25-45",
|
||||
"profession": "digital marketers"
|
||||
},
|
||||
"interests": ["SEO", "content marketing", "digital strategy"],
|
||||
"pain_points": [
|
||||
"low search rankings",
|
||||
"poor content performance",
|
||||
"lack of organic traffic"
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
# Verify brief structure
|
||||
self.assertIsInstance(brief, dict)
|
||||
self.assertIn('title', brief)
|
||||
self.assertIn('content_type', brief)
|
||||
self.assertIn('outline', brief)
|
||||
self.assertIn('key_points', brief)
|
||||
self.assertIn('content_flow', brief)
|
||||
self.assertIn('target_audience', brief)
|
||||
self.assertIn('seo_data', brief)
|
||||
self.assertIn('platform_specs', brief)
|
||||
|
||||
# Verify outline structure
|
||||
outline = brief['outline']
|
||||
self.assertIn('main_headings', outline)
|
||||
self.assertIn('subheadings', outline)
|
||||
|
||||
# Verify key points
|
||||
self.assertIsInstance(brief['key_points'], list)
|
||||
|
||||
# Verify content flow
|
||||
flow = brief['content_flow']
|
||||
self.assertIn('introduction', flow)
|
||||
self.assertIn('main_sections', flow)
|
||||
self.assertIn('conclusion', flow)
|
||||
self.assertIn('transitions', flow)
|
||||
self.assertIn('content_pacing', flow)
|
||||
|
||||
def test_generate_brief_without_audience(self):
|
||||
"""Test content brief generation without target audience data."""
|
||||
brief = self.generator.generate_brief(
|
||||
content_item=self.test_content_item
|
||||
)
|
||||
|
||||
self.assertIsInstance(brief, dict)
|
||||
self.assertIn('target_audience', brief)
|
||||
self.assertEqual(brief['target_audience'], {})
|
||||
|
||||
def test_generate_outline(self):
|
||||
"""Test outline generation."""
|
||||
outline = self.generator._generate_outline(self.test_content_item)
|
||||
|
||||
self.assertIsInstance(outline, dict)
|
||||
self.assertIn('main_headings', outline)
|
||||
self.assertIn('subheadings', outline)
|
||||
|
||||
# Verify main headings
|
||||
main_headings = outline['main_headings']
|
||||
self.assertIsInstance(main_headings, list)
|
||||
for heading in main_headings:
|
||||
self.assertIn('title', heading)
|
||||
self.assertIn('level', heading)
|
||||
self.assertIn('keywords', heading)
|
||||
self.assertIn('summary', heading)
|
||||
|
||||
# Verify subheadings
|
||||
subheadings = outline['subheadings']
|
||||
self.assertIsInstance(subheadings, dict)
|
||||
for heading_title, heading_subheadings in subheadings.items():
|
||||
self.assertIsInstance(heading_subheadings, list)
|
||||
for subheading in heading_subheadings:
|
||||
self.assertIn('title', subheading)
|
||||
self.assertIn('level', subheading)
|
||||
self.assertIn('keywords', subheading)
|
||||
self.assertIn('summary', subheading)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,171 +0,0 @@
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any
|
||||
|
||||
from ..integrations.integration_manager import IntegrationManager
|
||||
|
||||
class TestIntegrationManager(unittest.TestCase):
|
||||
"""Test cases for the IntegrationManager class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.integration_manager = IntegrationManager()
|
||||
self.start_date = datetime.now()
|
||||
self.end_date = self.start_date + timedelta(days=30)
|
||||
self.platforms = ['instagram', 'twitter', 'linkedin', 'blog', 'facebook']
|
||||
self.content_types = ['article', 'social', 'video']
|
||||
self.target_audience = {
|
||||
'age_range': '25-34',
|
||||
'interests': ['technology', 'marketing'],
|
||||
'location': 'global'
|
||||
}
|
||||
self.industry = 'technology'
|
||||
self.keywords = ['AI', 'content marketing', 'social media']
|
||||
|
||||
# Sample content item
|
||||
self.sample_content = {
|
||||
'title': 'The Future of AI in Content Marketing',
|
||||
'content': 'AI is revolutionizing content marketing...',
|
||||
'content_type': 'article',
|
||||
'keywords': ['AI', 'content marketing', 'automation'],
|
||||
'target_audience': self.target_audience,
|
||||
'industry': self.industry
|
||||
}
|
||||
|
||||
def test_create_cross_platform_calendar(self):
|
||||
"""Test creating a cross-platform content calendar."""
|
||||
calendar = self.integration_manager.create_cross_platform_calendar(
|
||||
start_date=self.start_date,
|
||||
end_date=self.end_date,
|
||||
platforms=self.platforms,
|
||||
content_types=self.content_types,
|
||||
target_audience=self.target_audience,
|
||||
industry=self.industry,
|
||||
keywords=self.keywords
|
||||
)
|
||||
|
||||
# Check basic structure
|
||||
self.assertIn('base_calendar', calendar)
|
||||
self.assertIn('platform_calendars', calendar)
|
||||
self.assertIn('metadata', calendar)
|
||||
|
||||
# Check platform calendars
|
||||
platform_calendars = calendar['platform_calendars']
|
||||
self.assertEqual(len(platform_calendars), len(self.platforms))
|
||||
|
||||
for platform in self.platforms:
|
||||
self.assertIn(platform, platform_calendars)
|
||||
platform_calendar = platform_calendars[platform]
|
||||
self.assertIn('content_items', platform_calendar)
|
||||
self.assertIn('metadata', platform_calendar)
|
||||
|
||||
def test_adapt_calendar_for_platform(self):
|
||||
"""Test adapting calendar for a specific platform."""
|
||||
# Create base calendar
|
||||
calendar = self.integration_manager.create_cross_platform_calendar(
|
||||
start_date=self.start_date,
|
||||
end_date=self.end_date,
|
||||
platforms=[self.platforms[0]], # Test with just Instagram
|
||||
content_types=self.content_types,
|
||||
target_audience=self.target_audience,
|
||||
industry=self.industry,
|
||||
keywords=self.keywords
|
||||
)
|
||||
|
||||
# Get platform calendar
|
||||
platform_calendar = calendar['platform_calendars'][self.platforms[0]]
|
||||
|
||||
# Check structure
|
||||
self.assertIn('content_items', platform_calendar)
|
||||
self.assertIn('metadata', platform_calendar)
|
||||
|
||||
# Check content items
|
||||
for item in platform_calendar['content_items']:
|
||||
self.assertIn('original_item', item)
|
||||
self.assertIn('adapted_content', item)
|
||||
self.assertIn('platform_specifics', item)
|
||||
|
||||
def test_adapt_content_item(self):
|
||||
"""Test adapting a content item for a platform."""
|
||||
adapted_item = self.integration_manager._adapt_content_item(
|
||||
item=self.sample_content,
|
||||
platform='instagram'
|
||||
)
|
||||
|
||||
# Check structure
|
||||
self.assertIsNotNone(adapted_item)
|
||||
self.assertIn('original_item', adapted_item)
|
||||
self.assertIn('adapted_content', adapted_item)
|
||||
self.assertIn('platform_specifics', adapted_item)
|
||||
|
||||
# Check content adaptation
|
||||
adapted_content = adapted_item['adapted_content']
|
||||
self.assertIn('captions', adapted_content)
|
||||
self.assertIn('hashtags', adapted_content)
|
||||
self.assertIn('media_suggestions', adapted_content)
|
||||
|
||||
def test_get_platform_suggestions(self):
|
||||
"""Test getting platform-specific suggestions."""
|
||||
suggestions = self.integration_manager.get_platform_suggestions(
|
||||
content=self.sample_content,
|
||||
platforms=self.platforms
|
||||
)
|
||||
|
||||
# Check structure
|
||||
self.assertEqual(len(suggestions), len(self.platforms))
|
||||
|
||||
for platform in self.platforms:
|
||||
self.assertIn(platform, suggestions)
|
||||
platform_suggestions = suggestions[platform]
|
||||
self.assertIsInstance(platform_suggestions, dict)
|
||||
|
||||
def test_validate_platform_content(self):
|
||||
"""Test validating content for a platform."""
|
||||
validation = self.integration_manager.validate_platform_content(
|
||||
content=self.sample_content,
|
||||
platform='instagram'
|
||||
)
|
||||
|
||||
# Check structure
|
||||
self.assertIn('platform', validation)
|
||||
self.assertIn('is_valid', validation)
|
||||
self.assertIn('specifications', validation)
|
||||
|
||||
# Check validation result
|
||||
self.assertIsInstance(validation['is_valid'], bool)
|
||||
|
||||
def test_optimize_cross_platform_content(self):
|
||||
"""Test optimizing content for multiple platforms."""
|
||||
optimized = self.integration_manager.optimize_cross_platform_content(
|
||||
content=self.sample_content,
|
||||
platforms=self.platforms
|
||||
)
|
||||
|
||||
# Check structure
|
||||
self.assertEqual(len(optimized), len(self.platforms))
|
||||
|
||||
for platform in self.platforms:
|
||||
self.assertIn(platform, optimized)
|
||||
platform_optimized = optimized[platform]
|
||||
self.assertIsInstance(platform_optimized, dict)
|
||||
|
||||
def test_error_handling(self):
|
||||
"""Test error handling with invalid inputs."""
|
||||
# Test with invalid platform
|
||||
with self.assertRaises(Exception):
|
||||
self.integration_manager.validate_platform_content(
|
||||
content=self.sample_content,
|
||||
platform='invalid_platform'
|
||||
)
|
||||
|
||||
# Test with invalid content
|
||||
invalid_content = {'title': 'Invalid Content'}
|
||||
validation = self.integration_manager.validate_platform_content(
|
||||
content=invalid_content,
|
||||
platform='instagram'
|
||||
)
|
||||
self.assertFalse(validation['is_valid'])
|
||||
self.assertIn('error', validation)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,186 +0,0 @@
|
||||
import unittest
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from ..integrations.platform_adapters import UnifiedPlatformAdapter
|
||||
|
||||
class TestUnifiedPlatformAdapter(unittest.TestCase):
|
||||
"""Test cases for the UnifiedPlatformAdapter."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test cases."""
|
||||
self.adapter = UnifiedPlatformAdapter()
|
||||
self.test_content = {
|
||||
'title': 'Test Content',
|
||||
'content': 'This is a test content for platform adaptation.',
|
||||
'keywords': ['test', 'content', 'platform'],
|
||||
'tone': 'professional',
|
||||
'cta': 'Learn More',
|
||||
'audience': 'For All',
|
||||
'language': 'English',
|
||||
'industry': 'technology',
|
||||
'word_count': 1000
|
||||
}
|
||||
|
||||
def test_adapt_instagram_content(self):
|
||||
"""Test Instagram content adaptation."""
|
||||
adapted_content = self.adapter.adapt_content(
|
||||
content=self.test_content,
|
||||
platform='instagram'
|
||||
)
|
||||
|
||||
self.assertIsInstance(adapted_content, dict)
|
||||
self.assertIn('captions', adapted_content)
|
||||
self.assertIn('hashtags', adapted_content)
|
||||
self.assertIn('media_suggestions', adapted_content)
|
||||
self.assertIn('platform_specific', adapted_content)
|
||||
|
||||
def test_adapt_twitter_content(self):
|
||||
"""Test Twitter content adaptation."""
|
||||
adapted_content = self.adapter.adapt_content(
|
||||
content=self.test_content,
|
||||
platform='twitter'
|
||||
)
|
||||
|
||||
self.assertIsInstance(adapted_content, dict)
|
||||
self.assertIn('tweets', adapted_content)
|
||||
self.assertIn('thread_structure', adapted_content)
|
||||
self.assertIn('media_suggestions', adapted_content)
|
||||
self.assertIn('platform_specific', adapted_content)
|
||||
|
||||
def test_adapt_linkedin_content(self):
|
||||
"""Test LinkedIn content adaptation."""
|
||||
adapted_content = self.adapter.adapt_content(
|
||||
content=self.test_content,
|
||||
platform='linkedin'
|
||||
)
|
||||
|
||||
self.assertIsInstance(adapted_content, dict)
|
||||
self.assertIn('post', adapted_content)
|
||||
self.assertIn('engagement_optimization', adapted_content)
|
||||
self.assertIn('media_suggestions', adapted_content)
|
||||
self.assertIn('platform_specific', adapted_content)
|
||||
|
||||
def test_adapt_blog_content(self):
|
||||
"""Test blog content adaptation."""
|
||||
adapted_content = self.adapter.adapt_content(
|
||||
content=self.test_content,
|
||||
platform='blog'
|
||||
)
|
||||
|
||||
self.assertIsInstance(adapted_content, dict)
|
||||
self.assertIn('post', adapted_content)
|
||||
self.assertIn('seo_optimization', adapted_content)
|
||||
self.assertIn('media_suggestions', adapted_content)
|
||||
self.assertIn('platform_specific', adapted_content)
|
||||
|
||||
def test_adapt_facebook_content(self):
|
||||
"""Test Facebook content adaptation."""
|
||||
adapted_content = self.adapter.adapt_content(
|
||||
content=self.test_content,
|
||||
platform='facebook'
|
||||
)
|
||||
|
||||
self.assertIsInstance(adapted_content, dict)
|
||||
self.assertIn('post', adapted_content)
|
||||
self.assertIn('engagement_optimization', adapted_content)
|
||||
self.assertIn('media_suggestions', adapted_content)
|
||||
self.assertIn('platform_specific', adapted_content)
|
||||
|
||||
def test_validate_content(self):
|
||||
"""Test content validation."""
|
||||
# Test valid content
|
||||
self.assertTrue(
|
||||
self.adapter.validate_content(
|
||||
self.test_content,
|
||||
'instagram'
|
||||
)
|
||||
)
|
||||
|
||||
# Test invalid content (missing required fields)
|
||||
invalid_content = {
|
||||
'title': 'Test Content',
|
||||
'content': 'This is a test content.'
|
||||
}
|
||||
self.assertFalse(
|
||||
self.adapter.validate_content(
|
||||
invalid_content,
|
||||
'instagram'
|
||||
)
|
||||
)
|
||||
|
||||
def test_unsupported_platform(self):
|
||||
"""Test handling of unsupported platform."""
|
||||
with self.assertRaises(ValueError):
|
||||
self.adapter.adapt_content(
|
||||
content=self.test_content,
|
||||
platform='unsupported_platform'
|
||||
)
|
||||
|
||||
def test_content_adaptation_with_context(self):
|
||||
"""Test content adaptation with additional context."""
|
||||
context = {
|
||||
'target_audience': 'professionals',
|
||||
'campaign_goals': ['awareness', 'engagement'],
|
||||
'brand_voice': 'authoritative'
|
||||
}
|
||||
|
||||
adapted_content = self.adapter.adapt_content(
|
||||
content=self.test_content,
|
||||
platform='linkedin',
|
||||
context=context
|
||||
)
|
||||
|
||||
self.assertIsInstance(adapted_content, dict)
|
||||
self.assertIn('post', adapted_content)
|
||||
self.assertIn('engagement_optimization', adapted_content)
|
||||
|
||||
def test_error_handling(self):
|
||||
"""Test error handling in content adaptation."""
|
||||
# Test with invalid content structure
|
||||
invalid_content = {
|
||||
'title': 123, # Invalid type
|
||||
'content': None # Missing required field
|
||||
}
|
||||
|
||||
adapted_content = self.adapter.adapt_content(
|
||||
content=invalid_content,
|
||||
platform='blog'
|
||||
)
|
||||
|
||||
self.assertIn('error', adapted_content)
|
||||
|
||||
def test_platform_specs(self):
|
||||
"""Test platform specifications."""
|
||||
specs = self.adapter.platform_specs
|
||||
|
||||
# Check Instagram specs
|
||||
self.assertIn('instagram', specs)
|
||||
self.assertIn('max_caption_length', specs['instagram'])
|
||||
self.assertIn('max_hashtags', specs['instagram'])
|
||||
self.assertIn('required_fields', specs['instagram'])
|
||||
|
||||
# Check Twitter specs
|
||||
self.assertIn('twitter', specs)
|
||||
self.assertIn('max_tweet_length', specs['twitter'])
|
||||
self.assertIn('max_thread_length', specs['twitter'])
|
||||
self.assertIn('required_fields', specs['twitter'])
|
||||
|
||||
# Check LinkedIn specs
|
||||
self.assertIn('linkedin', specs)
|
||||
self.assertIn('max_post_length', specs['linkedin'])
|
||||
self.assertIn('required_fields', specs['linkedin'])
|
||||
|
||||
# Check blog specs
|
||||
self.assertIn('blog', specs)
|
||||
self.assertIn('min_word_count', specs['blog'])
|
||||
self.assertIn('max_word_count', specs['blog'])
|
||||
self.assertIn('required_fields', specs['blog'])
|
||||
|
||||
# Check Facebook specs
|
||||
self.assertIn('facebook', specs)
|
||||
self.assertIn('max_post_length', specs['facebook'])
|
||||
self.assertIn('required_fields', specs['facebook'])
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,132 +0,0 @@
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
|
||||
from ..integrations.seo_optimizer import SEOOptimizer
|
||||
|
||||
class TestSEOOptimizer(unittest.TestCase):
|
||||
"""Test cases for the SEOOptimizer class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.seo_optimizer = SEOOptimizer()
|
||||
|
||||
# Sample content for testing
|
||||
self.sample_content = {
|
||||
'title': 'The Future of AI in Content Marketing',
|
||||
'content': 'AI is revolutionizing content marketing...',
|
||||
'keywords': ['AI', 'content marketing', 'automation'],
|
||||
'author': 'John Doe',
|
||||
'publish_date': datetime.now().isoformat(),
|
||||
'description': 'An in-depth look at AI in content marketing',
|
||||
'image_url': 'https://example.com/image.jpg',
|
||||
'url': 'https://example.com/article'
|
||||
}
|
||||
|
||||
# Sample calendar for testing
|
||||
self.sample_calendar = {
|
||||
'metadata': {
|
||||
'start_date': datetime.now().isoformat(),
|
||||
'end_date': datetime.now().isoformat(),
|
||||
'platforms': ['blog', 'social'],
|
||||
'content_types': ['article']
|
||||
},
|
||||
'content_items': [self.sample_content]
|
||||
}
|
||||
|
||||
def test_optimize_content(self):
|
||||
"""Test content optimization."""
|
||||
optimized = self.seo_optimizer.optimize_content(
|
||||
content=self.sample_content,
|
||||
content_type='article',
|
||||
language='English',
|
||||
search_intent='Informational Intent'
|
||||
)
|
||||
|
||||
# Check structure
|
||||
self.assertIn('original_content', optimized)
|
||||
self.assertIn('seo_optimized', optimized)
|
||||
|
||||
# Check SEO elements
|
||||
seo_elements = optimized['seo_optimized']
|
||||
self.assertIn('title', seo_elements)
|
||||
self.assertIn('meta_description', seo_elements)
|
||||
self.assertIn('structured_data', seo_elements)
|
||||
self.assertIn('keywords', seo_elements)
|
||||
|
||||
def test_optimize_title(self):
|
||||
"""Test title optimization."""
|
||||
titles = self.seo_optimizer._optimize_title(
|
||||
title=self.sample_content['title'],
|
||||
keywords=self.sample_content['keywords'],
|
||||
content_type='article',
|
||||
language='English',
|
||||
search_intent='Informational Intent'
|
||||
)
|
||||
|
||||
# Check titles
|
||||
self.assertIsInstance(titles, list)
|
||||
self.assertTrue(len(titles) > 0)
|
||||
|
||||
def test_generate_meta_description(self):
|
||||
"""Test meta description generation."""
|
||||
descriptions = self.seo_optimizer._generate_meta_description(
|
||||
keywords=self.sample_content['keywords'],
|
||||
content_type='article',
|
||||
language='English',
|
||||
search_intent='Informational Intent'
|
||||
)
|
||||
|
||||
# Check descriptions
|
||||
self.assertIsInstance(descriptions, list)
|
||||
self.assertTrue(len(descriptions) > 0)
|
||||
|
||||
def test_generate_structured_data(self):
|
||||
"""Test structured data generation."""
|
||||
structured_data = self.seo_optimizer._generate_structured_data(
|
||||
content=self.sample_content,
|
||||
content_type='article'
|
||||
)
|
||||
|
||||
# Check structured data
|
||||
self.assertIsNotNone(structured_data)
|
||||
|
||||
def test_optimize_calendar_content(self):
|
||||
"""Test calendar content optimization."""
|
||||
optimized_calendar = self.seo_optimizer.optimize_calendar_content(
|
||||
calendar=self.sample_calendar,
|
||||
content_type='article',
|
||||
language='English',
|
||||
search_intent='Informational Intent'
|
||||
)
|
||||
|
||||
# Check structure
|
||||
self.assertIn('metadata', optimized_calendar)
|
||||
self.assertIn('content_items', optimized_calendar)
|
||||
|
||||
# Check content items
|
||||
self.assertTrue(len(optimized_calendar['content_items']) > 0)
|
||||
for item in optimized_calendar['content_items']:
|
||||
self.assertIn('original_content', item)
|
||||
self.assertIn('seo_optimized', item)
|
||||
|
||||
def test_error_handling(self):
|
||||
"""Test error handling with invalid inputs."""
|
||||
# Test with invalid content
|
||||
invalid_content = {'title': 'Invalid Content'}
|
||||
optimized = self.seo_optimizer.optimize_content(
|
||||
content=invalid_content,
|
||||
content_type='article'
|
||||
)
|
||||
self.assertIn('error', optimized)
|
||||
|
||||
# Test with invalid calendar
|
||||
invalid_calendar = {'metadata': {}}
|
||||
optimized_calendar = self.seo_optimizer.optimize_calendar_content(
|
||||
calendar=invalid_calendar,
|
||||
content_type='article'
|
||||
)
|
||||
self.assertIn('error', optimized_calendar)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,125 +1,188 @@
|
||||
import streamlit as st
|
||||
from typing import Dict, Any, List
|
||||
from lib.ai_seo_tools.content_calendar.models.calendar import ContentItem
|
||||
from lib.database.models import ContentItem
|
||||
import logging
|
||||
from lib.ai_seo_tools.content_calendar.core.content_generator import ContentGenerator
|
||||
from lib.ai_seo_tools.content_calendar.core.calendar_manager import CalendarManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def render_ab_testing(
|
||||
content_generator,
|
||||
calendar_manager
|
||||
) -> None:
|
||||
def render_ab_testing(content_generator: ContentGenerator, calendar_manager: CalendarManager):
|
||||
"""Render the A/B testing interface."""
|
||||
try:
|
||||
st.header("A/B Testing")
|
||||
|
||||
# Test Configuration
|
||||
st.markdown("### Create A/B Test")
|
||||
col1, col2 = st.columns([2, 1])
|
||||
|
||||
with col1:
|
||||
test_content = st.selectbox(
|
||||
"Select content for A/B testing",
|
||||
options=[item.title for item in calendar_manager.get_calendar().get_all_content()],
|
||||
key="ab_test_content_select"
|
||||
)
|
||||
|
||||
with col2:
|
||||
num_variants = st.slider(
|
||||
"Number of variants",
|
||||
min_value=2,
|
||||
max_value=5,
|
||||
value=2,
|
||||
help="Number of different versions to test"
|
||||
)
|
||||
|
||||
if test_content:
|
||||
content_item = next(
|
||||
item for item in calendar_manager.get_calendar().get_all_content()
|
||||
if item.title == test_content
|
||||
)
|
||||
|
||||
# Test Settings
|
||||
with st.expander("Test Settings"):
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
test_duration = st.number_input(
|
||||
"Test Duration (days)",
|
||||
min_value=1,
|
||||
max_value=30,
|
||||
value=7
|
||||
)
|
||||
target_metric = st.selectbox(
|
||||
"Primary Metric",
|
||||
options=['Engagement', 'Conversion', 'Reach', 'Click-through'],
|
||||
value='Engagement'
|
||||
)
|
||||
with col2:
|
||||
audience_size = st.select_slider(
|
||||
"Audience Size",
|
||||
options=['Small', 'Medium', 'Large'],
|
||||
value='Medium'
|
||||
)
|
||||
confidence_level = st.slider(
|
||||
"Confidence Level",
|
||||
min_value=90,
|
||||
max_value=99,
|
||||
value=95,
|
||||
help="Statistical confidence level for test results"
|
||||
)
|
||||
|
||||
# Generate Variants
|
||||
if st.button("Generate Variants"):
|
||||
with st.spinner("Generating variants..."):
|
||||
variants = _generate_ab_test_variants(content_generator, content_item, num_variants)
|
||||
if variants:
|
||||
st.success(f"Generated {len(variants)} variants!")
|
||||
|
||||
# Display variants in tabs
|
||||
variant_tabs = st.tabs([f"Variant {i+1}" for i in range(len(variants))])
|
||||
for i, tab in enumerate(variant_tabs):
|
||||
with tab:
|
||||
st.markdown(f"### Variant {i+1}")
|
||||
st.json(variants[i]['content'])
|
||||
|
||||
# Variant metrics
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.metric(
|
||||
"Engagement Score",
|
||||
f"{variants[i]['metrics']['engagement_score']:.1f}%"
|
||||
)
|
||||
with col2:
|
||||
st.metric(
|
||||
"Conversion Rate",
|
||||
f"{variants[i]['metrics']['conversion_rate']:.1f}%"
|
||||
)
|
||||
with col3:
|
||||
st.metric(
|
||||
"Reach",
|
||||
f"{variants[i]['metrics']['reach']:,}"
|
||||
)
|
||||
|
||||
# Results Analysis
|
||||
st.markdown("### Analyze Results")
|
||||
if test_content in st.session_state.ab_test_results:
|
||||
test_data = st.session_state.ab_test_results[test_content]
|
||||
|
||||
# Test Status
|
||||
st.info(f"Test Status: {test_data['status']}")
|
||||
st.write(f"Started: {test_data['start_time']}")
|
||||
|
||||
if test_data['status'] == 'running':
|
||||
if st.button("End Test and Analyze"):
|
||||
with st.spinner("Analyzing results..."):
|
||||
results = _analyze_ab_test_results(content_item)
|
||||
if results:
|
||||
st.success("Analysis complete!")
|
||||
_display_test_results(results)
|
||||
st.header("A/B Testing")
|
||||
|
||||
# Check if calendar manager is available
|
||||
if 'calendar_manager' not in st.session_state:
|
||||
st.error("Calendar manager not initialized. Please refresh the page.")
|
||||
return
|
||||
|
||||
# Get available content
|
||||
try:
|
||||
available_content = calendar_manager.get_calendar().get_all_content()
|
||||
content_options = [item.title for item in available_content]
|
||||
except Exception as e:
|
||||
logger.error(f"Error in A/B testing interface: {str(e)}", exc_info=True)
|
||||
st.error(f"Error in A/B testing: {str(e)}")
|
||||
logger.error(f"Error getting content options: {str(e)}")
|
||||
st.error("Error loading content. Please try again.")
|
||||
return
|
||||
|
||||
if not content_options:
|
||||
st.info("""
|
||||
## Welcome to A/B Testing! 🧪
|
||||
|
||||
Test different versions of your content to find what works best. Here's what you can do:
|
||||
|
||||
### Features:
|
||||
- 🔄 **Variant Generation**: Create multiple versions of your content
|
||||
- 📊 **Performance Tracking**: Compare metrics across variants
|
||||
- 📈 **Statistical Analysis**: Get data-driven insights
|
||||
- 🎯 **Winner Selection**: Identify the best performing content
|
||||
|
||||
### Getting Started:
|
||||
1. First, add some content to your calendar
|
||||
2. Select the content you want to test
|
||||
3. Generate variants with different parameters
|
||||
4. Track performance and analyze results
|
||||
|
||||
Ready to get started? Add some content to your calendar first!
|
||||
""")
|
||||
return
|
||||
|
||||
# Content Selection
|
||||
selected_content = st.selectbox(
|
||||
"Select content to test",
|
||||
options=content_options,
|
||||
key="ab_test_content_select"
|
||||
)
|
||||
|
||||
if selected_content:
|
||||
try:
|
||||
content_item = next(
|
||||
item for item in available_content
|
||||
if item.title == selected_content
|
||||
)
|
||||
|
||||
# Show onboarding info if no test history
|
||||
if not st.session_state.get('ab_test_results', {}).get(content_item.title):
|
||||
st.info("""
|
||||
### A/B Testing Guide
|
||||
|
||||
Create and compare different versions of your content:
|
||||
|
||||
- **Headline Variations**: Test different titles and hooks
|
||||
- **Content Structure**: Try different content flows
|
||||
- **Call-to-Action**: Test various CTAs
|
||||
- **Visual Elements**: Compare different media placements
|
||||
|
||||
Click 'Generate Test Variants' to get started!
|
||||
""")
|
||||
|
||||
# Test Configuration
|
||||
st.markdown("### Create A/B Test")
|
||||
col1, col2 = st.columns([2, 1])
|
||||
|
||||
with col1:
|
||||
test_content = st.selectbox(
|
||||
"Select content to A/B test",
|
||||
options=content_options,
|
||||
key="ab_test_content_select_unique"
|
||||
)
|
||||
|
||||
with col2:
|
||||
num_variants = st.slider(
|
||||
"Number of variants",
|
||||
min_value=2,
|
||||
max_value=5,
|
||||
value=2,
|
||||
help="Number of different versions to test"
|
||||
)
|
||||
|
||||
if test_content:
|
||||
content_item = next(
|
||||
item for item in calendar_manager.get_calendar().get_all_content()
|
||||
if item.title == test_content
|
||||
)
|
||||
|
||||
# Test Settings
|
||||
with st.expander("Test Settings"):
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
test_duration = st.number_input(
|
||||
"Test Duration (days)",
|
||||
min_value=1,
|
||||
max_value=30,
|
||||
value=7
|
||||
)
|
||||
target_metric = st.selectbox(
|
||||
"Primary Metric",
|
||||
options=['Engagement', 'Conversion', 'Reach', 'Click-through'],
|
||||
index=0
|
||||
)
|
||||
with col2:
|
||||
audience_size = st.select_slider(
|
||||
"Audience Size",
|
||||
options=['Small', 'Medium', 'Large'],
|
||||
value='Medium'
|
||||
)
|
||||
confidence_level = st.slider(
|
||||
"Confidence Level",
|
||||
min_value=90,
|
||||
max_value=99,
|
||||
value=95,
|
||||
help="Statistical confidence level for test results"
|
||||
)
|
||||
|
||||
# Generate Variants
|
||||
if st.button("Generate Variants"):
|
||||
with st.spinner("Generating variants..."):
|
||||
variants = _generate_ab_test_variants(content_generator, content_item, num_variants)
|
||||
if variants:
|
||||
st.success(f"Generated {len(variants)} variants!")
|
||||
|
||||
# Display variants in tabs
|
||||
variant_tabs = st.tabs([f"Variant {i+1}" for i in range(len(variants))])
|
||||
for i, tab in enumerate(variant_tabs):
|
||||
with tab:
|
||||
st.markdown(f"### Variant {i+1}")
|
||||
st.json(variants[i]['content'])
|
||||
|
||||
# Variant metrics
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.metric(
|
||||
"Engagement Score",
|
||||
f"{variants[i]['metrics']['engagement_score']:.1f}%"
|
||||
)
|
||||
with col2:
|
||||
st.metric(
|
||||
"Conversion Rate",
|
||||
f"{variants[i]['metrics']['conversion_rate']:.1f}%"
|
||||
)
|
||||
with col3:
|
||||
st.metric(
|
||||
"Reach",
|
||||
f"{variants[i]['metrics']['reach']:,}"
|
||||
)
|
||||
|
||||
# Results Analysis
|
||||
st.markdown("### Analyze Results")
|
||||
if test_content in st.session_state.ab_test_results:
|
||||
test_data = st.session_state.ab_test_results[test_content]
|
||||
|
||||
# Test Status
|
||||
st.info(f"Test Status: {test_data['status']}")
|
||||
st.write(f"Started: {test_data['start_time']}")
|
||||
|
||||
if test_data['status'] == 'running':
|
||||
if st.button("End Test and Analyze"):
|
||||
with st.spinner("Analyzing results..."):
|
||||
results = _analyze_ab_test_results(content_item)
|
||||
if results:
|
||||
st.success("Analysis complete!")
|
||||
_display_test_results(results)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in A/B testing interface: {str(e)}", exc_info=True)
|
||||
st.error(f"Error in A/B testing: {str(e)}")
|
||||
|
||||
def _generate_ab_test_variants(
|
||||
content_generator,
|
||||
|
||||
@@ -2,14 +2,19 @@ import streamlit as st
|
||||
from typing import Dict, Any, List
|
||||
from datetime import datetime
|
||||
import pandas as pd
|
||||
from ...core.content_generator import ContentGenerator
|
||||
from ...core.ai_generator import AIGenerator
|
||||
from ...integrations.seo_optimizer import SEOOptimizer
|
||||
from ...models.calendar import ContentItem, ContentType, Platform, SEOData
|
||||
from lib.ai_seo_tools.content_calendar.core.content_generator import ContentGenerator
|
||||
from lib.ai_seo_tools.content_calendar.core.ai_generator import AIGenerator
|
||||
from lib.ai_seo_tools.content_calendar.integrations.seo_optimizer import SEOOptimizer
|
||||
from lib.database.models import ContentItem, ContentType, Platform, SEOData
|
||||
import logging
|
||||
from lib.database.models import get_engine, get_session, init_db
|
||||
|
||||
logger = logging.getLogger('content_calendar.optimization')
|
||||
|
||||
engine = get_engine()
|
||||
init_db(engine)
|
||||
session = get_session(engine)
|
||||
|
||||
class OptimizationManager:
|
||||
def __init__(self):
|
||||
if 'optimization_history' not in st.session_state:
|
||||
@@ -165,7 +170,7 @@ def render_content_optimization(
|
||||
seo_optimizer: SEOOptimizer
|
||||
):
|
||||
"""Render the content optimization interface with advanced features."""
|
||||
st.header("Content Optimization")
|
||||
st.title("Content Calendar")
|
||||
|
||||
# Initialize optimization manager
|
||||
optimization_manager = OptimizationManager()
|
||||
@@ -174,61 +179,257 @@ def render_content_optimization(
|
||||
if 'calendar_manager' not in st.session_state:
|
||||
st.error("Calendar manager not initialized. Please refresh the page.")
|
||||
return
|
||||
|
||||
# Create main tabs
|
||||
main_tabs = st.tabs(["Content Planning", "Content Optimization"])
|
||||
|
||||
# Get available content
|
||||
try:
|
||||
available_content = st.session_state.calendar_manager.get_calendar().get_all_content()
|
||||
content_options = [item.title for item in available_content]
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting content options: {str(e)}")
|
||||
st.error("Error loading content. Please try again.")
|
||||
return
|
||||
|
||||
if not content_options:
|
||||
st.info("No content available for optimization. Please add some content first.")
|
||||
return
|
||||
|
||||
# Content Selection
|
||||
selected_content = st.selectbox(
|
||||
"Select content to optimize",
|
||||
options=content_options,
|
||||
key="optimize_content_select"
|
||||
)
|
||||
|
||||
if selected_content:
|
||||
try:
|
||||
content_item = next(
|
||||
item for item in available_content
|
||||
if item.title == selected_content
|
||||
with main_tabs[0]:
|
||||
# Create two columns for the layout
|
||||
col1, col2 = st.columns([1, 1])
|
||||
|
||||
with col1:
|
||||
st.header("Quick Calendar Generation")
|
||||
st.markdown("""
|
||||
Generate a content calendar in three simple steps:
|
||||
1. Enter your keywords
|
||||
2. Select target platforms
|
||||
3. Choose time period
|
||||
""")
|
||||
|
||||
# Step 1: Keywords Input
|
||||
st.subheader("Step 1: Enter Keywords")
|
||||
keywords = st.text_area(
|
||||
"Enter keywords or topics (one per line)",
|
||||
help="Enter the main topics or keywords you want to create content about"
|
||||
)
|
||||
|
||||
# Create tabs for different optimization aspects
|
||||
opt_tabs = st.tabs(["Content Optimization", "SEO Optimization", "Preview", "History", "Analytics"])
|
||||
# Step 2: Platform Selection
|
||||
st.subheader("Step 2: Select Target Platforms")
|
||||
platform_categories = {
|
||||
"Website": ["WEBSITE"],
|
||||
"Social Media": ["INSTAGRAM", "FACEBOOK", "TWITTER", "LINKEDIN"],
|
||||
"Video": ["YOUTUBE"],
|
||||
"Newsletter": ["NEWSLETTER"]
|
||||
}
|
||||
|
||||
with opt_tabs[0]:
|
||||
st.subheader("Content Optimization")
|
||||
selected_platforms = []
|
||||
for category, platforms in platform_categories.items():
|
||||
st.markdown(f"**{category}**")
|
||||
for platform in platforms:
|
||||
if st.checkbox(platform.replace("_", " ").title(), key=f"platform_{platform}"):
|
||||
selected_platforms.append(platform)
|
||||
|
||||
# Step 3: Time Period
|
||||
st.subheader("Step 3: Choose Time Period")
|
||||
time_period = st.selectbox(
|
||||
"Select time period",
|
||||
["1 Week", "2 Weeks", "1 Month", "3 Months", "6 Months"],
|
||||
help="Choose how far ahead you want to plan your content"
|
||||
)
|
||||
|
||||
# Generate Calendar Button
|
||||
if st.button("Generate with AI", type="primary"):
|
||||
if not keywords or not selected_platforms:
|
||||
st.error("Please enter keywords and select at least one platform.")
|
||||
else:
|
||||
with st.spinner("Generating content calendar..."):
|
||||
try:
|
||||
# Generate content ideas based on keywords
|
||||
content_ideas = []
|
||||
for keyword in keywords.split('\n'):
|
||||
if keyword.strip():
|
||||
# Generate content ideas for each platform
|
||||
for platform in selected_platforms:
|
||||
try:
|
||||
# Create a content item for the AI generator
|
||||
content_item = ContentItem(
|
||||
title=keyword.strip(),
|
||||
description=f"Content about {keyword.strip()}",
|
||||
content_type=ContentType.BLOG_POST if platform == "WEBSITE" else ContentType.SOCIAL_MEDIA,
|
||||
platforms=[Platform[platform]],
|
||||
publish_date=datetime.now(),
|
||||
seo_data=SEOData(
|
||||
title=keyword.strip(),
|
||||
meta_description=f"Content about {keyword.strip()}",
|
||||
keywords=[keyword.strip()],
|
||||
structured_data={}
|
||||
)
|
||||
)
|
||||
|
||||
# Generate content using AI generator
|
||||
content_idea = ai_generator.enhance_content(
|
||||
content=content_item,
|
||||
enhancement_type='content_generation',
|
||||
target_audience={
|
||||
'content_settings': {
|
||||
'tone': 'professional',
|
||||
'length': 'medium',
|
||||
'engagement_goal': 'awareness',
|
||||
'creativity_level': 5
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if content_idea:
|
||||
content_ideas.append({
|
||||
'title': content_idea.get('title', keyword.strip()),
|
||||
'introduction': content_idea.get('content', f"Content about {keyword.strip()}"),
|
||||
'platform': platform,
|
||||
'meta_description': content_idea.get('meta_description', ''),
|
||||
'keywords': [keyword.strip()]
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating content for {keyword} on {platform}: {str(e)}")
|
||||
continue
|
||||
|
||||
if content_ideas:
|
||||
# Create calendar entries
|
||||
calendar = st.session_state.calendar_manager.get_calendar()
|
||||
for idea in content_ideas:
|
||||
try:
|
||||
# Create content item
|
||||
content_item = ContentItem(
|
||||
title=idea['title'],
|
||||
description=idea['introduction'],
|
||||
content_type=ContentType.BLOG_POST if idea['platform'] == "WEBSITE" else ContentType.SOCIAL_MEDIA,
|
||||
platforms=[Platform[idea['platform']]],
|
||||
publish_date=datetime.now(),
|
||||
seo_data=SEOData(
|
||||
title=idea['title'],
|
||||
meta_description=idea.get('meta_description', ''),
|
||||
keywords=idea.get('keywords', []),
|
||||
structured_data={}
|
||||
)
|
||||
)
|
||||
calendar.add_content(content_item)
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding content to calendar: {str(e)}")
|
||||
continue
|
||||
|
||||
st.success("Content calendar generated successfully!")
|
||||
st.rerun() # Refresh to show new content
|
||||
else:
|
||||
st.error("Failed to generate any content ideas. Please try different keywords or settings.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating content calendar: {str(e)}")
|
||||
st.error("An error occurred while generating the content calendar. Please try again.")
|
||||
|
||||
with col2:
|
||||
st.header("Scheduled Content")
|
||||
# Get all content from calendar
|
||||
calendar = st.session_state.calendar_manager.get_calendar()
|
||||
if not calendar:
|
||||
st.info("No content scheduled yet. Generate content using the form on the left.")
|
||||
else:
|
||||
# Group content by platform
|
||||
platform_content = {}
|
||||
for item in calendar.get_all_content():
|
||||
platform = item.platforms[0].name if item.platforms else "Unknown"
|
||||
if platform not in platform_content:
|
||||
platform_content[platform] = []
|
||||
platform_content[platform].append(item)
|
||||
|
||||
# Advanced Optimization Settings
|
||||
with st.expander("Advanced Settings", expanded=True):
|
||||
col1, col2 = st.columns(2)
|
||||
# Create tabs for each platform
|
||||
platform_tabs = st.tabs(list(platform_content.keys()))
|
||||
|
||||
for i, (platform, content) in enumerate(platform_content.items()):
|
||||
with platform_tabs[i]:
|
||||
st.write(f"### {platform} Content")
|
||||
|
||||
# Convert content to DataFrame for better display
|
||||
content_data = []
|
||||
for item in content:
|
||||
content_data.append({
|
||||
'Date': item.publish_date.strftime('%Y-%m-%d'),
|
||||
'Title': item.title,
|
||||
'Type': item.content_type.name,
|
||||
'Status': item.status
|
||||
})
|
||||
|
||||
if content_data:
|
||||
df = pd.DataFrame(content_data)
|
||||
st.dataframe(df, use_container_width=True)
|
||||
|
||||
# Add action buttons for each content item
|
||||
for item in content:
|
||||
with st.expander(f"Actions for: {item.title}"):
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
if st.button("Edit", key=f"edit_{item.title}"):
|
||||
st.session_state.selected_content = item.title
|
||||
with col2:
|
||||
if st.button("Optimize", key=f"optimize_{item.title}"):
|
||||
st.session_state.selected_content = item.title
|
||||
st.session_state.active_tab = "Content Optimization"
|
||||
with col3:
|
||||
if st.button("Delete", key=f"delete_{item.title}"):
|
||||
calendar.remove_content(item)
|
||||
st.success(f"Removed {item.title}")
|
||||
st.rerun()
|
||||
|
||||
with main_tabs[1]:
|
||||
st.header("Content Optimization")
|
||||
# Get available content
|
||||
calendar = st.session_state.calendar_manager.get_calendar()
|
||||
if not calendar:
|
||||
st.info("No content available for optimization. Use the Content Planning tab to generate content.")
|
||||
return
|
||||
|
||||
available_content = calendar.get_all_content()
|
||||
content_options = [item.title for item in available_content]
|
||||
|
||||
# Content selection
|
||||
selected_content = st.selectbox(
|
||||
"Select content to optimize",
|
||||
options=content_options,
|
||||
key="optimize_content_select"
|
||||
)
|
||||
|
||||
if selected_content:
|
||||
try:
|
||||
content_item = next(
|
||||
item for item in available_content
|
||||
if item.title == selected_content
|
||||
)
|
||||
|
||||
# Create tabs for different optimization aspects
|
||||
opt_tabs = st.tabs(["Content Optimization", "SEO Optimization", "Preview", "History", "Analytics"])
|
||||
|
||||
with opt_tabs[0]:
|
||||
st.subheader("Content Optimization")
|
||||
|
||||
# Show onboarding info if no optimization history
|
||||
if not optimization_manager.get_optimization_history(content_item.title):
|
||||
st.info("""
|
||||
### Content Optimization Guide
|
||||
|
||||
Use these tools to enhance your content:
|
||||
|
||||
- **Content Tone**: Adjust the writing style to match your brand voice
|
||||
- **Content Length**: Optimize for your target platform's requirements
|
||||
- **Engagement Goal**: Focus on specific audience actions
|
||||
- **Creativity Level**: Balance between creative and professional content
|
||||
|
||||
Click 'Generate Optimization' to get started!
|
||||
""")
|
||||
|
||||
# Advanced Optimization Settings
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
tone = st.select_slider(
|
||||
"Content Tone",
|
||||
options=['Professional', 'Casual', 'Friendly', 'Authoritative', 'Conversational'],
|
||||
value='Professional'
|
||||
options=["Professional", "Casual", "Educational", "Entertaining", "Persuasive"],
|
||||
value="Professional"
|
||||
)
|
||||
length = st.select_slider(
|
||||
length = st.radio(
|
||||
"Content Length",
|
||||
options=['Short', 'Medium', 'Long', 'Comprehensive'],
|
||||
value='Medium'
|
||||
["Short", "Medium", "Long"],
|
||||
horizontal=True
|
||||
)
|
||||
|
||||
with col2:
|
||||
engagement_goal = st.select_slider(
|
||||
engagement_goal = st.selectbox(
|
||||
"Engagement Goal",
|
||||
options=['Awareness', 'Consideration', 'Conversion', 'Retention'],
|
||||
value='Consideration'
|
||||
["Awareness", "Consideration", "Conversion", "Retention"]
|
||||
)
|
||||
creativity_level = st.slider(
|
||||
"Creativity Level",
|
||||
@@ -236,232 +437,62 @@ def render_content_optimization(
|
||||
max_value=10,
|
||||
value=5
|
||||
)
|
||||
|
||||
# Platform-Specific Optimization
|
||||
st.subheader("Platform-Specific Optimization")
|
||||
platforms = st.multiselect(
|
||||
"Target Platforms",
|
||||
options=[p.name for p in content_item.platforms],
|
||||
default=[p.name for p in content_item.platforms]
|
||||
)
|
||||
|
||||
# Generate Optimization
|
||||
if st.button("Generate Optimization"):
|
||||
with st.spinner("Generating optimization..."):
|
||||
try:
|
||||
# Generate optimized content
|
||||
optimized_content = content_generator.optimize_for_platform(
|
||||
content=content_item,
|
||||
platform=Platform[platforms[0]] if platforms else content_item.platforms[0],
|
||||
requirements={
|
||||
'tone': tone,
|
||||
'length': length,
|
||||
'engagement_goal': engagement_goal,
|
||||
'creativity_level': creativity_level
|
||||
}
|
||||
)
|
||||
|
||||
if optimized_content:
|
||||
# Track optimization
|
||||
optimization_manager.track_optimization(
|
||||
content_item.title,
|
||||
{
|
||||
'type': 'content',
|
||||
'changes': optimized_content.get('changes', []),
|
||||
'metrics': optimized_content.get('metrics', {}),
|
||||
'content': optimized_content.get('content', ''),
|
||||
'engagement_metrics': optimized_content.get('engagement_metrics', {})
|
||||
}
|
||||
|
||||
if st.button("Generate Optimization", type="primary"):
|
||||
with st.spinner("Optimizing content..."):
|
||||
try:
|
||||
# Generate optimization
|
||||
optimization = content_generator.optimize_content(
|
||||
content=content_item,
|
||||
tone=tone,
|
||||
length=length,
|
||||
engagement_goal=engagement_goal,
|
||||
creativity_level=creativity_level
|
||||
)
|
||||
|
||||
# Save preview
|
||||
optimization_manager.save_preview(
|
||||
content_item.title,
|
||||
{
|
||||
'original': content_item.description,
|
||||
'optimized': optimized_content.get('content', ''),
|
||||
'changes': optimized_content.get('changes', []),
|
||||
'metrics': optimized_content.get('metrics', {})
|
||||
}
|
||||
)
|
||||
|
||||
st.success("Content optimized successfully!")
|
||||
except Exception as e:
|
||||
logger.error(f"Error optimizing content: {str(e)}")
|
||||
st.error(f"Error optimizing content: {str(e)}")
|
||||
|
||||
with opt_tabs[1]:
|
||||
st.subheader("SEO Optimization")
|
||||
if optimization:
|
||||
st.success("Content optimized successfully!")
|
||||
|
||||
# Show optimization results
|
||||
st.subheader("Optimization Results")
|
||||
st.write(optimization.get('content', ''))
|
||||
|
||||
# Save optimization history
|
||||
optimization_manager.track_optimization(
|
||||
content_item.title,
|
||||
{
|
||||
'tone': tone,
|
||||
'length': length,
|
||||
'engagement_goal': engagement_goal,
|
||||
'creativity_level': creativity_level,
|
||||
'content': optimization.get('content', ''),
|
||||
'timestamp': datetime.now()
|
||||
}
|
||||
)
|
||||
else:
|
||||
st.error("Failed to optimize content. Please try again.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error optimizing content: {str(e)}")
|
||||
st.error("An error occurred while optimizing content. Please try again.")
|
||||
|
||||
# SEO Settings
|
||||
with st.expander("SEO Settings", expanded=True):
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
keyword_density = st.slider(
|
||||
"Target Keyword Density",
|
||||
min_value=1,
|
||||
max_value=5,
|
||||
value=2,
|
||||
help="Target percentage of keywords in content"
|
||||
)
|
||||
internal_linking = st.checkbox(
|
||||
"Enable Internal Linking",
|
||||
value=True,
|
||||
help="Automatically add internal links to related content"
|
||||
)
|
||||
|
||||
with col2:
|
||||
external_linking = st.checkbox(
|
||||
"Enable External Linking",
|
||||
value=True,
|
||||
help="Add relevant external links for credibility"
|
||||
)
|
||||
structured_data = st.checkbox(
|
||||
"Add Structured Data",
|
||||
value=True,
|
||||
help="Include schema.org structured data"
|
||||
)
|
||||
with opt_tabs[1]:
|
||||
st.subheader("SEO Optimization")
|
||||
# SEO optimization content here
|
||||
|
||||
# Generate SEO Optimization
|
||||
if st.button("Generate SEO Optimization"):
|
||||
with st.spinner("Generating SEO optimization..."):
|
||||
try:
|
||||
# Generate SEO-optimized content
|
||||
seo_optimized = seo_optimizer.optimize_content(
|
||||
content=content_item,
|
||||
content_type=content_item.content_type.name,
|
||||
language='English',
|
||||
search_intent='Informational Intent',
|
||||
settings={
|
||||
'keyword_density': keyword_density,
|
||||
'internal_linking': internal_linking,
|
||||
'external_linking': external_linking,
|
||||
'structured_data': structured_data
|
||||
}
|
||||
)
|
||||
|
||||
if seo_optimized:
|
||||
# Track optimization
|
||||
optimization_manager.track_optimization(
|
||||
content_item.title,
|
||||
{
|
||||
'type': 'seo',
|
||||
'changes': seo_optimized.get('changes', []),
|
||||
'metrics': seo_optimized.get('metrics', {}),
|
||||
'seo_data': seo_optimized
|
||||
}
|
||||
)
|
||||
|
||||
# Save preview
|
||||
optimization_manager.save_preview(
|
||||
content_item.title,
|
||||
{
|
||||
'meta_description': seo_optimized.get('meta_description', ''),
|
||||
'keywords': seo_optimized.get('keywords', []),
|
||||
'structured_data': seo_optimized.get('structured_data', {}),
|
||||
'changes': seo_optimized.get('changes', [])
|
||||
}
|
||||
)
|
||||
|
||||
st.success("SEO optimization completed!")
|
||||
except Exception as e:
|
||||
logger.error(f"Error optimizing SEO: {str(e)}")
|
||||
st.error(f"Error optimizing SEO: {str(e)}")
|
||||
|
||||
with opt_tabs[2]:
|
||||
st.subheader("Optimization Preview")
|
||||
with opt_tabs[2]:
|
||||
st.subheader("Content Preview")
|
||||
# Content preview here
|
||||
|
||||
preview_data = optimization_manager.get_preview(content_item.title)
|
||||
if preview_data:
|
||||
# Content Preview
|
||||
if 'original' in preview_data:
|
||||
st.markdown("### Content Changes")
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.markdown("#### Original Content")
|
||||
st.write(preview_data['original'])
|
||||
|
||||
with col2:
|
||||
st.markdown("#### Optimized Content")
|
||||
st.write(preview_data['optimized'])
|
||||
|
||||
st.markdown("#### Key Changes")
|
||||
for change in preview_data.get('changes', []):
|
||||
st.write(f"- {change}")
|
||||
|
||||
# SEO Preview
|
||||
if 'meta_description' in preview_data:
|
||||
st.markdown("### SEO Changes")
|
||||
st.markdown("#### Meta Description")
|
||||
st.write(preview_data['meta_description'])
|
||||
|
||||
st.markdown("#### Keywords")
|
||||
st.write(", ".join(preview_data['keywords']))
|
||||
|
||||
st.markdown("#### Structured Data")
|
||||
st.json(preview_data['structured_data'])
|
||||
|
||||
# Metrics Preview
|
||||
if 'metrics' in preview_data:
|
||||
st.markdown("### Optimization Metrics")
|
||||
metrics = preview_data['metrics']
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
st.metric("Readability Score", f"{metrics.get('readability_score', 0):.1%}")
|
||||
with col2:
|
||||
st.metric("SEO Score", f"{metrics.get('seo_score', 0):.1%}")
|
||||
with col3:
|
||||
st.metric("Engagement Potential", f"{metrics.get('engagement_potential', 0):.1%}")
|
||||
else:
|
||||
st.info("No optimization preview available. Generate optimization first.")
|
||||
|
||||
with opt_tabs[3]:
|
||||
st.subheader("Optimization History")
|
||||
with opt_tabs[3]:
|
||||
st.subheader("Optimization History")
|
||||
# Optimization history here
|
||||
|
||||
history = optimization_manager.get_optimization_history(content_item.title)
|
||||
if history:
|
||||
for entry in history:
|
||||
with st.expander(f"Optimization at {entry['timestamp']}"):
|
||||
st.write(f"Type: {entry['type']}")
|
||||
st.write("Changes:")
|
||||
for change in entry.get('changes', []):
|
||||
st.write(f"- {change}")
|
||||
|
||||
if 'metrics' in entry:
|
||||
st.write("Metrics:")
|
||||
st.json(entry['metrics'])
|
||||
else:
|
||||
st.info("No optimization history available.")
|
||||
|
||||
with opt_tabs[4]:
|
||||
st.subheader("Optimization Analytics")
|
||||
|
||||
metrics_history = optimization_manager.get_optimization_metrics(content_item.title)
|
||||
if metrics_history:
|
||||
# Convert metrics history to DataFrame
|
||||
df = pd.DataFrame(metrics_history)
|
||||
with opt_tabs[4]:
|
||||
st.subheader("Performance Analytics")
|
||||
# Analytics content here
|
||||
|
||||
# Plot metrics over time
|
||||
st.line_chart(df[['readability_score', 'seo_score', 'engagement_potential', 'content_quality']])
|
||||
|
||||
# Display current metrics
|
||||
current_metrics = metrics_history[-1]
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
st.metric("Readability", f"{current_metrics.get('readability_score', 0):.1%}")
|
||||
with col2:
|
||||
st.metric("SEO Score", f"{current_metrics.get('seo_score', 0):.1%}")
|
||||
with col3:
|
||||
st.metric("Engagement", f"{current_metrics.get('engagement_potential', 0):.1%}")
|
||||
with col4:
|
||||
st.metric("Overall Quality", f"{current_metrics.get('content_quality', 0):.1%}")
|
||||
|
||||
# Display keyword density trend
|
||||
st.subheader("Keyword Density Trend")
|
||||
st.line_chart(df['keyword_density'])
|
||||
else:
|
||||
st.info("No optimization metrics available. Generate optimization first.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing selected content: {str(e)}")
|
||||
st.error("Error processing selected content. Please try again.")
|
||||
|
||||
# Remove everything after this point
|
||||
@@ -0,0 +1,517 @@
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
from typing import Dict, List, Any, Optional
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# Add parent directory to path to import existing tools
|
||||
parent_dir = str(Path(__file__).parent.parent.parent.parent.parent)
|
||||
if parent_dir not in sys.path:
|
||||
sys.path.append(parent_dir)
|
||||
|
||||
from lib.database.models import ContentItem, ContentType, Platform, SEOData
|
||||
from lib.ai_seo_tools.content_calendar.core.content_repurposer import SmartContentRepurposingEngine
|
||||
from lib.ai_seo_tools.content_calendar.core.content_generator import ContentGenerator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ContentRepurposingUI:
|
||||
"""
|
||||
Streamlit UI component for the Smart Content Repurposing Engine.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.repurposing_engine = SmartContentRepurposingEngine()
|
||||
self.content_generator = ContentGenerator()
|
||||
self.logger = logging.getLogger('content_calendar.repurposing_ui')
|
||||
|
||||
def render_repurposing_interface(self):
|
||||
"""Render the main repurposing interface."""
|
||||
st.header("🔄 Smart Content Repurposing Engine")
|
||||
st.markdown("Transform your content into multiple platform-optimized pieces with AI-powered repurposing.")
|
||||
|
||||
# Create tabs for different repurposing functions
|
||||
tab1, tab2, tab3, tab4 = st.tabs([
|
||||
"📝 Single Content Repurposing",
|
||||
"📚 Content Series Creation",
|
||||
"🔍 Content Analysis",
|
||||
"📊 Repurposing Dashboard"
|
||||
])
|
||||
|
||||
with tab1:
|
||||
self._render_single_content_repurposing()
|
||||
|
||||
with tab2:
|
||||
self._render_content_series_creation()
|
||||
|
||||
with tab3:
|
||||
self._render_content_analysis()
|
||||
|
||||
with tab4:
|
||||
self._render_repurposing_dashboard()
|
||||
|
||||
def _render_single_content_repurposing(self):
|
||||
"""Render the single content repurposing interface."""
|
||||
st.subheader("Repurpose Single Content")
|
||||
st.markdown("Transform one piece of content into multiple platform-optimized variations.")
|
||||
|
||||
# Content input section
|
||||
col1, col2 = st.columns([2, 1])
|
||||
|
||||
with col1:
|
||||
st.markdown("### 📄 Source Content")
|
||||
|
||||
# Content input options
|
||||
input_method = st.radio(
|
||||
"How would you like to provide content?",
|
||||
["Manual Input", "Upload File", "Select from Calendar"],
|
||||
horizontal=True
|
||||
)
|
||||
|
||||
source_content = None
|
||||
|
||||
if input_method == "Manual Input":
|
||||
source_content = self._render_manual_content_input()
|
||||
elif input_method == "Upload File":
|
||||
source_content = self._render_file_upload_input()
|
||||
else: # Select from Calendar
|
||||
source_content = self._render_calendar_selection()
|
||||
|
||||
with col2:
|
||||
st.markdown("### 🎯 Target Platforms")
|
||||
|
||||
# Platform selection
|
||||
available_platforms = [
|
||||
Platform.TWITTER,
|
||||
Platform.LINKEDIN,
|
||||
Platform.INSTAGRAM,
|
||||
Platform.FACEBOOK,
|
||||
Platform.WEBSITE
|
||||
]
|
||||
|
||||
selected_platforms = st.multiselect(
|
||||
"Select target platforms:",
|
||||
options=available_platforms,
|
||||
default=[Platform.TWITTER, Platform.LINKEDIN],
|
||||
format_func=lambda x: x.name.title()
|
||||
)
|
||||
|
||||
# Repurposing strategy
|
||||
strategy = st.selectbox(
|
||||
"Repurposing Strategy:",
|
||||
["adaptive", "atomic", "series"],
|
||||
help="Adaptive: AI chooses best approach, Atomic: Break into small pieces, Series: Create connected content"
|
||||
)
|
||||
|
||||
# Generate repurposed content
|
||||
if st.button("🚀 Generate Repurposed Content", type="primary"):
|
||||
if source_content and selected_platforms:
|
||||
with st.spinner("Repurposing content..."):
|
||||
try:
|
||||
repurposed_content = self.content_generator.repurpose_content_for_platforms(
|
||||
content_item=source_content,
|
||||
target_platforms=selected_platforms,
|
||||
strategy=strategy
|
||||
)
|
||||
|
||||
if repurposed_content:
|
||||
self._display_repurposed_content(repurposed_content)
|
||||
else:
|
||||
st.error("Failed to generate repurposed content. Please try again.")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error during repurposing: {str(e)}")
|
||||
else:
|
||||
st.warning("Please provide source content and select at least one target platform.")
|
||||
|
||||
def _render_content_series_creation(self):
|
||||
"""Render the content series creation interface."""
|
||||
st.subheader("Create Cross-Platform Content Series")
|
||||
st.markdown("Generate a strategic content series that progressively reveals information across platforms.")
|
||||
|
||||
# Source content input
|
||||
source_content = self._render_manual_content_input(key_suffix="_series")
|
||||
|
||||
if source_content:
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.markdown("### 🌐 Platform Strategy")
|
||||
|
||||
# Platform selection with strategy
|
||||
platforms = st.multiselect(
|
||||
"Select platforms for series:",
|
||||
options=[Platform.TWITTER, Platform.LINKEDIN, Platform.INSTAGRAM, Platform.FACEBOOK, Platform.WEBSITE],
|
||||
default=[Platform.TWITTER, Platform.LINKEDIN, Platform.WEBSITE],
|
||||
format_func=lambda x: x.name.title(),
|
||||
key="series_platforms"
|
||||
)
|
||||
|
||||
series_type = st.selectbox(
|
||||
"Series Strategy:",
|
||||
["progressive_disclosure", "platform_native"],
|
||||
help="Progressive: Gradually reveal info across platforms, Native: Optimize for each platform's strengths"
|
||||
)
|
||||
|
||||
with col2:
|
||||
st.markdown("### 📅 Timeline Preview")
|
||||
|
||||
if platforms:
|
||||
# Show timeline preview
|
||||
timeline_df = self._create_series_timeline_preview(source_content, platforms)
|
||||
st.dataframe(timeline_df, use_container_width=True)
|
||||
|
||||
# Generate series
|
||||
if st.button("📚 Create Content Series", type="primary", key="create_series"):
|
||||
if platforms:
|
||||
with st.spinner("Creating content series..."):
|
||||
try:
|
||||
series_content = self.content_generator.create_content_series_across_platforms(
|
||||
source_content=source_content,
|
||||
platforms=platforms,
|
||||
series_type=series_type
|
||||
)
|
||||
|
||||
if series_content:
|
||||
self._display_content_series(series_content)
|
||||
else:
|
||||
st.error("Failed to create content series. Please try again.")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error creating series: {str(e)}")
|
||||
else:
|
||||
st.warning("Please select at least one platform for the series.")
|
||||
|
||||
def _render_content_analysis(self):
|
||||
"""Render the content analysis interface."""
|
||||
st.subheader("Content Repurposing Analysis")
|
||||
st.markdown("Analyze your content's repurposing potential and get AI-powered recommendations.")
|
||||
|
||||
# Content input
|
||||
content_to_analyze = self._render_manual_content_input(key_suffix="_analysis")
|
||||
|
||||
if content_to_analyze:
|
||||
col1, col2 = st.columns([1, 1])
|
||||
|
||||
with col1:
|
||||
available_platforms = st.multiselect(
|
||||
"Available platforms for analysis:",
|
||||
options=[Platform.TWITTER, Platform.LINKEDIN, Platform.INSTAGRAM, Platform.FACEBOOK, Platform.WEBSITE],
|
||||
default=[Platform.TWITTER, Platform.LINKEDIN, Platform.INSTAGRAM, Platform.FACEBOOK, Platform.WEBSITE],
|
||||
format_func=lambda x: x.name.title(),
|
||||
key="analysis_platforms"
|
||||
)
|
||||
|
||||
with col2:
|
||||
if st.button("🔍 Analyze Content", type="primary"):
|
||||
if available_platforms:
|
||||
with st.spinner("Analyzing content..."):
|
||||
try:
|
||||
analysis = self.content_generator.analyze_content_for_repurposing(
|
||||
content_item=content_to_analyze,
|
||||
available_platforms=available_platforms
|
||||
)
|
||||
|
||||
if analysis:
|
||||
self._display_content_analysis(analysis)
|
||||
else:
|
||||
st.error("Failed to analyze content. Please try again.")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error during analysis: {str(e)}")
|
||||
else:
|
||||
st.warning("Please select at least one platform for analysis.")
|
||||
|
||||
def _render_repurposing_dashboard(self):
|
||||
"""Render the repurposing dashboard with metrics and insights."""
|
||||
st.subheader("Repurposing Dashboard")
|
||||
st.markdown("Track your content repurposing performance and insights.")
|
||||
|
||||
# Mock data for demonstration
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
st.metric("Content Pieces Created", "156", "+23")
|
||||
|
||||
with col2:
|
||||
st.metric("Time Saved", "312 hours", "+45 hours")
|
||||
|
||||
with col3:
|
||||
st.metric("Platform Coverage", "85%", "+12%")
|
||||
|
||||
with col4:
|
||||
st.metric("Engagement Boost", "34%", "+8%")
|
||||
|
||||
# Recent repurposing activity
|
||||
st.markdown("### 📈 Recent Repurposing Activity")
|
||||
|
||||
# Mock data for recent activity
|
||||
recent_activity = pd.DataFrame({
|
||||
'Date': ['2024-01-15', '2024-01-14', '2024-01-13', '2024-01-12'],
|
||||
'Source Content': ['AI Writing Tips', 'SEO Best Practices', 'Content Strategy Guide', 'Social Media Trends'],
|
||||
'Platforms': ['Twitter, LinkedIn', 'LinkedIn, Instagram', 'All Platforms', 'Twitter, Facebook'],
|
||||
'Pieces Created': [3, 2, 5, 2],
|
||||
'Status': ['Published', 'Scheduled', 'Draft', 'Published']
|
||||
})
|
||||
|
||||
st.dataframe(recent_activity, use_container_width=True)
|
||||
|
||||
# Performance insights
|
||||
st.markdown("### 💡 Performance Insights")
|
||||
|
||||
insights_col1, insights_col2 = st.columns(2)
|
||||
|
||||
with insights_col1:
|
||||
st.info("🎯 **Best Performing Platform**: LinkedIn posts show 45% higher engagement when repurposed from blog content.")
|
||||
|
||||
with insights_col2:
|
||||
st.success("📊 **Optimization Tip**: Twitter threads perform 60% better when created from long-form content with statistics.")
|
||||
|
||||
def _render_manual_content_input(self, key_suffix: str = "") -> Optional[ContentItem]:
|
||||
"""Render manual content input form."""
|
||||
with st.form(f"content_input_form{key_suffix}"):
|
||||
title = st.text_input("Content Title:", key=f"title{key_suffix}")
|
||||
content_type = st.selectbox(
|
||||
"Content Type:",
|
||||
options=[ContentType.BLOG_POST, ContentType.SOCIAL_MEDIA, ContentType.VIDEO, ContentType.NEWSLETTER],
|
||||
format_func=lambda x: x.name.replace('_', ' ').title(),
|
||||
key=f"content_type{key_suffix}"
|
||||
)
|
||||
|
||||
description = st.text_area(
|
||||
"Content Description/Body:",
|
||||
height=200,
|
||||
help="Paste your content here. This will be analyzed and repurposed.",
|
||||
key=f"description{key_suffix}"
|
||||
)
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
author = st.text_input("Author:", value="Content Creator", key=f"author{key_suffix}")
|
||||
with col2:
|
||||
tags = st.text_input("Tags (comma-separated):", key=f"tags{key_suffix}")
|
||||
|
||||
submitted = st.form_submit_button("📝 Use This Content")
|
||||
|
||||
if submitted and title and description:
|
||||
# Create ContentItem
|
||||
content_item = ContentItem(
|
||||
title=title,
|
||||
description=description,
|
||||
content_type=content_type,
|
||||
platforms=[],
|
||||
publish_date=datetime.now(),
|
||||
status="draft",
|
||||
author=author,
|
||||
tags=tags.split(',') if tags else [],
|
||||
notes="",
|
||||
seo_data=SEOData(title=title, meta_description="", keywords=[], structured_data={})
|
||||
)
|
||||
return content_item
|
||||
|
||||
return None
|
||||
|
||||
def _render_file_upload_input(self) -> Optional[ContentItem]:
|
||||
"""Render file upload input."""
|
||||
uploaded_file = st.file_uploader(
|
||||
"Upload content file:",
|
||||
type=['txt', 'md', 'docx'],
|
||||
help="Upload a text file, markdown file, or Word document"
|
||||
)
|
||||
|
||||
if uploaded_file:
|
||||
try:
|
||||
# Read file content
|
||||
if uploaded_file.type == "text/plain":
|
||||
content = str(uploaded_file.read(), "utf-8")
|
||||
else:
|
||||
content = str(uploaded_file.read(), "utf-8") # Simplified for demo
|
||||
|
||||
# Extract title from filename
|
||||
title = uploaded_file.name.split('.')[0].replace('_', ' ').title()
|
||||
|
||||
# Create ContentItem
|
||||
content_item = ContentItem(
|
||||
title=title,
|
||||
description=content,
|
||||
content_type=ContentType.BLOG_POST,
|
||||
platforms=[],
|
||||
publish_date=datetime.now(),
|
||||
status="draft",
|
||||
author="Uploaded Content",
|
||||
tags=[],
|
||||
notes=f"Uploaded from file: {uploaded_file.name}",
|
||||
seo_data=SEOData(title=title, meta_description="", keywords=[], structured_data={})
|
||||
)
|
||||
|
||||
st.success(f"✅ File uploaded: {uploaded_file.name}")
|
||||
return content_item
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error reading file: {str(e)}")
|
||||
|
||||
return None
|
||||
|
||||
def _render_calendar_selection(self) -> Optional[ContentItem]:
|
||||
"""Render calendar content selection."""
|
||||
st.info("📅 Calendar integration coming soon! For now, please use manual input or file upload.")
|
||||
return None
|
||||
|
||||
def _display_repurposed_content(self, repurposed_content: List[ContentItem]):
|
||||
"""Display the repurposed content results."""
|
||||
st.success(f"✅ Successfully created {len(repurposed_content)} repurposed content pieces!")
|
||||
|
||||
for i, content in enumerate(repurposed_content):
|
||||
with st.expander(f"📱 {content.platforms[0].name.title()} - {content.title}"):
|
||||
st.markdown(f"**Platform:** {content.platforms[0].name.title()}")
|
||||
st.markdown(f"**Content Type:** {content.content_type.name.replace('_', ' ').title()}")
|
||||
st.markdown(f"**Scheduled for:** {content.publish_date.strftime('%Y-%m-%d')}")
|
||||
|
||||
st.markdown("**Content:**")
|
||||
st.write(content.description)
|
||||
|
||||
if content.tags:
|
||||
st.markdown(f"**Tags:** {', '.join(content.tags)}")
|
||||
|
||||
# Action buttons
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
if st.button(f"📝 Edit", key=f"edit_{i}"):
|
||||
st.info("Edit functionality coming soon!")
|
||||
with col2:
|
||||
if st.button(f"📅 Schedule", key=f"schedule_{i}"):
|
||||
st.info("Scheduling functionality coming soon!")
|
||||
with col3:
|
||||
if st.button(f"📋 Copy", key=f"copy_{i}"):
|
||||
st.code(content.description)
|
||||
|
||||
def _display_content_series(self, series_content: Dict[str, List[ContentItem]]):
|
||||
"""Display the content series results."""
|
||||
total_pieces = sum(len(pieces) for pieces in series_content.values())
|
||||
st.success(f"✅ Successfully created content series with {total_pieces} pieces across {len(series_content)} platforms!")
|
||||
|
||||
for platform, content_pieces in series_content.items():
|
||||
st.markdown(f"### 📱 {platform.title()} Series ({len(content_pieces)} pieces)")
|
||||
|
||||
for i, content in enumerate(content_pieces):
|
||||
with st.expander(f"Part {i+1}: {content.title}"):
|
||||
st.markdown(f"**Scheduled for:** {content.publish_date.strftime('%Y-%m-%d')}")
|
||||
st.markdown("**Content:**")
|
||||
st.write(content.description)
|
||||
|
||||
if content.tags:
|
||||
st.markdown(f"**Tags:** {', '.join(content.tags)}")
|
||||
|
||||
def _display_content_analysis(self, analysis: Dict[str, Any]):
|
||||
"""Display content analysis results."""
|
||||
st.markdown("### 📊 Content Analysis Results")
|
||||
|
||||
# Content metrics
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
content_analysis = analysis.get('content_analysis', {})
|
||||
|
||||
with col1:
|
||||
st.metric("Word Count", content_analysis.get('word_count', 0))
|
||||
|
||||
with col2:
|
||||
richness = content_analysis.get('content_richness', 'Unknown')
|
||||
st.metric("Content Richness", richness)
|
||||
|
||||
with col3:
|
||||
potential = content_analysis.get('repurposing_potential', 'Unknown')
|
||||
st.metric("Repurposing Potential", potential)
|
||||
|
||||
# Recommendations
|
||||
st.markdown("### 💡 Recommendations")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.markdown("**Recommended Platforms:**")
|
||||
platforms = analysis.get('platform_suggestions', [])
|
||||
for platform in platforms:
|
||||
st.write(f"• {platform.name.title()}")
|
||||
|
||||
with col2:
|
||||
st.markdown("**Suggested Strategies:**")
|
||||
strategies = analysis.get('strategy_suggestions', [])
|
||||
for strategy in strategies:
|
||||
st.write(f"• {strategy.replace('_', ' ').title()}")
|
||||
|
||||
# Content atoms
|
||||
st.markdown("### 🔬 Content Atoms Analysis")
|
||||
|
||||
atoms = content_analysis.get('content_atoms', {})
|
||||
|
||||
for atom_type, atom_list in atoms.items():
|
||||
if atom_list:
|
||||
with st.expander(f"{atom_type.title()} ({len(atom_list)} found)"):
|
||||
for atom in atom_list:
|
||||
st.write(f"• {atom}")
|
||||
|
||||
# Estimated output
|
||||
estimated = analysis.get('estimated_output', {})
|
||||
if estimated:
|
||||
st.markdown("### 📈 Estimated Output")
|
||||
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
st.metric("Total Pieces", estimated.get('total_pieces', 0))
|
||||
|
||||
with col2:
|
||||
st.metric("Time Savings", estimated.get('time_savings', '0 hours'))
|
||||
|
||||
with col3:
|
||||
st.metric("Content Multiplication", estimated.get('content_multiplication', '1x'))
|
||||
|
||||
def _create_series_timeline_preview(self, content: ContentItem, platforms: List[Platform]) -> pd.DataFrame:
|
||||
"""Create a preview timeline for content series."""
|
||||
timeline_data = []
|
||||
base_date = datetime.now()
|
||||
|
||||
for i, platform in enumerate(platforms):
|
||||
release_date = base_date + timedelta(days=i)
|
||||
timeline_data.append({
|
||||
'Platform': platform.name.title(),
|
||||
'Release Date': release_date.strftime('%Y-%m-%d'),
|
||||
'Content Type': self._get_platform_content_type(platform),
|
||||
'Strategy': self._get_platform_strategy(platform)
|
||||
})
|
||||
|
||||
return pd.DataFrame(timeline_data)
|
||||
|
||||
def _get_platform_content_type(self, platform: Platform) -> str:
|
||||
"""Get content type for platform."""
|
||||
types = {
|
||||
Platform.TWITTER: "Thread/Tweet",
|
||||
Platform.LINKEDIN: "Professional Post",
|
||||
Platform.INSTAGRAM: "Visual Post",
|
||||
Platform.FACEBOOK: "Engaging Post",
|
||||
Platform.WEBSITE: "Blog Article"
|
||||
}
|
||||
return types.get(platform, "Standard Post")
|
||||
|
||||
def _get_platform_strategy(self, platform: Platform) -> str:
|
||||
"""Get strategy for platform."""
|
||||
strategies = {
|
||||
Platform.TWITTER: "Hook & Engage",
|
||||
Platform.LINKEDIN: "Authority Building",
|
||||
Platform.INSTAGRAM: "Visual Storytelling",
|
||||
Platform.FACEBOOK: "Community Discussion",
|
||||
Platform.WEBSITE: "Complete Information"
|
||||
}
|
||||
return strategies.get(platform, "Standard Approach")
|
||||
|
||||
# Main function to render the UI
|
||||
def render_content_repurposing_ui():
|
||||
"""Main function to render the content repurposing UI."""
|
||||
ui = ContentRepurposingUI()
|
||||
ui.render_repurposing_interface()
|
||||
|
||||
# For testing
|
||||
if __name__ == "__main__":
|
||||
render_content_repurposing_ui()
|
||||
@@ -2,10 +2,10 @@ import streamlit as st
|
||||
from typing import Dict, Any, List
|
||||
from datetime import datetime, timedelta
|
||||
import pandas as pd
|
||||
from ...core.content_generator import ContentGenerator
|
||||
from ...core.ai_generator import AIGenerator
|
||||
from ...integrations.seo_optimizer import SEOOptimizer
|
||||
from ...models.calendar import ContentItem, ContentType, Platform, SEOData
|
||||
from lib.ai_seo_tools.content_calendar.core.content_generator import ContentGenerator
|
||||
from lib.ai_seo_tools.content_calendar.core.ai_generator import AIGenerator
|
||||
from lib.ai_seo_tools.content_calendar.integrations.seo_optimizer import SEOOptimizer
|
||||
from lib.database.models import ContentItem, ContentType, Platform, SEOData
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('content_calendar.series')
|
||||
@@ -21,7 +21,7 @@ class SeriesManager:
|
||||
st.session_state.series_performance = {}
|
||||
|
||||
def create_series(self, series_id: str, topic: str, num_pieces: int, content_type: ContentType,
|
||||
platforms: List[Platform], schedule_strategy: str = 'linear') -> Dict[str, Any]:
|
||||
platforms: List[Platform], schedule_strategy: str = 'linear', series_type: str = '', series_flow: str = '', metadata: Dict[str, Any] = {}) -> Dict[str, Any]:
|
||||
"""Create a new content series with tracking and scheduling."""
|
||||
try:
|
||||
series = {
|
||||
@@ -31,12 +31,15 @@ class SeriesManager:
|
||||
'content_type': content_type,
|
||||
'platforms': platforms,
|
||||
'schedule_strategy': schedule_strategy,
|
||||
'series_type': series_type,
|
||||
'series_flow': series_flow,
|
||||
'pieces': [],
|
||||
'performance': {},
|
||||
'created_at': datetime.now(),
|
||||
'status': 'draft',
|
||||
'relationships': {},
|
||||
'platform_distribution': {p.name: [] for p in platforms}
|
||||
'platform_distribution': {p.name: [] for p in platforms},
|
||||
'metadata': metadata
|
||||
}
|
||||
st.session_state.content_series[series_id] = series
|
||||
return series
|
||||
@@ -50,23 +53,38 @@ class SeriesManager:
|
||||
if series_id in st.session_state.content_series:
|
||||
series = st.session_state.content_series[series_id]
|
||||
piece_id = f"piece_{len(series['pieces'])}"
|
||||
piece['id'] = piece_id
|
||||
|
||||
# Create a structured piece object
|
||||
structured_piece = {
|
||||
'id': piece_id,
|
||||
'title': piece.get('title', f"Part {len(series['pieces']) + 1}"),
|
||||
'content': piece.get('content', ''),
|
||||
'platform': piece.get('platform', series['platforms'][0]),
|
||||
'scheduled_date': None,
|
||||
'status': 'draft',
|
||||
'relationships': {
|
||||
'previous': None,
|
||||
'next': None
|
||||
},
|
||||
'performance': {
|
||||
'engagement': 0,
|
||||
'reach': 0,
|
||||
'conversion_rate': 0
|
||||
}
|
||||
}
|
||||
|
||||
# Track relationships
|
||||
if series['pieces']:
|
||||
previous_piece = series['pieces'][-1]
|
||||
piece['relationships'] = {
|
||||
'previous': previous_piece['id'],
|
||||
'next': None
|
||||
}
|
||||
previous_piece['relationships']['next'] = piece_id
|
||||
structured_piece['relationships']['previous'] = previous_piece['id']
|
||||
structured_piece['relationships']['next'] = piece_id
|
||||
|
||||
# Add to platform distribution
|
||||
for platform in piece.get('platforms', []):
|
||||
if platform.name in series['platform_distribution']:
|
||||
series['platform_distribution'][platform.name].append(piece_id)
|
||||
platform_name = structured_piece['platform'].name
|
||||
if platform_name in series['platform_distribution']:
|
||||
series['platform_distribution'][platform_name].append(piece_id)
|
||||
|
||||
series['pieces'].append(piece)
|
||||
series['pieces'].append(structured_piece)
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
@@ -176,11 +194,68 @@ class SeriesManager:
|
||||
logger.error(f"Error scheduling series: {str(e)}")
|
||||
return False
|
||||
|
||||
def render_content_series_generator(ai_generator: AIGenerator, content_generator: ContentGenerator,
|
||||
seo_optimizer: SEOOptimizer):
|
||||
"""Render the content series generator interface with enhanced features."""
|
||||
def render_content_series_generator(
|
||||
ai_generator: AIGenerator,
|
||||
content_generator: ContentGenerator,
|
||||
seo_optimizer: SEOOptimizer
|
||||
):
|
||||
"""Render the content series generator interface."""
|
||||
st.header("Content Series Generator")
|
||||
|
||||
# Check if calendar manager is available
|
||||
if 'calendar_manager' not in st.session_state:
|
||||
st.error("Calendar manager not initialized. Please refresh the page.")
|
||||
return
|
||||
|
||||
# Get available content
|
||||
try:
|
||||
available_content = st.session_state.calendar_manager.get_calendar().get_all_content()
|
||||
content_options = [item.title for item in available_content]
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting content options: {str(e)}")
|
||||
st.error("Error loading content. Please try again.")
|
||||
return
|
||||
|
||||
if not content_options:
|
||||
st.info("""
|
||||
## Welcome to Content Series Generator! 📚
|
||||
|
||||
Create and manage content series across multiple platforms. Here's what you can do:
|
||||
|
||||
### Features:
|
||||
- 📝 **Series Creation**: Generate connected content pieces
|
||||
- 🔄 **Cross-Platform Distribution**: Optimize for different platforms
|
||||
- 📊 **Series Analytics**: Track performance across the series
|
||||
- 📅 **Smart Scheduling**: Plan content distribution
|
||||
|
||||
### Getting Started:
|
||||
1. First, add some content to your calendar
|
||||
2. Select a topic for your content series
|
||||
3. Configure series parameters and platforms
|
||||
4. Generate and schedule your series
|
||||
|
||||
Ready to get started? Add some content to your calendar first!
|
||||
""")
|
||||
return
|
||||
|
||||
# Series Configuration
|
||||
st.subheader("Create New Content Series")
|
||||
|
||||
# Show onboarding info if no series exist
|
||||
if not st.session_state.get('content_series', {}):
|
||||
st.info("""
|
||||
### Content Series Guide
|
||||
|
||||
Create engaging content series with these features:
|
||||
|
||||
- **Series Planning**: Define your series structure and goals
|
||||
- **Content Generation**: Create connected content pieces
|
||||
- **Platform Optimization**: Adapt content for each platform
|
||||
- **Performance Tracking**: Monitor series success
|
||||
|
||||
Fill out the form below to create your first series!
|
||||
""")
|
||||
|
||||
# Initialize series manager
|
||||
series_manager = SeriesManager()
|
||||
|
||||
@@ -231,144 +306,125 @@ def render_content_series_generator(ai_generator: AIGenerator, content_generator
|
||||
try:
|
||||
# Create series
|
||||
series_id = f"series_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
|
||||
# Prepare metadata with default values
|
||||
metadata = {
|
||||
'tone': series_tone,
|
||||
'length': 'medium', # Default length
|
||||
'engagement_goal': series_goals[0] if series_goals else 'Awareness',
|
||||
'creativity_level': 'balanced' # Default creativity level
|
||||
}
|
||||
|
||||
series = series_manager.create_series(
|
||||
series_id=series_id,
|
||||
topic=series_topic,
|
||||
num_pieces=num_pieces,
|
||||
content_type=ContentType[content_type],
|
||||
platforms=[Platform[p] for p in platforms],
|
||||
schedule_strategy=schedule_strategy
|
||||
schedule_strategy=schedule_strategy,
|
||||
series_type=series_goals[0] if series_goals else 'Awareness',
|
||||
series_flow='sequential', # Default flow
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
if series:
|
||||
# Generate series content
|
||||
for i in range(num_pieces):
|
||||
content_item = ContentItem(
|
||||
title=f"{series_topic} - Part {i+1}",
|
||||
description="",
|
||||
content_type=ContentType[content_type],
|
||||
platforms=[Platform[p] for p in platforms],
|
||||
publish_date=datetime.now() + timedelta(days=i*7),
|
||||
seo_data=SEOData(
|
||||
title=f"{series_topic} - Part {i+1}",
|
||||
meta_description="",
|
||||
keywords=[],
|
||||
structured_data={}
|
||||
),
|
||||
status='Draft'
|
||||
)
|
||||
|
||||
# Generate content using AI
|
||||
base_content = ai_generator.generate_series_content(
|
||||
content_item=content_item,
|
||||
series_info={
|
||||
'topic': series_topic,
|
||||
'part_number': i+1,
|
||||
'total_parts': num_pieces,
|
||||
'content_type': content_type,
|
||||
'platforms': platforms,
|
||||
'audience': target_audience,
|
||||
'goals': series_goals,
|
||||
'tone': series_tone
|
||||
}
|
||||
)
|
||||
|
||||
if base_content:
|
||||
# Enhance with Content Generator
|
||||
enhanced_content = content_generator.enhance_series_content(
|
||||
content=base_content,
|
||||
series_info={
|
||||
'topic': series_topic,
|
||||
'part_number': i+1,
|
||||
'total_parts': num_pieces
|
||||
}
|
||||
series_content = content_generator.generate_content(
|
||||
content_type=ContentType[content_type],
|
||||
topic=series_topic,
|
||||
platforms=[Platform[p] for p in platforms],
|
||||
num_pieces=num_pieces,
|
||||
requirements={
|
||||
'tone': series_tone,
|
||||
'length': metadata['length'],
|
||||
'engagement_goal': metadata['engagement_goal'],
|
||||
'creativity_level': metadata['creativity_level'],
|
||||
'series_type': metadata['engagement_goal'],
|
||||
'series_flow': 'sequential',
|
||||
'target_audience': target_audience
|
||||
}
|
||||
)
|
||||
|
||||
if series_content:
|
||||
# Add content pieces to series
|
||||
for piece in series_content:
|
||||
series_manager.add_piece(
|
||||
series_id=series['id'],
|
||||
piece=piece
|
||||
)
|
||||
|
||||
if enhanced_content:
|
||||
base_content.update(enhanced_content)
|
||||
|
||||
# Add to series
|
||||
series_manager.add_piece(series_id, {
|
||||
'part_number': i+1,
|
||||
'content': base_content,
|
||||
'seo_data': seo_optimizer.optimize_content(
|
||||
content=base_content,
|
||||
content_type=content_type,
|
||||
language='English',
|
||||
search_intent='Informational Intent'
|
||||
|
||||
# Schedule series
|
||||
if schedule_strategy == 'linear':
|
||||
start_date = st.date_input("Start Date", datetime.now())
|
||||
interval = st.number_input("Days between pieces", min_value=1, value=7)
|
||||
series_manager.schedule_series(
|
||||
series_id=series['id'],
|
||||
start_date=start_date,
|
||||
interval_days=interval
|
||||
)
|
||||
elif schedule_strategy == 'burst':
|
||||
start_date = st.date_input("Start Date", datetime.now())
|
||||
burst_size = st.number_input("Burst Size", min_value=1, value=1)
|
||||
series_manager.schedule_series(
|
||||
series_id=series['id'],
|
||||
start_date=start_date,
|
||||
interval_days=1,
|
||||
burst_size=burst_size
|
||||
)
|
||||
else: # custom
|
||||
for i, piece in enumerate(series_manager.series_data[series['id']]['pieces']):
|
||||
piece['scheduled_date'] = st.date_input(
|
||||
f"Publish Date for Part {i+1}",
|
||||
datetime.now() + timedelta(days=i*7)
|
||||
)
|
||||
})
|
||||
|
||||
st.success(f"Generated {num_pieces} content pieces for series!")
|
||||
|
||||
# Display series preview
|
||||
with st.expander("Series Preview", expanded=True):
|
||||
for piece in series_manager.series_data[series_id]['pieces']:
|
||||
st.markdown(f"### Part {piece['part_number']}")
|
||||
st.json(piece['content'])
|
||||
|
||||
# Platform-specific previews
|
||||
st.markdown("#### Platform Previews")
|
||||
if st.button("Save Schedule"):
|
||||
st.success("Series schedule saved!")
|
||||
|
||||
st.success(f"Generated {num_pieces} content pieces for series!")
|
||||
|
||||
# Display series preview
|
||||
with st.expander("Series Preview", expanded=True):
|
||||
for piece in series_manager.series_data[series_id]['pieces']:
|
||||
st.markdown(f"### Part {piece['part_number']}")
|
||||
st.json(piece['content'])
|
||||
|
||||
# Platform-specific previews
|
||||
st.markdown("#### Platform Previews")
|
||||
for platform in platforms:
|
||||
with st.expander(f"{platform} Preview"):
|
||||
st.write(piece['content'].get('platform_previews', {}).get(platform, 'No preview available'))
|
||||
|
||||
# Series performance tracking
|
||||
st.subheader("Series Performance")
|
||||
performance_data = series_manager.get_series_performance(series_id)
|
||||
if performance_data:
|
||||
st.write("### Overall Performance")
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.metric("Total Engagement", f"{performance_data['overall']['total_engagement']:.1f}%")
|
||||
with col2:
|
||||
st.metric("Total Reach", f"{performance_data['overall']['total_reach']:,}")
|
||||
with col3:
|
||||
st.metric("Conversion Rate", f"{performance_data['overall']['conversion_rate']:.1f}%")
|
||||
|
||||
# Platform-specific performance
|
||||
st.write("### Platform Performance")
|
||||
for platform in platforms:
|
||||
with st.expander(f"{platform} Preview"):
|
||||
st.write(piece['content'].get('platform_previews', {}).get(platform, 'No preview available'))
|
||||
|
||||
# Series scheduling
|
||||
st.subheader("Series Scheduling")
|
||||
if schedule_strategy == 'linear':
|
||||
start_date = st.date_input("Start Date", datetime.now())
|
||||
interval = st.number_input("Days between pieces", min_value=1, value=7)
|
||||
|
||||
if st.button("Schedule Series"):
|
||||
series_manager.schedule_series(series_id, start_date, interval)
|
||||
st.success("Series scheduled successfully!")
|
||||
|
||||
elif schedule_strategy == 'burst':
|
||||
start_date = st.date_input("Start Date", datetime.now())
|
||||
if st.button("Schedule Series"):
|
||||
series_manager.schedule_series(series_id, start_date, interval=1)
|
||||
st.success("Series scheduled successfully!")
|
||||
|
||||
else: # custom
|
||||
for i, piece in enumerate(series_manager.series_data[series_id]['pieces']):
|
||||
piece['scheduled_date'] = st.date_input(
|
||||
f"Publish Date for Part {i+1}",
|
||||
datetime.now() + timedelta(days=i*7)
|
||||
)
|
||||
|
||||
if st.button("Save Schedule"):
|
||||
st.success("Series schedule saved!")
|
||||
|
||||
# Series performance tracking
|
||||
st.subheader("Series Performance")
|
||||
performance_data = series_manager.get_series_performance(series_id)
|
||||
if performance_data:
|
||||
st.write("### Overall Performance")
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.metric("Total Engagement", f"{performance_data['overall']['total_engagement']:.1f}%")
|
||||
with col2:
|
||||
st.metric("Total Reach", f"{performance_data['overall']['total_reach']:,}")
|
||||
with col3:
|
||||
st.metric("Conversion Rate", f"{performance_data['overall']['conversion_rate']:.1f}%")
|
||||
|
||||
# Platform-specific performance
|
||||
st.write("### Platform Performance")
|
||||
for platform in platforms:
|
||||
with st.expander(f"{platform} Performance"):
|
||||
platform_data = performance_data['platforms'].get(platform, {})
|
||||
st.write(f"Engagement: {platform_data.get('engagement', 0):.1f}%")
|
||||
st.write(f"Reach: {platform_data.get('reach', 0):,}")
|
||||
st.write(f"Conversions: {platform_data.get('conversion_rate', 0):.1f}%")
|
||||
|
||||
# Performance trends
|
||||
st.write("### Performance Trends")
|
||||
trend_data = performance_data['trends']
|
||||
st.line_chart(pd.DataFrame({
|
||||
'Engagement': trend_data['engagement'],
|
||||
'Reach': trend_data['reach'],
|
||||
'Conversions': trend_data['conversions']
|
||||
}))
|
||||
with st.expander(f"{platform} Performance"):
|
||||
platform_data = performance_data['platforms'].get(platform, {})
|
||||
st.write(f"Engagement: {platform_data.get('engagement', 0):.1f}%")
|
||||
st.write(f"Reach: {platform_data.get('reach', 0):,}")
|
||||
st.write(f"Conversions: {platform_data.get('conversion_rate', 0):.1f}%")
|
||||
|
||||
# Performance trends
|
||||
st.write("### Performance Trends")
|
||||
trend_data = performance_data['trends']
|
||||
st.line_chart(pd.DataFrame({
|
||||
'Engagement': trend_data['engagement'],
|
||||
'Reach': trend_data['reach'],
|
||||
'Conversions': trend_data['conversions']
|
||||
}))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating series: {str(e)}", exc_info=True)
|
||||
@@ -389,4 +445,13 @@ def render_content_series_generator(ai_generator: AIGenerator, content_generator
|
||||
|
||||
if st.button(f"Delete Series", key=f"delete_{series_id}"):
|
||||
del st.session_state.content_series[series_id]
|
||||
st.experimental_rerun()
|
||||
st.rerun()
|
||||
|
||||
def on_series_complete():
|
||||
"""Handle series completion."""
|
||||
try:
|
||||
st.session_state.series_complete = True
|
||||
st.rerun()
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling series completion: {str(e)}")
|
||||
st.error("An error occurred while completing the series. Please try again.")
|
||||
@@ -1,6 +1,6 @@
|
||||
import streamlit as st
|
||||
from typing import Dict, Any
|
||||
from lib.ai_seo_tools.content_calendar.models.calendar import ContentItem
|
||||
from lib.database.models import ContentItem
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1,64 +1,71 @@
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import sys
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
from .calendar_view import render_calendar_view
|
||||
from .filters import render_filters
|
||||
from .add_content_modal import render_add_content_modal
|
||||
from .ai_suggestions_modal import render_ai_suggestions_modal
|
||||
from .components.performance_insights import render_performance_insights
|
||||
from .components.content_series import render_content_series_generator
|
||||
from .components.ab_testing import render_ab_testing
|
||||
from .components.content_optimization import render_content_optimization
|
||||
from .components.ab_testing import render_ab_testing
|
||||
from .components.content_series import render_content_series_generator
|
||||
from .components.performance_insights import render_performance_insights
|
||||
import json
|
||||
from lib.content_scheduler.ui.dashboard import run_dashboard as run_scheduler_dashboard
|
||||
|
||||
# Add parent directory to path to import existing tools
|
||||
parent_dir = str(Path(__file__).parent.parent.parent.parent)
|
||||
if parent_dir not in sys.path:
|
||||
sys.path.append(parent_dir)
|
||||
|
||||
from lib.database.models import ContentItem, ContentType, Platform, get_engine, get_session, init_db
|
||||
from ..core.calendar_manager import CalendarManager
|
||||
from ..core.content_brief import ContentBriefGenerator
|
||||
from ..core.content_generator import ContentGenerator
|
||||
from ..core.ai_generator import AIGenerator
|
||||
from ..integrations.platform_adapters import UnifiedPlatformAdapter
|
||||
from ..core.content_brief import ContentBriefGenerator
|
||||
from ..integrations.seo_optimizer import SEOOptimizer
|
||||
from lib.ai_seo_tools.content_calendar.models.calendar import ContentItem, Platform, ContentType, SEOData, Calendar
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
from typing import Dict, Any, List, Tuple
|
||||
import json
|
||||
from lib.integrations.platform_adapters import PlatformAdapter, UnifiedPlatformAdapter
|
||||
|
||||
# Initialize logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Initialize DB/session (do this once at app startup)
|
||||
engine = get_engine()
|
||||
init_db(engine)
|
||||
session = get_session(engine)
|
||||
|
||||
# Import content repurposing UI with error handling
|
||||
def render_smart_repurposing_tab():
|
||||
"""Render the Smart Content Repurposing tab with error handling."""
|
||||
try:
|
||||
from lib.ai_seo_tools.content_calendar.ui.components.content_repurposing_ui import render_content_repurposing_ui
|
||||
render_content_repurposing_ui()
|
||||
except ImportError as e:
|
||||
st.error(f"Smart Content Repurposing feature is not available: {str(e)}")
|
||||
st.info("Please ensure all dependencies are installed correctly.")
|
||||
except Exception as e:
|
||||
st.error(f"Error loading Smart Content Repurposing: {str(e)}")
|
||||
st.info("Please check the logs for more details.")
|
||||
|
||||
class ContentCalendarDashboard:
|
||||
"""Interactive dashboard for content calendar management."""
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('content_calendar.dashboard')
|
||||
self.logger.info("Initializing ContentCalendarDashboard")
|
||||
|
||||
# Initialize calendar manager and store in session state
|
||||
if 'calendar_manager' not in st.session_state:
|
||||
st.session_state.calendar_manager = CalendarManager()
|
||||
st.session_state.calendar_manager.load_calendar_from_json()
|
||||
|
||||
self.calendar_manager = st.session_state.calendar_manager
|
||||
self.content_brief_generator = ContentBriefGenerator()
|
||||
self.content_generator = ContentGenerator()
|
||||
self.ai_generator = AIGenerator()
|
||||
self.platform_adapter = UnifiedPlatformAdapter()
|
||||
self.seo_optimizer = SEOOptimizer()
|
||||
|
||||
# Initialize A/B testing state
|
||||
# Initialize session state variables
|
||||
if 'ab_test_results' not in st.session_state:
|
||||
st.session_state.ab_test_results = {}
|
||||
|
||||
# Initialize content optimization state
|
||||
if 'optimization_history' not in st.session_state:
|
||||
st.session_state.optimization_history = {}
|
||||
|
||||
# Ensure a calendar exists
|
||||
if not self.calendar_manager.get_calendar():
|
||||
self.calendar_manager._calendar = Calendar(
|
||||
start_date=datetime.now(),
|
||||
duration='monthly',
|
||||
platforms=[Platform.WEBSITE, Platform.INSTAGRAM, Platform.TWITTER, Platform.LINKEDIN, Platform.FACEBOOK],
|
||||
schedule={}
|
||||
)
|
||||
|
||||
# Initialize session state
|
||||
if 'calendar_data' not in st.session_state:
|
||||
st.session_state.calendar_data = None
|
||||
if 'selected_content' not in st.session_state:
|
||||
@@ -67,9 +74,8 @@ class ContentCalendarDashboard:
|
||||
st.session_state.view_mode = 'day'
|
||||
if 'selected_date' not in st.session_state:
|
||||
st.session_state.selected_date = datetime.now()
|
||||
|
||||
self.logger.info("ContentCalendarDashboard initialized successfully")
|
||||
|
||||
|
||||
def render(self):
|
||||
self.logger.info("Starting dashboard render (tabbed UI)")
|
||||
try:
|
||||
@@ -78,8 +84,15 @@ class ContentCalendarDashboard:
|
||||
st.markdown("""
|
||||
Plan, schedule, and manage your content strategy with AI-powered insights. Use the calendar to organize your content and leverage AI tools for optimization.
|
||||
""")
|
||||
tabs = st.tabs(["Content Planning", "Content Optimization", "A/B Testing", "Content Series", "Analytics"])
|
||||
|
||||
tabs = st.tabs([
|
||||
"Content Planning",
|
||||
"Content Optimization",
|
||||
"🔄 Smart Repurposing",
|
||||
"A/B Testing",
|
||||
"Content Series",
|
||||
"Analytics",
|
||||
"Content Scheduling"
|
||||
])
|
||||
with tabs[0]:
|
||||
icon_map = {
|
||||
'Blog': '📝', 'Website': '🌐', 'Instagram': '📸', 'Twitter': '🐦', 'LinkedIn': '💼', 'Facebook': '📘',
|
||||
@@ -90,17 +103,26 @@ class ContentCalendarDashboard:
|
||||
}
|
||||
calendar_data = self._get_calendar_data()
|
||||
def on_edit(row):
|
||||
st.session_state["editing_item_key"] = self._get_item_key(row)
|
||||
st.experimental_rerun()
|
||||
try:
|
||||
st.session_state.editing_content = row
|
||||
st.rerun()
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling edit action: {str(e)}")
|
||||
st.error("An error occurred while editing content. Please try again.")
|
||||
def on_delete(row):
|
||||
self._delete_content(row)
|
||||
st.experimental_rerun()
|
||||
try:
|
||||
self._delete_content(row)
|
||||
st.success(f"Successfully deleted content: {row['title']}")
|
||||
st.rerun()
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling delete action: {str(e)}")
|
||||
st.error("An error occurred while deleting content. Please try again.")
|
||||
def on_generate(row):
|
||||
st.session_state['show_ai_modal'] = True
|
||||
st.session_state['ai_modal_topic'] = row['title']
|
||||
st.session_state['ai_modal_type'] = str(row['type'])
|
||||
st.session_state['ai_modal_platform'] = str(row['platform'])
|
||||
st.experimental_rerun()
|
||||
st.rerun()
|
||||
render_calendar_view(
|
||||
calendar_data=calendar_data,
|
||||
icon_map=icon_map,
|
||||
@@ -121,7 +143,7 @@ class ContentCalendarDashboard:
|
||||
})
|
||||
st.session_state['show_add_content_dialog'] = False
|
||||
st.success("Content added!")
|
||||
st.experimental_rerun()
|
||||
st.rerun()
|
||||
def handle_generate_with_ai(title, platform, content_type):
|
||||
st.session_state['show_add_content_dialog'] = False
|
||||
st.session_state['show_ai_modal'] = True
|
||||
@@ -145,48 +167,47 @@ class ContentCalendarDashboard:
|
||||
)
|
||||
if st.button("Close"):
|
||||
st.session_state['show_ai_modal'] = False
|
||||
|
||||
with tabs[1]:
|
||||
render_content_optimization(
|
||||
content_generator=self.content_generator,
|
||||
ai_generator=self.ai_generator,
|
||||
seo_optimizer=self.seo_optimizer
|
||||
)
|
||||
|
||||
with tabs[2]:
|
||||
render_ab_testing(self.content_generator, self.calendar_manager)
|
||||
|
||||
render_smart_repurposing_tab()
|
||||
with tabs[3]:
|
||||
render_ab_testing(self.content_generator, None)
|
||||
with tabs[4]:
|
||||
render_content_series_generator(
|
||||
self.ai_generator,
|
||||
self.content_generator,
|
||||
self.seo_optimizer
|
||||
)
|
||||
|
||||
with tabs[4]:
|
||||
with tabs[5]:
|
||||
st.header("Analytics")
|
||||
st.markdown("### Performance Insights")
|
||||
all_content = session.query(ContentItem).all()
|
||||
selected_content = st.selectbox(
|
||||
"Select content to analyze",
|
||||
options=[item.title for item in self.calendar_manager.get_calendar().get_all_content()],
|
||||
options=[item.title for item in all_content],
|
||||
key="analytics_content_select"
|
||||
)
|
||||
if selected_content:
|
||||
content_item = next(
|
||||
item for item in self.calendar_manager.get_calendar().get_all_content()
|
||||
item for item in all_content
|
||||
if item.title == selected_content
|
||||
)
|
||||
render_performance_insights(content_item, self.platform_adapter)
|
||||
|
||||
st.markdown("### Optimization History")
|
||||
if selected_content in st.session_state.optimization_history:
|
||||
st.json(st.session_state.optimization_history[selected_content])
|
||||
|
||||
with tabs[6]:
|
||||
run_scheduler_dashboard()
|
||||
self.logger.info("Dashboard render completed successfully (tabbed UI)")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error rendering dashboard: {str(e)}", exc_info=True)
|
||||
st.error(f"An error occurred: {str(e)}")
|
||||
|
||||
|
||||
def _inject_custom_css(self):
|
||||
st.markdown("""
|
||||
<style>
|
||||
@@ -197,20 +218,16 @@ class ContentCalendarDashboard:
|
||||
def _get_calendar_data(self):
|
||||
self.logger.info("_get_calendar_data called")
|
||||
try:
|
||||
calendar_obj = self.calendar_manager.get_calendar()
|
||||
if not calendar_obj:
|
||||
self.logger.info("No calendar found in manager")
|
||||
return None
|
||||
all_content = session.query(ContentItem).all()
|
||||
data = []
|
||||
for date_str, items in calendar_obj.schedule.items():
|
||||
for item in items:
|
||||
data.append({
|
||||
'date': pd.to_datetime(date_str),
|
||||
'title': item.title,
|
||||
'platform': item.platforms[0] if item.platforms else 'Unknown',
|
||||
'type': item.content_type,
|
||||
'status': item.status
|
||||
})
|
||||
for item in all_content:
|
||||
data.append({
|
||||
'date': item.publish_date,
|
||||
'title': item.title,
|
||||
'platform': item.platforms[0] if item.platforms else 'Unknown',
|
||||
'type': item.content_type.value if hasattr(item.content_type, 'value') else str(item.content_type),
|
||||
'status': item.status
|
||||
})
|
||||
df = pd.DataFrame(data) if data else None
|
||||
return df
|
||||
except Exception as e:
|
||||
@@ -219,10 +236,6 @@ class ContentCalendarDashboard:
|
||||
return None
|
||||
|
||||
def _add_content(self, content):
|
||||
calendar = self.calendar_manager.get_calendar()
|
||||
if not calendar:
|
||||
st.error("No calendar found. Please create a calendar first.")
|
||||
return
|
||||
platform_map = {
|
||||
'Blog': Platform.WEBSITE,
|
||||
'Instagram': Platform.INSTAGRAM,
|
||||
@@ -238,41 +251,32 @@ class ContentCalendarDashboard:
|
||||
'Newsletter': ContentType.NEWSLETTER,
|
||||
}
|
||||
content_type_enum = content_type_map.get(content['type'], ContentType.BLOG_POST)
|
||||
seo_data = SEOData(
|
||||
title=content['title'],
|
||||
meta_description="",
|
||||
keywords=[],
|
||||
structured_data={},
|
||||
)
|
||||
new_item = ContentItem(
|
||||
title=content['title'],
|
||||
description="",
|
||||
content_type=content_type_enum,
|
||||
platforms=[platform_enum],
|
||||
platforms=[platform_enum.value],
|
||||
publish_date=pd.to_datetime(content['publish_date']),
|
||||
seo_data=seo_data,
|
||||
status=content.get('status', 'Draft')
|
||||
status=content.get('status', 'Draft'),
|
||||
author=None,
|
||||
tags=[],
|
||||
notes=None,
|
||||
seo_data={}
|
||||
)
|
||||
calendar.add_content(new_item)
|
||||
self.calendar_manager.save_calendar_to_json()
|
||||
session.add(new_item)
|
||||
session.commit()
|
||||
|
||||
def _delete_content(self, row):
|
||||
calendar = self.calendar_manager.get_calendar()
|
||||
if not calendar:
|
||||
return
|
||||
for date_str, items in list(calendar.schedule.items()):
|
||||
calendar.schedule[date_str] = [
|
||||
item for item in items
|
||||
if not (
|
||||
item.title == row['title'] and
|
||||
str(item.publish_date.date()) == str(row['date'].date()) and
|
||||
item.platforms[0].name == str(row['platform']) and
|
||||
item.content_type.name == str(row['type'])
|
||||
)
|
||||
]
|
||||
if not calendar.schedule[date_str]:
|
||||
del calendar.schedule[date_str]
|
||||
self.calendar_manager.save_calendar_to_json()
|
||||
# Find by title and publish_date (could be improved with unique IDs)
|
||||
all_content = session.query(ContentItem).all()
|
||||
for item in all_content:
|
||||
if (item.title == row['title'] and
|
||||
str(item.publish_date.date()) == str(row['date'].date()) and
|
||||
(item.platforms[0] if item.platforms else 'Unknown') == str(row['platform']) and
|
||||
(item.content_type.value if hasattr(item.content_type, 'value') else str(item.content_type)) == str(row['type'])):
|
||||
session.delete(item)
|
||||
session.commit()
|
||||
break
|
||||
|
||||
def _edit_content(self, row, new_title, new_platform, new_type, new_status):
|
||||
self._delete_content(row)
|
||||
|
||||
Reference in New Issue
Block a user