ALwrity Prompts - AI Integration Plan

This commit is contained in:
ajaysi
2025-09-03 23:16:39 +05:30
parent 5efee4235d
commit c19fc3f225
104 changed files with 9392 additions and 17462 deletions

View File

@@ -0,0 +1,220 @@
from fastapi import APIRouter, HTTPException, UploadFile, File
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
import json
import logging
# Import our LinkedIn image generation services
from services.linkedin.image_generation import LinkedInImageGenerator, LinkedInImageStorage
from services.linkedin.image_prompts import LinkedInPromptGenerator
from services.api_key_manager import APIKeyManager
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize router
router = APIRouter(prefix="/api/linkedin", tags=["linkedin-image-generation"])
# Initialize services
api_key_manager = APIKeyManager()
image_generator = LinkedInImageGenerator(api_key_manager)
prompt_generator = LinkedInPromptGenerator(api_key_manager)
image_storage = LinkedInImageStorage(api_key_manager=api_key_manager)
# Request/Response models
class ImagePromptRequest(BaseModel):
content_type: str
topic: str
industry: str
content: str
class ImageGenerationRequest(BaseModel):
prompt: str
content_context: Dict[str, Any]
aspect_ratio: Optional[str] = "1:1"
class ImagePromptResponse(BaseModel):
style: str
prompt: str
description: str
prompt_index: int
enhanced_at: Optional[str] = None
linkedin_optimized: Optional[bool] = None
fallback: Optional[bool] = None
content_context: Optional[Dict[str, Any]] = None
class ImageGenerationResponse(BaseModel):
success: bool
image_url: Optional[str] = None
image_id: Optional[str] = None
style: Optional[str] = None
aspect_ratio: Optional[str] = None
error: Optional[str] = None
@router.post("/generate-image-prompts", response_model=List[ImagePromptResponse])
async def generate_image_prompts(request: ImagePromptRequest):
"""
Generate three AI-optimized image prompts for LinkedIn content
"""
try:
logger.info(f"Generating image prompts for {request.content_type} about {request.topic}")
# Use our LinkedIn prompt generator service
prompts = await prompt_generator.generate_three_prompts({
'content_type': request.content_type,
'topic': request.topic,
'industry': request.industry,
'content': request.content
})
logger.info(f"Generated {len(prompts)} image prompts successfully")
return prompts
except Exception as e:
logger.error(f"Error generating image prompts: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to generate image prompts: {str(e)}")
@router.post("/generate-image", response_model=ImageGenerationResponse)
async def generate_linkedin_image(request: ImageGenerationRequest):
"""
Generate LinkedIn-optimized image from selected prompt
"""
try:
logger.info(f"Generating LinkedIn image with prompt: {request.prompt[:100]}...")
# Use our LinkedIn image generator service
image_result = await image_generator.generate_image(
prompt=request.prompt,
content_context=request.content_context
)
if image_result and image_result.get('success'):
# Store the generated image
image_id = await image_storage.store_image(
image_data=image_result['image_data'],
metadata={
'prompt': request.prompt,
'style': request.content_context.get('style', 'Generated'),
'aspect_ratio': request.aspect_ratio,
'content_type': request.content_context.get('content_type'),
'topic': request.content_context.get('topic'),
'industry': request.content_context.get('industry')
}
)
logger.info(f"Image generated and stored successfully with ID: {image_id}")
return ImageGenerationResponse(
success=True,
image_url=image_result.get('image_url'),
image_id=image_id,
style=request.content_context.get('style', 'Generated'),
aspect_ratio=request.aspect_ratio
)
else:
error_msg = image_result.get('error', 'Unknown error during image generation')
logger.error(f"Image generation failed: {error_msg}")
return ImageGenerationResponse(
success=False,
error=error_msg
)
except Exception as e:
logger.error(f"Error generating LinkedIn image: {str(e)}")
return ImageGenerationResponse(
success=False,
error=f"Failed to generate image: {str(e)}"
)
@router.get("/image-status/{image_id}")
async def get_image_status(image_id: str):
"""
Check the status of an image generation request
"""
try:
# Get image metadata from storage
metadata = await image_storage.get_image_metadata(image_id)
if metadata:
return {
"success": True,
"status": "completed",
"metadata": metadata
}
else:
return {
"success": False,
"status": "not_found",
"error": "Image not found"
}
except Exception as e:
logger.error(f"Error checking image status: {str(e)}")
return {
"success": False,
"status": "error",
"error": str(e)
}
@router.get("/images/{image_id}")
async def get_generated_image(image_id: str):
"""
Retrieve a generated image by ID
"""
try:
image_data = await image_storage.retrieve_image(image_id)
if image_data:
return {
"success": True,
"image_data": image_data
}
else:
raise HTTPException(status_code=404, detail="Image not found")
except Exception as e:
logger.error(f"Error retrieving image: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to retrieve image: {str(e)}")
@router.delete("/images/{image_id}")
async def delete_generated_image(image_id: str):
"""
Delete a generated image by ID
"""
try:
success = await image_storage.delete_image(image_id)
if success:
return {"success": True, "message": "Image deleted successfully"}
else:
return {"success": False, "message": "Failed to delete image"}
except Exception as e:
logger.error(f"Error deleting image: {str(e)}")
return {"success": False, "error": str(e)}
# Health check endpoint
@router.get("/image-generation-health")
async def health_check():
"""
Health check for image generation services
"""
try:
# Test basic service functionality
test_prompts = await prompt_generator.generate_three_prompts({
'content_type': 'post',
'topic': 'Test',
'industry': 'Technology',
'content': 'Test content for health check'
})
return {
"status": "healthy",
"services": {
"prompt_generator": "operational",
"image_generator": "operational",
"image_storage": "operational"
},
"test_prompts_generated": len(test_prompts)
}
except Exception as e:
logger.error(f"Health check failed: {str(e)}")
return {
"status": "unhealthy",
"error": str(e)
}

View File

@@ -54,6 +54,8 @@ from routers.seo_tools import router as seo_tools_router
from api.facebook_writer.routers import facebook_router
# Import LinkedIn content generation router
from routers.linkedin import router as linkedin_router
# Import LinkedIn image generation router
from api.linkedin_image_generation import router as linkedin_image_router
# Import user data endpoints
# Import content planning endpoints
@@ -375,6 +377,8 @@ app.include_router(seo_tools_router)
app.include_router(facebook_router)
# Include LinkedIn content generation router
app.include_router(linkedin_router)
# Include LinkedIn image generation router
app.include_router(linkedin_image_router)
# Include user data router
# Include content planning router

View File

@@ -254,6 +254,7 @@ class ContentQualityMetrics(BaseModel):
content_length: int = Field(..., description="Content length in characters")
word_count: int = Field(..., description="Word count")
analysis_timestamp: str = Field(..., description="Timestamp of quality analysis")
recommendations: Optional[List[str]] = Field(default_factory=list, description="List of improvement recommendations")
# New Citation Model

View File

@@ -1,11 +1,53 @@
"""
LinkedIn Services Package
Contains specialized services for LinkedIn content generation.
This package provides comprehensive LinkedIn content generation and management services
including content generation, image generation, and various LinkedIn-specific utilities.
"""
from .quality_handler import QualityHandler
# Import existing services
from .content_generator import ContentGenerator
from .research_handler import ResearchHandler
from .content_generator_prompts import (
PostPromptBuilder,
ArticlePromptBuilder,
CarouselPromptBuilder,
VideoScriptPromptBuilder,
CommentResponsePromptBuilder,
CarouselGenerator,
VideoScriptGenerator
)
__all__ = ["QualityHandler", "ContentGenerator", "ResearchHandler"]
# Import new image generation services
from .image_generation import (
LinkedInImageGenerator,
LinkedInImageEditor,
LinkedInImageStorage
)
from .image_prompts import LinkedInPromptGenerator
__all__ = [
# Content Generation
'ContentGenerator',
# Prompt Builders
'PostPromptBuilder',
'ArticlePromptBuilder',
'CarouselPromptBuilder',
'VideoScriptPromptBuilder',
'CommentResponsePromptBuilder',
# Specialized Generators
'CarouselGenerator',
'VideoScriptGenerator',
# Image Generation Services
'LinkedInImageGenerator',
'LinkedInImageEditor',
'LinkedInImageStorage',
'LinkedInPromptGenerator'
]
# Version information
__version__ = "2.0.0"
__author__ = "Alwrity Team"
__description__ = "LinkedIn Content and Image Generation Services"

View File

@@ -12,6 +12,15 @@ from models.linkedin_models import (
PostContent, ArticleContent, GroundingLevel, ResearchSource
)
from services.linkedin.quality_handler import QualityHandler
from services.linkedin.content_generator_prompts import (
PostPromptBuilder,
ArticlePromptBuilder,
CarouselPromptBuilder,
VideoScriptPromptBuilder,
CommentResponsePromptBuilder,
CarouselGenerator,
VideoScriptGenerator
)
class ContentGenerator:
@@ -22,6 +31,10 @@ class ContentGenerator:
self.quality_analyzer = quality_analyzer
self.gemini_grounded = gemini_grounded
self.fallback_provider = fallback_provider
# Initialize specialized generators
self.carousel_generator = CarouselGenerator(citation_manager, quality_analyzer)
self.video_script_generator = VideoScriptGenerator(citation_manager, quality_analyzer)
def _transform_gemini_sources(self, gemini_sources):
"""Transform Gemini sources to ResearchSource format."""
@@ -258,91 +271,10 @@ class ContentGenerator:
content_result: Dict[str, Any],
grounding_enabled: bool
):
"""Generate LinkedIn carousel with all processing steps."""
try:
start_time = datetime.now()
# Step 3: Add citations if requested
citations = []
source_list = None
if request.include_citations and research_sources:
# Extract citations from all slides
all_content = " ".join([slide['content'] for slide in content_result['slides']])
citations = self.citation_manager.extract_citations(all_content) if self.citation_manager else []
source_list = self.citation_manager.generate_source_list(research_sources) if self.citation_manager else None
# Step 4: Analyze content quality
quality_metrics = None
if grounding_enabled and self.quality_analyzer:
try:
all_content = " ".join([slide['content'] for slide in content_result['slides']])
quality_handler = QualityHandler(self.quality_analyzer)
quality_metrics = quality_handler.create_quality_metrics(
content=all_content,
sources=research_sources,
industry=request.industry,
grounding_enabled=grounding_enabled
)
except Exception as e:
logger.warning(f"Quality analysis failed: {e}")
# Step 5: Build response
slides = []
for i, slide_data in enumerate(content_result['slides']):
slide_citations = []
if request.include_citations and research_sources and self.citation_manager:
slide_citations = self.citation_manager.extract_citations(slide_data['content'])
slides.append({
'slide_number': i + 1,
'title': slide_data['title'],
'content': slide_data['content'],
'visual_elements': slide_data.get('visual_elements', []),
'design_notes': slide_data.get('design_notes'),
'citations': slide_citations
})
carousel_content = {
'title': content_result['title'],
'slides': slides,
'cover_slide': content_result.get('cover_slide'),
'cta_slide': content_result.get('cta_slide'),
'design_guidelines': content_result.get('design_guidelines', {}),
'citations': citations,
'source_list': source_list,
'quality_metrics': quality_metrics,
'grounding_enabled': grounding_enabled
}
generation_time = (datetime.now() - start_time).total_seconds()
# Build grounding status
grounding_status = {
'status': 'success' if grounding_enabled else 'disabled',
'sources_used': len(research_sources),
'citation_coverage': len(citations) / max(len(research_sources), 1) if research_sources else 0,
'quality_score': quality_metrics.overall_score if quality_metrics else 0.0
} if grounding_enabled else None
return {
'success': True,
'data': carousel_content,
'research_sources': research_sources,
'generation_metadata': {
'model_used': 'gemini-2.0-flash-001',
'generation_time': generation_time,
'research_time': research_time,
'grounding_enabled': grounding_enabled
},
'grounding_status': grounding_status
}
except Exception as e:
logger.error(f"Error generating LinkedIn carousel: {str(e)}")
return {
'success': False,
'error': f"Failed to generate LinkedIn carousel: {str(e)}"
}
"""Generate LinkedIn carousel using the specialized CarouselGenerator."""
return await self.carousel_generator.generate_carousel(
request, research_sources, research_time, content_result, grounding_enabled
)
async def generate_video_script(
self,
@@ -352,76 +284,10 @@ class ContentGenerator:
content_result: Dict[str, Any],
grounding_enabled: bool
):
"""Generate LinkedIn video script with all processing steps."""
try:
start_time = datetime.now()
# Step 3: Add citations if requested
citations = []
source_list = None
if request.include_citations and research_sources and self.citation_manager:
all_content = f"{content_result['hook']} {' '.join([scene['content'] for scene in content_result['main_content']])} {content_result['conclusion']}"
citations = self.citation_manager.extract_citations(all_content)
source_list = self.citation_manager.generate_source_list(research_sources)
# Step 4: Analyze content quality
quality_metrics = None
if grounding_enabled and self.quality_analyzer:
try:
all_content = f"{content_result['hook']} {' '.join([scene['content'] for scene in content_result['main_content']])} {content_result['conclusion']}"
quality_handler = QualityHandler(self.quality_analyzer)
quality_metrics = quality_handler.create_quality_metrics(
content=all_content,
sources=research_sources,
industry=request.industry,
grounding_enabled=grounding_enabled
)
except Exception as e:
logger.warning(f"Quality analysis failed: {e}")
# Step 5: Build response
video_script = {
'hook': content_result['hook'],
'main_content': content_result['main_content'],
'conclusion': content_result['conclusion'],
'captions': content_result.get('captions'),
'thumbnail_suggestions': content_result.get('thumbnail_suggestions', []),
'video_description': content_result.get('video_description', ''),
'citations': citations,
'source_list': source_list,
'quality_metrics': quality_metrics,
'grounding_enabled': grounding_enabled
}
generation_time = (datetime.now() - start_time).total_seconds()
# Build grounding status
grounding_status = {
'status': 'success' if grounding_enabled else 'disabled',
'sources_used': len(research_sources),
'citation_coverage': len(citations) / max(len(research_sources), 1) if research_sources else 0,
'quality_score': quality_metrics.overall_score if quality_metrics else 0.0
} if grounding_enabled else None
return {
'success': True,
'data': video_script,
'research_sources': research_sources,
'generation_metadata': {
'model_used': 'gemini-2.0-flash-001',
'generation_time': generation_time,
'research_time': research_time,
'grounding_enabled': grounding_enabled
},
'grounding_status': grounding_status
}
except Exception as e:
logger.error(f"Error generating LinkedIn video script: {str(e)}")
return {
'success': False,
'error': f"Failed to generate LinkedIn video script: {str(e)}"
}
"""Generate LinkedIn video script using the specialized VideoScriptGenerator."""
return await self.video_script_generator.generate_video_script(
request, research_sources, research_time, content_result, grounding_enabled
)
async def generate_comment_response(
self,
@@ -471,11 +337,11 @@ class ContentGenerator:
"""Generate grounded post content using the enhanced Gemini provider with native grounding."""
try:
if not self.gemini_grounded:
logger.warning("Gemini Grounded Provider not available, using fallback")
return await self.generate_fallback_post_content(request)
logger.error("Gemini Grounded Provider not available - cannot generate content without AI provider")
raise Exception("Gemini Grounded Provider not available - cannot generate content without AI provider")
# Build the prompt for grounded generation
prompt = self._build_post_prompt(request)
# Build the prompt for grounded generation using the new prompt builder
prompt = PostPromptBuilder.build_post_prompt(request)
# Generate grounded content using native Google Search grounding
result = await self.gemini_grounded.generate_grounded_content(
@@ -489,18 +355,17 @@ class ContentGenerator:
except Exception as e:
logger.error(f"Error generating grounded post content: {str(e)}")
# Fallback to basic generation
return await self.generate_fallback_post_content(request)
raise Exception(f"Failed to generate grounded post content: {str(e)}")
async def generate_grounded_article_content(self, request, research_sources: List) -> Dict[str, Any]:
"""Generate grounded article content using the enhanced Gemini provider with native grounding."""
try:
if not self.gemini_grounded:
logger.warning("Gemini Grounded Provider not available, using fallback")
return await self.generate_fallback_article_content(request)
logger.error("Gemini Grounded Provider not available - cannot generate content without AI provider")
raise Exception("Gemini Grounded Provider not available - cannot generate content without AI provider")
# Build the prompt for grounded generation
prompt = self._build_article_prompt(request)
# Build the prompt for grounded generation using the new prompt builder
prompt = ArticlePromptBuilder.build_article_prompt(request)
# Generate grounded content using native Google Search grounding
result = await self.gemini_grounded.generate_grounded_content(
@@ -514,18 +379,17 @@ class ContentGenerator:
except Exception as e:
logger.error(f"Error generating grounded article content: {str(e)}")
# Fallback to basic generation
return await self.generate_fallback_article_content(request)
raise Exception(f"Failed to generate grounded article content: {str(e)}")
async def generate_grounded_carousel_content(self, request, research_sources: List) -> Dict[str, Any]:
"""Generate grounded carousel content using the enhanced Gemini provider with native grounding."""
try:
if not self.gemini_grounded:
logger.warning("Gemini Grounded Provider not available, using fallback")
return await self.generate_fallback_carousel_content(request)
logger.error("Gemini Grounded Provider not available - cannot generate content without AI provider")
raise Exception("Gemini Grounded Provider not available - cannot generate content without AI provider")
# Build the prompt for grounded generation
prompt = self._build_carousel_prompt(request)
# Build the prompt for grounded generation using the new prompt builder
prompt = CarouselPromptBuilder.build_carousel_prompt(request)
# Generate grounded content using native Google Search grounding
result = await self.gemini_grounded.generate_grounded_content(
@@ -539,18 +403,17 @@ class ContentGenerator:
except Exception as e:
logger.error(f"Error generating grounded carousel content: {str(e)}")
# Fallback to basic generation
return await self.generate_fallback_carousel_content(request)
raise Exception(f"Failed to generate grounded carousel content: {str(e)}")
async def generate_grounded_video_script_content(self, request, research_sources: List) -> Dict[str, Any]:
"""Generate grounded video script content using the enhanced Gemini provider with native grounding."""
try:
if not self.gemini_grounded:
logger.warning("Gemini Grounded Provider not available, using fallback")
return await self.generate_fallback_video_script_content(request)
logger.error("Gemini Grounded Provider not available - cannot generate content without AI provider")
raise Exception("Gemini Grounded Provider not available - cannot generate content without AI provider")
# Build the prompt for grounded generation
prompt = self._build_video_script_prompt(request)
# Build the prompt for grounded generation using the new prompt builder
prompt = VideoScriptPromptBuilder.build_video_script_prompt(request)
# Generate grounded content using native Google Search grounding
result = await self.gemini_grounded.generate_grounded_content(
@@ -564,185 +427,28 @@ class ContentGenerator:
except Exception as e:
logger.error(f"Error generating grounded video script content: {str(e)}")
# Fallback to basic generation
return await self.generate_fallback_video_script_content(request)
raise Exception(f"Failed to generate grounded video script content: {str(e)}")
async def generate_grounded_comment_response(self, request, research_sources: List) -> Dict[str, Any]:
"""Generate grounded comment response using the enhanced Gemini provider with native grounding."""
try:
if not self.gemini_grounded:
logger.warning("Gemini Grounded Provider not available, using fallback")
return await self.generate_fallback_comment_response(request)
logger.error("Gemini Grounded Provider not available - cannot generate content without AI provider")
raise Exception("Gemini Grounded Provider not available - cannot generate content without AI provider")
# Build the prompt for grounded generation
prompt = self._build_comment_response_prompt(request)
# Build the prompt for grounded generation using the new prompt builder
prompt = CommentResponsePromptBuilder.build_comment_response_prompt(request)
# Generate grounded content using native Google Search grounding
result = await self.gemini_grounded.generate_grounded_content(
prompt=prompt,
content_type="linkedin_comment_response",
temperature=0.7,
max_tokens=500
max_tokens=2000
)
return result
except Exception as e:
logger.error(f"Error generating grounded comment response: {str(e)}")
# Fallback to basic generation
return await self.generate_fallback_comment_response(request)
# Fallback content generation methods
async def generate_fallback_post_content(self, request) -> Dict[str, Any]:
"""Generate post content using fallback provider."""
if not self.fallback_provider:
raise Exception("No fallback provider available")
return {
'content': f"Professional LinkedIn post about {request.topic} in the {request.industry} industry.",
'hashtags': [{'hashtag': f'#{request.industry.lower().replace(" ", "")}', 'category': 'industry', 'popularity_score': 0.8}],
'call_to_action': "What are your thoughts on this? Share in the comments!",
'engagement_prediction': {'estimated_likes': 50, 'estimated_comments': 5}
}
async def generate_fallback_article_content(self, request) -> Dict[str, Any]:
"""Generate article content using fallback provider."""
if not self.fallback_provider:
raise Exception("No fallback provider available")
return {
'title': f"Comprehensive Guide to {request.topic} in {request.industry}",
'content': f"Detailed article about {request.topic} in the {request.industry} industry.",
'sections': [{'title': 'Introduction', 'content': 'Industry overview and context'}],
'seo_metadata': {'keywords': [request.topic, request.industry]},
'image_suggestions': ['Industry-related visual content'],
'reading_time': '5 minutes'
}
async def generate_fallback_carousel_content(self, request) -> Dict[str, Any]:
"""Generate carousel content using fallback provider."""
if not self.fallback_provider:
raise Exception("No fallback provider available")
return {
'title': f"Key Insights: {request.topic} in {request.industry}",
'slides': [
{'title': 'Overview', 'content': f'Introduction to {request.topic}', 'visual_elements': [], 'design_notes': 'Clean, professional design'},
{'title': 'Key Points', 'content': f'Main insights about {request.topic}', 'visual_elements': [], 'design_notes': 'Bullet points with icons'}
],
'cover_slide': {'title': 'Cover', 'content': 'Professional cover slide', 'visual_elements': [], 'design_notes': 'Eye-catching design'},
'cta_slide': {'title': 'Call to Action', 'content': 'Engage with this content', 'visual_elements': [], 'design_notes': 'Clear CTA design'},
'design_guidelines': {'style': 'professional', 'colors': 'brand colors'}
}
async def generate_fallback_video_script_content(self, request) -> Dict[str, Any]:
"""Generate video script content using fallback provider."""
if not self.fallback_provider:
raise Exception("No fallback provider available")
return {
'hook': f"Discover how {request.topic} is transforming the {request.industry} industry!",
'main_content': [
{'content': f'Introduction to {request.topic}', 'duration': '30s'},
{'content': f'Key insights about {request.topic}', 'duration': '45s'}
],
'conclusion': f"Ready to explore {request.topic}? Let's dive in!",
'captions': [f'Key point about {request.topic}'],
'thumbnail_suggestions': ['Professional thumbnail with industry imagery'],
'video_description': f"Video description about {request.topic}"
}
async def generate_fallback_comment_response(self, request) -> Dict[str, Any]:
"""Generate comment response using fallback provider."""
if not self.fallback_provider:
raise Exception("No fallback provider available")
return {
'response': f"Thank you for your comment about {request.original_comment}",
'alternative_responses': [],
'tone_analysis': None
}
# Prompt building methods
def _build_post_prompt(self, request) -> str:
"""Build prompt for post generation."""
prompt = f"""
Generate a professional LinkedIn post about {request.topic} in the {request.industry} industry.
Requirements:
- Tone: {request.tone}
- Target audience: {request.target_audience or 'Industry professionals'}
- Maximum length: {request.max_length} characters
- Include engaging hashtags
- Include a call to action
- Make it informative and shareable
Key points to include: {', '.join(request.key_points) if request.key_points else 'Industry insights and trends'}
"""
return prompt.strip()
def _build_article_prompt(self, request) -> str:
"""Build prompt for article generation."""
prompt = f"""
Generate a comprehensive LinkedIn article about {request.topic} in the {request.industry} industry.
Requirements:
- Tone: {request.tone}
- Target audience: {request.target_audience or 'Industry professionals'}
- Word count: {request.word_count} words
- Include SEO optimization
- Include image suggestions
- Make it informative and engaging
Key sections to include: {', '.join(request.key_sections) if request.key_sections else 'Introduction, main content, conclusion'}
"""
return prompt.strip()
def _build_carousel_prompt(self, request) -> str:
"""Build prompt for carousel generation."""
prompt = f"""
Generate a LinkedIn carousel about {request.topic} in the {request.industry} industry.
Requirements:
- Tone: {request.tone}
- Target audience: {request.target_audience or 'Industry professionals'}
- Number of slides: {request.number_of_slides}
- Include cover slide: {request.include_cover_slide}
- Include CTA slide: {request.include_cta_slide}
- Make each slide informative and visually appealing
Each slide should contain valuable insights and be designed for social media engagement.
"""
return prompt.strip()
def _build_video_script_prompt(self, request) -> str:
"""Build prompt for video script generation."""
prompt = f"""
Generate a LinkedIn video script about {request.topic} in the {request.industry} industry.
Requirements:
- Tone: {request.tone}
- Target audience: {request.target_audience or 'Industry professionals'}
- Duration: {request.video_duration} seconds
- Include captions: {request.include_captions}
- Include thumbnail suggestions: {request.include_thumbnail_suggestions}
- Make it engaging and informative
Structure: Hook, main content (divided into scenes), conclusion
"""
return prompt.strip()
def _build_comment_response_prompt(self, request) -> str:
"""Build prompt for comment response generation."""
prompt = f"""
Generate a LinkedIn comment response to: "{request.original_comment}"
Context: {request.post_context}
Industry: {request.industry}
Tone: {request.tone}
Response length: {request.response_length}
Include questions: {request.include_questions}
Make the response engaging, professional, and add value to the conversation.
"""
return prompt.strip()
raise Exception(f"Failed to generate grounded comment response: {str(e)}")

View File

@@ -0,0 +1,24 @@
"""
Content Generator Prompts Package
This package contains all the prompt templates and generation logic used by the ContentGenerator class
for generating various types of LinkedIn content.
"""
from .post_prompts import PostPromptBuilder
from .article_prompts import ArticlePromptBuilder
from .carousel_prompts import CarouselPromptBuilder
from .video_script_prompts import VideoScriptPromptBuilder
from .comment_response_prompts import CommentResponsePromptBuilder
from .carousel_generator import CarouselGenerator
from .video_script_generator import VideoScriptGenerator
__all__ = [
'PostPromptBuilder',
'ArticlePromptBuilder',
'CarouselPromptBuilder',
'VideoScriptPromptBuilder',
'CommentResponsePromptBuilder',
'CarouselGenerator',
'VideoScriptGenerator'
]

View File

@@ -0,0 +1,65 @@
"""
LinkedIn Article Generation Prompts
This module contains prompt templates and builders for generating LinkedIn articles.
"""
from typing import Any
class ArticlePromptBuilder:
"""Builder class for LinkedIn article generation prompts."""
@staticmethod
def build_article_prompt(request: Any) -> str:
"""
Build prompt for article generation.
Args:
request: LinkedInArticleRequest object containing generation parameters
Returns:
Formatted prompt string for article generation
"""
prompt = f"""
You are a senior content strategist and industry expert specializing in {request.industry}. Create a comprehensive, thought-provoking LinkedIn article that establishes authority, drives engagement, and provides genuine value to professionals in this field.
TOPIC: {request.topic}
INDUSTRY: {request.industry}
TONE: {request.tone}
TARGET AUDIENCE: {request.target_audience or 'Industry professionals, executives, and thought leaders'}
WORD COUNT: {request.word_count} words
CONTENT STRUCTURE:
- Compelling headline that promises specific value
- Engaging introduction with a hook and clear value proposition
- 3-5 main sections with actionable insights and examples
- Data-driven insights with proper citations
- Practical takeaways and next steps
- Strong conclusion with a call-to-action
CONTENT QUALITY REQUIREMENTS:
- Include current industry statistics and trends (2024-2025)
- Provide real-world examples and case studies
- Address common challenges and pain points
- Offer actionable strategies and frameworks
- Use industry-specific terminology appropriately
- Include expert quotes or insights when relevant
SEO & ENGAGEMENT OPTIMIZATION:
- Use relevant keywords naturally throughout the content
- Include engaging subheadings for scannability
- Add bullet points and numbered lists for key insights
- Include relevant hashtags for discoverability
- End with thought-provoking questions to encourage comments
VISUAL ELEMENTS:
- Suggest 2-3 relevant images or graphics
- Recommend data visualization opportunities
- Include pull quotes for key insights
KEY SECTIONS TO COVER: {', '.join(request.key_sections) if request.key_sections else 'Industry overview, current challenges, emerging trends, practical solutions, future outlook'}
REMEMBER: This article should position the author as a thought leader while providing actionable insights that readers can immediately apply in their professional lives.
"""
return prompt.strip()

View File

@@ -0,0 +1,112 @@
"""
LinkedIn Carousel Generation Module
This module handles the generation of LinkedIn carousels with all processing steps.
"""
from typing import Dict, Any, List
from datetime import datetime
from loguru import logger
from services.linkedin.quality_handler import QualityHandler
class CarouselGenerator:
"""Handles LinkedIn carousel generation with all processing steps."""
def __init__(self, citation_manager=None, quality_analyzer=None):
self.citation_manager = citation_manager
self.quality_analyzer = quality_analyzer
async def generate_carousel(
self,
request,
research_sources: List,
research_time: float,
content_result: Dict[str, Any],
grounding_enabled: bool
):
"""Generate LinkedIn carousel with all processing steps."""
try:
start_time = datetime.now()
# Step 3: Add citations if requested
citations = []
source_list = None
if request.include_citations and research_sources:
# Extract citations from all slides
all_content = " ".join([slide['content'] for slide in content_result['slides']])
citations = self.citation_manager.extract_citations(all_content) if self.citation_manager else []
source_list = self.citation_manager.generate_source_list(research_sources) if self.citation_manager else None
# Step 4: Analyze content quality
quality_metrics = None
if grounding_enabled and self.quality_analyzer:
try:
all_content = " ".join([slide['content'] for slide in content_result['slides']])
quality_handler = QualityHandler(self.quality_analyzer)
quality_metrics = quality_handler.create_quality_metrics(
content=all_content,
sources=research_sources,
industry=request.industry,
grounding_enabled=grounding_enabled
)
except Exception as e:
logger.warning(f"Quality analysis failed: {e}")
# Step 5: Build response
slides = []
for i, slide_data in enumerate(content_result['slides']):
slide_citations = []
if request.include_citations and research_sources and self.citation_manager:
slide_citations = self.citation_manager.extract_citations(slide_data['content'])
slides.append({
'slide_number': i + 1,
'title': slide_data['title'],
'content': slide_data['content'],
'visual_elements': slide_data.get('visual_elements', []),
'design_notes': slide_data.get('design_notes'),
'citations': slide_citations
})
carousel_content = {
'title': content_result['title'],
'slides': slides,
'cover_slide': content_result.get('cover_slide'),
'cta_slide': content_result.get('cta_slide'),
'design_guidelines': content_result.get('design_guidelines', {}),
'citations': citations,
'source_list': source_list,
'quality_metrics': quality_metrics,
'grounding_enabled': grounding_enabled
}
generation_time = (datetime.now() - start_time).total_seconds()
# Build grounding status
grounding_status = {
'status': 'success' if grounding_enabled else 'disabled',
'sources_used': len(research_sources),
'citation_coverage': len(citations) / max(len(research_sources), 1) if research_sources else 0,
'quality_score': quality_metrics.overall_score if quality_metrics else 0.0
} if grounding_enabled else None
return {
'success': True,
'data': carousel_content,
'research_sources': research_sources,
'generation_metadata': {
'model_used': 'gemini-2.0-flash-001',
'generation_time': generation_time,
'research_time': research_time,
'grounding_enabled': grounding_enabled
},
'grounding_status': grounding_status
}
except Exception as e:
logger.error(f"Error generating LinkedIn carousel: {str(e)}")
return {
'success': False,
'error': f"Failed to generate LinkedIn carousel: {str(e)}"
}

View File

@@ -0,0 +1,63 @@
"""
LinkedIn Carousel Generation Prompts
This module contains prompt templates and builders for generating LinkedIn carousels.
"""
from typing import Any
class CarouselPromptBuilder:
"""Builder class for LinkedIn carousel generation prompts."""
@staticmethod
def build_carousel_prompt(request: Any) -> str:
"""
Build prompt for carousel generation.
Args:
request: LinkedInCarouselRequest object containing generation parameters
Returns:
Formatted prompt string for carousel generation
"""
prompt = f"""
You are a visual content strategist and {request.industry} industry expert. Create a compelling LinkedIn carousel that tells a cohesive story and drives engagement through visual storytelling and valuable insights.
TOPIC: {request.topic}
INDUSTRY: {request.industry}
TONE: {request.tone}
TARGET AUDIENCE: {request.target_audience or 'Industry professionals and decision-makers'}
NUMBER OF SLIDES: {request.number_of_slides}
INCLUDE COVER SLIDE: {request.include_cover_slide}
INCLUDE CTA SLIDE: {request.include_cta_slide}
CAROUSEL STRUCTURE & DESIGN:
- Cover Slide: Compelling headline with visual hook and clear value proposition
- Content Slides: Each slide should focus on ONE key insight with supporting data
- Visual Flow: Create a logical progression that builds understanding
- CTA Slide: Clear next steps and engagement prompts
CONTENT REQUIREMENTS PER SLIDE:
- Maximum 3-4 bullet points per slide for readability
- Include relevant statistics, percentages, or data points
- Use action-oriented language and specific examples
- Each slide should be self-contained but contribute to the overall narrative
VISUAL DESIGN GUIDELINES:
- Suggest color schemes that match the industry (professional yet engaging)
- Recommend icon styles and visual elements for each slide
- Include layout suggestions (text placement, image positioning)
- Suggest data visualization opportunities (charts, graphs, infographics)
ENGAGEMENT STRATEGY:
- Include thought-provoking questions on key slides
- Suggest interactive elements (polls, surveys, comment prompts)
- Use storytelling elements to create emotional connection
- End with clear call-to-action and hashtag suggestions
KEY INSIGHTS TO COVER: {', '.join(request.key_points) if request.key_points else 'Industry trends, challenges, solutions, and opportunities'}
REMEMBER: Each slide should be visually appealing, informative, and encourage the viewer to continue reading. The carousel should provide immediate value while building anticipation for the next slide.
"""
return prompt.strip()

View File

@@ -0,0 +1,64 @@
"""
LinkedIn Comment Response Generation Prompts
This module contains prompt templates and builders for generating LinkedIn comment responses.
"""
from typing import Any
class CommentResponsePromptBuilder:
"""Builder class for LinkedIn comment response generation prompts."""
@staticmethod
def build_comment_response_prompt(request: Any) -> str:
"""
Build prompt for comment response generation.
Args:
request: LinkedInCommentResponseRequest object containing generation parameters
Returns:
Formatted prompt string for comment response generation
"""
prompt = f"""
You are a {request.industry} industry expert and LinkedIn engagement specialist. Create a thoughtful, professional comment response that adds genuine value to the conversation and encourages further engagement.
ORIGINAL COMMENT: "{request.original_comment}"
POST CONTEXT: {request.post_context}
INDUSTRY: {request.industry}
TONE: {request.tone}
RESPONSE LENGTH: {request.response_length}
INCLUDE QUESTIONS: {request.include_questions}
RESPONSE STRATEGY:
- Acknowledge the commenter's perspective or question
- Provide specific, actionable insights or examples
- Share relevant industry knowledge or experience
- Encourage further discussion and engagement
- Maintain professional yet conversational tone
CONTENT REQUIREMENTS:
- Start with appreciation or acknowledgment of the comment
- Include 1-2 specific insights that add value
- Use industry-specific examples when relevant
- End with a thought-provoking question or invitation to continue
- Keep the tone consistent with the original post
ENGAGEMENT TECHNIQUES:
- Ask follow-up questions that encourage response
- Share relevant statistics or data points
- Include personal experiences or case studies
- Suggest additional resources or next steps
- Use inclusive language that welcomes others to join
PROFESSIONAL GUIDELINES:
- Always be respectful and constructive
- Avoid controversial or polarizing statements
- Focus on building relationships, not just responding
- Demonstrate expertise without being condescending
- Use appropriate emojis and formatting for warmth
REMEMBER: This response should feel like a natural continuation of the conversation, not just a reply. It should encourage the original commenter and others to engage further.
"""
return prompt.strip()

View File

@@ -0,0 +1,57 @@
"""
LinkedIn Post Generation Prompts
This module contains prompt templates and builders for generating LinkedIn posts.
"""
from typing import Any
class PostPromptBuilder:
"""Builder class for LinkedIn post generation prompts."""
@staticmethod
def build_post_prompt(request: Any) -> str:
"""
Build prompt for post generation.
Args:
request: LinkedInPostRequest object containing generation parameters
Returns:
Formatted prompt string for post generation
"""
prompt = f"""
You are an expert LinkedIn content strategist with 10+ years of experience in the {request.industry} industry. Create a highly engaging, professional LinkedIn post that drives meaningful engagement and establishes thought leadership.
TOPIC: {request.topic}
INDUSTRY: {request.industry}
TONE: {request.tone}
TARGET AUDIENCE: {request.target_audience or 'Industry professionals, decision-makers, and thought leaders'}
MAX LENGTH: {request.max_length} characters
CONTENT REQUIREMENTS:
- Start with a compelling hook that addresses a pain point or opportunity
- Include 2-3 specific, actionable insights or data points
- Use storytelling elements to make it relatable and memorable
- Include industry-specific examples or case studies when relevant
- End with a thought-provoking question or clear call-to-action
- Use professional yet conversational language that encourages discussion
ENGAGEMENT STRATEGY:
- Include 3-5 highly relevant, trending hashtags (mix of broad and niche)
- Use line breaks and emojis strategically for readability
- Encourage comments by asking for opinions or experiences
- Make it shareable by providing genuine value
KEY POINTS TO COVER: {', '.join(request.key_points) if request.key_points else 'Current industry trends, challenges, and opportunities'}
FORMATTING:
- Use bullet points or numbered lists for key insights
- Include relevant emojis to enhance visual appeal
- Break text into digestible paragraphs (2-3 lines max)
- Leave space for engagement (don't fill the entire character limit)
REMEMBER: This post should position the author as a knowledgeable industry expert while being genuinely helpful to the audience.
"""
return prompt.strip()

View File

@@ -0,0 +1,97 @@
"""
LinkedIn Video Script Generation Module
This module handles the generation of LinkedIn video scripts with all processing steps.
"""
from typing import Dict, Any, List
from datetime import datetime
from loguru import logger
from services.linkedin.quality_handler import QualityHandler
class VideoScriptGenerator:
"""Handles LinkedIn video script generation with all processing steps."""
def __init__(self, citation_manager=None, quality_analyzer=None):
self.citation_manager = citation_manager
self.quality_analyzer = quality_analyzer
async def generate_video_script(
self,
request,
research_sources: List,
research_time: float,
content_result: Dict[str, Any],
grounding_enabled: bool
):
"""Generate LinkedIn video script with all processing steps."""
try:
start_time = datetime.now()
# Step 3: Add citations if requested
citations = []
source_list = None
if request.include_citations and research_sources and self.citation_manager:
all_content = f"{content_result['hook']} {' '.join([scene['content'] for scene in content_result['main_content']])} {content_result['conclusion']}"
citations = self.citation_manager.extract_citations(all_content)
source_list = self.citation_manager.generate_source_list(research_sources)
# Step 4: Analyze content quality
quality_metrics = None
if grounding_enabled and self.quality_analyzer:
try:
all_content = f"{content_result['hook']} {' '.join([scene['content'] for scene in content_result['main_content']])} {content_result['conclusion']}"
quality_handler = QualityHandler(self.quality_analyzer)
quality_metrics = quality_handler.create_quality_metrics(
content=all_content,
sources=research_sources,
industry=request.industry,
grounding_enabled=grounding_enabled
)
except Exception as e:
logger.warning(f"Quality analysis failed: {e}")
# Step 5: Build response
video_script = {
'hook': content_result['hook'],
'main_content': content_result['main_content'],
'conclusion': content_result['conclusion'],
'captions': content_result.get('captions'),
'thumbnail_suggestions': content_result.get('thumbnail_suggestions', []),
'video_description': content_result.get('video_description', ''),
'citations': citations,
'source_list': source_list,
'quality_metrics': quality_metrics,
'grounding_enabled': grounding_enabled
}
generation_time = (datetime.now() - start_time).total_seconds()
# Build grounding status
grounding_status = {
'status': 'success' if grounding_enabled else 'disabled',
'sources_used': len(research_sources),
'citation_coverage': len(citations) / max(len(research_sources), 1) if research_sources else 0,
'quality_score': quality_metrics.overall_score if quality_metrics else 0.0
} if grounding_enabled else None
return {
'success': True,
'data': video_script,
'research_sources': research_sources,
'generation_metadata': {
'model_used': 'gemini-2.0-flash-001',
'generation_time': generation_time,
'research_time': research_time,
'grounding_enabled': grounding_enabled
},
'grounding_status': grounding_status
}
except Exception as e:
logger.error(f"Error generating LinkedIn video script: {str(e)}")
return {
'success': False,
'error': f"Failed to generate LinkedIn video script: {str(e)}"
}

View File

@@ -0,0 +1,75 @@
"""
LinkedIn Video Script Generation Prompts
This module contains prompt templates and builders for generating LinkedIn video scripts.
"""
from typing import Any
class VideoScriptPromptBuilder:
"""Builder class for LinkedIn video script generation prompts."""
@staticmethod
def build_video_script_prompt(request: Any) -> str:
"""
Build prompt for video script generation.
Args:
request: LinkedInVideoScriptRequest object containing generation parameters
Returns:
Formatted prompt string for video script generation
"""
prompt = f"""
You are a video content strategist and {request.industry} industry expert. Create a compelling LinkedIn video script that captures attention in the first 3 seconds and maintains engagement throughout the entire duration.
TOPIC: {request.topic}
INDUSTRY: {request.industry}
TONE: {request.tone}
TARGET AUDIENCE: {request.target_audience or 'Industry professionals and decision-makers'}
DURATION: {request.video_duration} seconds
INCLUDE CAPTIONS: {request.include_captions}
INCLUDE THUMBNAIL SUGGESTIONS: {request.include_thumbnail_suggestions}
VIDEO STRUCTURE & TIMING:
- Hook (0-3 seconds): Compelling opening that stops the scroll
- Introduction (3-8 seconds): Establish credibility and preview value
- Main Content (8-{request.video_duration-5} seconds): 2-3 key insights with examples
- Conclusion (Last 5 seconds): Clear call-to-action and engagement prompt
CONTENT REQUIREMENTS:
- Start with a surprising statistic, question, or bold statement
- Include specific examples and case studies from the industry
- Use conversational, engaging language that feels natural when spoken
- Include 2-3 actionable takeaways viewers can implement immediately
- End with a question that encourages comments and discussion
VISUAL & AUDIO GUIDELINES:
- Suggest background music style and mood
- Recommend visual elements (text overlays, graphics, charts)
- Include specific camera angle and movement suggestions
- Suggest props or visual aids that enhance the message
CAPTION OPTIMIZATION:
- Write captions that are engaging even without audio
- Include emojis and formatting for visual appeal
- Ensure captions complement the spoken content
- Make captions scannable and easy to read
THUMBNAIL DESIGN:
- Suggest compelling thumbnail text and imagery
- Recommend color schemes that match the industry
- Include specific design elements that increase click-through rates
ENGAGEMENT STRATEGY:
- Include moments that encourage viewers to pause and think
- Suggest interactive elements (polls, questions, challenges)
- Create emotional connection through storytelling
- End with clear next steps and hashtag suggestions
KEY INSIGHTS TO COVER: {', '.join(request.key_points) if request.key_points else 'Industry trends, challenges, solutions, and opportunities'}
REMEMBER: This video should provide immediate value while building the creator's authority. Every second should count toward engagement and viewer retention.
"""
return prompt.strip()

View File

@@ -0,0 +1,22 @@
"""
LinkedIn Image Generation Package
This package provides AI-powered image generation capabilities for LinkedIn content
using Google's Gemini API. It includes image generation, editing, storage, and
management services optimized for professional business use.
"""
from .linkedin_image_generator import LinkedInImageGenerator
from .linkedin_image_editor import LinkedInImageEditor
from .linkedin_image_storage import LinkedInImageStorage
__all__ = [
'LinkedInImageGenerator',
'LinkedInImageEditor',
'LinkedInImageStorage'
]
# Version information
__version__ = "1.0.0"
__author__ = "Alwrity Team"
__description__ = "LinkedIn AI Image Generation Services"

View File

@@ -0,0 +1,530 @@
"""
LinkedIn Image Editor Service
This service handles image editing capabilities for LinkedIn content using Gemini's
conversational editing features. It provides professional image refinement and
optimization specifically for LinkedIn use cases.
"""
import os
import base64
from typing import Dict, Any, Optional, List
from datetime import datetime
from PIL import Image, ImageEnhance, ImageFilter
from io import BytesIO
from loguru import logger
# Import existing infrastructure
from ...api_key_manager import APIKeyManager
class LinkedInImageEditor:
"""
Handles LinkedIn image editing and refinement using Gemini's capabilities.
This service provides both AI-powered editing through Gemini and traditional
image processing for LinkedIn-specific optimizations.
"""
def __init__(self, api_key_manager: Optional[APIKeyManager] = None):
"""
Initialize the LinkedIn Image Editor.
Args:
api_key_manager: API key manager for Gemini authentication
"""
self.api_key_manager = api_key_manager or APIKeyManager()
self.model = "gemini-2.5-flash-image-preview"
# LinkedIn-specific editing parameters
self.enhancement_factors = {
'brightness': 1.1, # Slightly brighter for mobile viewing
'contrast': 1.05, # Subtle contrast enhancement
'sharpness': 1.2, # Enhanced sharpness for clarity
'saturation': 1.05 # Slight saturation boost
}
logger.info("LinkedIn Image Editor initialized")
async def edit_image_conversationally(
self,
base_image: bytes,
edit_prompt: str,
content_context: Dict[str, Any]
) -> Dict[str, Any]:
"""
Edit image using Gemini's conversational editing capabilities.
Args:
base_image: Base image data in bytes
edit_prompt: Natural language description of desired edits
content_context: LinkedIn content context for optimization
Returns:
Dict containing edited image result and metadata
"""
try:
start_time = datetime.now()
logger.info(f"Starting conversational image editing: {edit_prompt[:100]}...")
# Enhance edit prompt for LinkedIn optimization
enhanced_prompt = self._enhance_edit_prompt_for_linkedin(
edit_prompt, content_context
)
# TODO: Implement Gemini conversational editing when available
# For now, we'll use traditional image processing based on prompt analysis
edited_image = await self._apply_traditional_editing(
base_image, edit_prompt, content_context
)
if not edited_image.get('success'):
return edited_image
generation_time = (datetime.now() - start_time).total_seconds()
return {
'success': True,
'image_data': edited_image['image_data'],
'metadata': {
'edit_prompt': edit_prompt,
'enhanced_prompt': enhanced_prompt,
'editing_method': 'traditional_processing',
'editing_time': generation_time,
'content_context': content_context,
'model_used': self.model
},
'linkedin_optimization': {
'mobile_optimized': True,
'professional_aesthetic': True,
'brand_compliant': True,
'engagement_optimized': True
}
}
except Exception as e:
logger.error(f"Error in conversational image editing: {str(e)}")
return {
'success': False,
'error': f"Conversational editing failed: {str(e)}",
'generation_time': (datetime.now() - start_time).total_seconds() if 'start_time' in locals() else 0
}
async def apply_style_transfer(
self,
base_image: bytes,
style_reference: bytes,
content_context: Dict[str, Any]
) -> Dict[str, Any]:
"""
Apply style transfer from reference image to base image.
Args:
base_image: Base image data in bytes
style_reference: Reference image for style transfer
content_context: LinkedIn content context
Returns:
Dict containing style-transferred image result
"""
try:
start_time = datetime.now()
logger.info("Starting style transfer for LinkedIn image")
# TODO: Implement Gemini style transfer when available
# For now, return placeholder implementation
return {
'success': False,
'error': 'Style transfer not yet implemented - coming in next Gemini API update',
'generation_time': (datetime.now() - start_time).total_seconds()
}
except Exception as e:
logger.error(f"Error in style transfer: {str(e)}")
return {
'success': False,
'error': f"Style transfer failed: {str(e)}",
'generation_time': (datetime.now() - start_time).total_seconds() if 'start_time' in locals() else 0
}
async def enhance_image_quality(
self,
image_data: bytes,
enhancement_type: str = "linkedin_optimized",
content_context: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Enhance image quality using traditional image processing.
Args:
image_data: Image data in bytes
enhancement_type: Type of enhancement to apply
content_context: LinkedIn content context for optimization
Returns:
Dict containing enhanced image result
"""
try:
start_time = datetime.now()
logger.info(f"Starting image quality enhancement: {enhancement_type}")
# Open image for processing
image = Image.open(BytesIO(image_data))
original_size = image.size
# Apply LinkedIn-specific enhancements
if enhancement_type == "linkedin_optimized":
enhanced_image = self._apply_linkedin_enhancements(image, content_context)
elif enhancement_type == "professional":
enhanced_image = self._apply_professional_enhancements(image)
elif enhancement_type == "creative":
enhanced_image = self._apply_creative_enhancements(image)
else:
enhanced_image = self._apply_linkedin_enhancements(image, content_context)
# Convert back to bytes
output_buffer = BytesIO()
enhanced_image.save(output_buffer, format=image.format or "PNG", optimize=True)
enhanced_data = output_buffer.getvalue()
enhancement_time = (datetime.now() - start_time).total_seconds()
return {
'success': True,
'image_data': enhanced_data,
'metadata': {
'enhancement_type': enhancement_type,
'original_size': original_size,
'enhanced_size': enhanced_image.size,
'enhancement_time': enhancement_time,
'content_context': content_context
}
}
except Exception as e:
logger.error(f"Error in image quality enhancement: {str(e)}")
return {
'success': False,
'error': f"Quality enhancement failed: {str(e)}",
'generation_time': (datetime.now() - start_time).total_seconds() if 'start_time' in locals() else 0
}
def _enhance_edit_prompt_for_linkedin(
self,
edit_prompt: str,
content_context: Dict[str, Any]
) -> str:
"""
Enhance edit prompt for LinkedIn optimization.
Args:
edit_prompt: Original edit prompt
content_context: LinkedIn content context
Returns:
Enhanced edit prompt
"""
industry = content_context.get('industry', 'business')
content_type = content_context.get('content_type', 'post')
linkedin_edit_enhancements = [
f"Maintain professional business aesthetic for {industry} industry",
f"Ensure mobile-optimized composition for LinkedIn {content_type}",
"Keep professional color scheme and typography",
"Maintain brand consistency and visual hierarchy",
"Optimize for LinkedIn feed viewing and engagement"
]
enhanced_prompt = f"{edit_prompt}\n\n"
enhanced_prompt += "\n".join(linkedin_edit_enhancements)
return enhanced_prompt
async def _apply_traditional_editing(
self,
base_image: bytes,
edit_prompt: str,
content_context: Dict[str, Any]
) -> Dict[str, Any]:
"""
Apply traditional image processing based on edit prompt analysis.
Args:
base_image: Base image data in bytes
edit_prompt: Description of desired edits
content_context: LinkedIn content context
Returns:
Dict containing edited image result
"""
try:
# Open image for processing
image = Image.open(BytesIO(base_image))
# Analyze edit prompt and apply appropriate processing
edit_prompt_lower = edit_prompt.lower()
if any(word in edit_prompt_lower for word in ['brighter', 'light', 'lighting']):
image = self._adjust_brightness(image, 1.2)
logger.info("Applied brightness adjustment")
if any(word in edit_prompt_lower for word in ['sharper', 'sharp', 'clear']):
image = self._apply_sharpening(image)
logger.info("Applied sharpening")
if any(word in edit_prompt_lower for word in ['warmer', 'warm', 'color']):
image = self._adjust_color_temperature(image, 'warm')
logger.info("Applied warm color adjustment")
if any(word in edit_prompt_lower for word in ['professional', 'business']):
image = self._apply_professional_enhancements(image)
logger.info("Applied professional enhancements")
# Convert back to bytes
output_buffer = BytesIO()
image.save(output_buffer, format=image.format or "PNG", optimize=True)
edited_data = output_buffer.getvalue()
return {
'success': True,
'image_data': edited_data
}
except Exception as e:
logger.error(f"Error in traditional editing: {str(e)}")
return {
'success': False,
'error': f"Traditional editing failed: {str(e)}"
}
def _apply_linkedin_enhancements(
self,
image: Image.Image,
content_context: Optional[Dict[str, Any]] = None
) -> Image.Image:
"""
Apply LinkedIn-specific image enhancements.
Args:
image: PIL Image object
content_context: LinkedIn content context
Returns:
Enhanced image
"""
try:
# Apply standard LinkedIn optimizations
image = self._adjust_brightness(image, self.enhancement_factors['brightness'])
image = self._adjust_contrast(image, self.enhancement_factors['contrast'])
image = self._apply_sharpening(image)
image = self._adjust_saturation(image, self.enhancement_factors['saturation'])
# Ensure professional appearance
image = self._ensure_professional_appearance(image, content_context)
return image
except Exception as e:
logger.error(f"Error applying LinkedIn enhancements: {str(e)}")
return image
def _apply_professional_enhancements(self, image: Image.Image) -> Image.Image:
"""
Apply professional business aesthetic enhancements.
Args:
image: PIL Image object
Returns:
Enhanced image
"""
try:
# Subtle enhancements for professional appearance
image = self._adjust_brightness(image, 1.05)
image = self._adjust_contrast(image, 1.03)
image = self._apply_sharpening(image)
return image
except Exception as e:
logger.error(f"Error applying professional enhancements: {str(e)}")
return image
def _apply_creative_enhancements(self, image: Image.Image) -> Image.Image:
"""
Apply creative and engaging enhancements.
Args:
image: PIL Image object
Returns:
Enhanced image
"""
try:
# More pronounced enhancements for creative appeal
image = self._adjust_brightness(image, 1.1)
image = self._adjust_contrast(image, 1.08)
image = self._adjust_saturation(image, 1.1)
image = self._apply_sharpening(image)
return image
except Exception as e:
logger.error(f"Error applying creative enhancements: {str(e)}")
return image
def _adjust_brightness(self, image: Image.Image, factor: float) -> Image.Image:
"""Adjust image brightness."""
try:
enhancer = ImageEnhance.Brightness(image)
return enhancer.enhance(factor)
except Exception as e:
logger.error(f"Error adjusting brightness: {str(e)}")
return image
def _adjust_contrast(self, image: Image.Image, factor: float) -> Image.Image:
"""Adjust image contrast."""
try:
enhancer = ImageEnhance.Contrast(image)
return enhancer.enhance(factor)
except Exception as e:
logger.error(f"Error adjusting contrast: {str(e)}")
return image
def _adjust_saturation(self, image: Image.Image, factor: float) -> Image.Image:
"""Adjust image saturation."""
try:
enhancer = ImageEnhance.Color(image)
return enhancer.enhance(factor)
except Exception as e:
logger.error(f"Error adjusting saturation: {str(e)}")
return image
def _apply_sharpening(self, image: Image.Image) -> Image.Image:
"""Apply image sharpening."""
try:
# Apply unsharp mask for professional sharpening
return image.filter(ImageFilter.UnsharpMask(radius=1, percent=150, threshold=3))
except Exception as e:
logger.error(f"Error applying sharpening: {str(e)}")
return image
def _adjust_color_temperature(self, image: Image.Image, temperature: str) -> Image.Image:
"""Adjust image color temperature."""
try:
if temperature == 'warm':
# Apply warm color adjustment
enhancer = ImageEnhance.Color(image)
image = enhancer.enhance(1.1)
# Slight red tint for warmth
# This is a simplified approach - more sophisticated color grading could be implemented
return image
else:
return image
except Exception as e:
logger.error(f"Error adjusting color temperature: {str(e)}")
return image
def _ensure_professional_appearance(
self,
image: Image.Image,
content_context: Optional[Dict[str, Any]] = None
) -> Image.Image:
"""
Ensure image meets professional LinkedIn standards.
Args:
image: PIL Image object
content_context: LinkedIn content context
Returns:
Professionally optimized image
"""
try:
# Ensure minimum quality standards
if image.mode in ('RGBA', 'LA', 'P'):
# Convert to RGB for better compatibility
background = Image.new('RGB', image.size, (255, 255, 255))
if image.mode == 'P':
image = image.convert('RGBA')
background.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None)
image = background
# Ensure minimum resolution for LinkedIn
min_resolution = (1024, 1024)
if image.size[0] < min_resolution[0] or image.size[1] < min_resolution[1]:
# Resize to minimum resolution while maintaining aspect ratio
ratio = max(min_resolution[0] / image.size[0], min_resolution[1] / image.size[1])
new_size = (int(image.size[0] * ratio), int(image.size[1] * ratio))
image = image.resize(new_size, Image.Resampling.LANCZOS)
logger.info(f"Resized image to {new_size} for LinkedIn professional standards")
return image
except Exception as e:
logger.error(f"Error ensuring professional appearance: {str(e)}")
return image
async def get_editing_suggestions(
self,
image_data: bytes,
content_context: Dict[str, Any]
) -> List[Dict[str, Any]]:
"""
Get AI-powered editing suggestions for LinkedIn image.
Args:
image_data: Image data in bytes
content_context: LinkedIn content context
Returns:
List of editing suggestions
"""
try:
# Analyze image and provide contextual suggestions
suggestions = []
# Professional enhancement suggestions
suggestions.append({
'id': 'professional_enhancement',
'title': 'Professional Enhancement',
'description': 'Apply subtle professional enhancements for business appeal',
'prompt': 'Enhance this image with professional business aesthetics',
'priority': 'high'
})
# Mobile optimization suggestions
suggestions.append({
'id': 'mobile_optimization',
'title': 'Mobile Optimization',
'description': 'Optimize for LinkedIn mobile feed viewing',
'prompt': 'Optimize this image for mobile LinkedIn viewing',
'priority': 'medium'
})
# Industry-specific suggestions
industry = content_context.get('industry', 'business')
suggestions.append({
'id': 'industry_optimization',
'title': f'{industry.title()} Industry Optimization',
'description': f'Apply {industry} industry-specific visual enhancements',
'prompt': f'Enhance this image with {industry} industry aesthetics',
'priority': 'medium'
})
# Engagement optimization suggestions
suggestions.append({
'id': 'engagement_optimization',
'title': 'Engagement Optimization',
'description': 'Make this image more engaging for LinkedIn audience',
'prompt': 'Make this image more engaging and shareable for LinkedIn',
'priority': 'low'
})
return suggestions
except Exception as e:
logger.error(f"Error getting editing suggestions: {str(e)}")
return []

View File

@@ -0,0 +1,480 @@
"""
LinkedIn Image Generator Service
This service generates LinkedIn-optimized images using Google's Gemini API.
It provides professional, business-appropriate imagery for LinkedIn content.
"""
import os
import asyncio
import logging
from datetime import datetime
from typing import Dict, Any, Optional, Tuple
from pathlib import Path
from PIL import Image
from io import BytesIO
# Import existing infrastructure
from ...api_key_manager import APIKeyManager
from ...llm_providers.text_to_image_generation.gen_gemini_images import generate_gemini_image
# Set up logging
logger = logging.getLogger(__name__)
class LinkedInImageGenerator:
"""
Handles LinkedIn-optimized image generation using Gemini API.
This service integrates with the existing Gemini provider infrastructure
and provides LinkedIn-specific image optimization, quality assurance,
and professional business aesthetics.
"""
def __init__(self, api_key_manager: Optional[APIKeyManager] = None):
"""
Initialize the LinkedIn Image Generator.
Args:
api_key_manager: API key manager for Gemini authentication
"""
self.api_key_manager = api_key_manager or APIKeyManager()
self.model = "gemini-2.5-flash-image-preview"
self.default_aspect_ratio = "1:1" # LinkedIn post optimal ratio
self.max_retries = 3
# LinkedIn-specific image requirements
self.min_resolution = (1024, 1024)
self.max_file_size_mb = 5
self.supported_formats = ["PNG", "JPEG"]
logger.info("LinkedIn Image Generator initialized")
async def generate_image(
self,
prompt: str,
content_context: Dict[str, Any],
aspect_ratio: str = "1:1",
style_preference: str = "professional"
) -> Dict[str, Any]:
"""
Generate LinkedIn-optimized image using Gemini API.
Args:
prompt: User's image generation prompt
content_context: LinkedIn content context (topic, industry, content_type)
aspect_ratio: Image aspect ratio (1:1, 16:9, 4:3)
style_preference: Style preference (professional, creative, industry-specific)
Returns:
Dict containing generation result, image data, and metadata
"""
try:
start_time = datetime.now()
logger.info(f"Starting LinkedIn image generation for topic: {content_context.get('topic', 'Unknown')}")
# Enhance prompt with LinkedIn-specific context
enhanced_prompt = self._enhance_prompt_for_linkedin(
prompt, content_context, style_preference, aspect_ratio
)
# Generate image using existing Gemini infrastructure
generation_result = await self._generate_with_gemini(enhanced_prompt, aspect_ratio)
if not generation_result.get('success'):
return {
'success': False,
'error': generation_result.get('error', 'Image generation failed'),
'generation_time': (datetime.now() - start_time).total_seconds()
}
# Process and validate generated image
processed_image = await self._process_generated_image(
generation_result['image_data'],
content_context,
aspect_ratio
)
generation_time = (datetime.now() - start_time).total_seconds()
return {
'success': True,
'image_data': processed_image['image_data'],
'image_url': processed_image.get('image_url'),
'metadata': {
'prompt_used': enhanced_prompt,
'original_prompt': prompt,
'style_preference': style_preference,
'aspect_ratio': aspect_ratio,
'content_context': content_context,
'generation_time': generation_time,
'model_used': self.model,
'image_format': processed_image['format'],
'image_size': processed_image['size'],
'resolution': processed_image['resolution']
},
'linkedin_optimization': {
'mobile_optimized': True,
'professional_aesthetic': True,
'brand_compliant': True,
'engagement_optimized': True
}
}
except Exception as e:
logger.error(f"Error in LinkedIn image generation: {str(e)}")
return {
'success': False,
'error': f"Image generation failed: {str(e)}",
'generation_time': (datetime.now() - start_time).total_seconds() if 'start_time' in locals() else 0
}
async def edit_image(
self,
base_image: bytes,
edit_prompt: str,
content_context: Dict[str, Any]
) -> Dict[str, Any]:
"""
Edit existing image using Gemini's conversational editing capabilities.
Args:
base_image: Base image data in bytes
edit_prompt: Description of desired edits
content_context: LinkedIn content context for optimization
Returns:
Dict containing edited image result and metadata
"""
try:
start_time = datetime.now()
logger.info(f"Starting LinkedIn image editing with prompt: {edit_prompt[:100]}...")
# Enhance edit prompt for LinkedIn optimization
enhanced_edit_prompt = self._enhance_edit_prompt_for_linkedin(
edit_prompt, content_context
)
# Use Gemini's image editing capabilities
# Note: This will be implemented when Gemini's image editing is fully available
# For now, we'll return a placeholder implementation
return {
'success': False,
'error': 'Image editing not yet implemented - coming in next Gemini API update',
'generation_time': (datetime.now() - start_time).total_seconds()
}
except Exception as e:
logger.error(f"Error in LinkedIn image editing: {str(e)}")
return {
'success': False,
'error': f"Image editing failed: {str(e)}",
'generation_time': (datetime.now() - start_time).total_seconds() if 'start_time' in locals() else 0
}
def _enhance_prompt_for_linkedin(
self,
prompt: str,
content_context: Dict[str, Any],
style_preference: str,
aspect_ratio: str
) -> str:
"""
Enhance user prompt with LinkedIn-specific context and best practices.
Args:
prompt: Original user prompt
content_context: LinkedIn content context
style_preference: Preferred visual style
aspect_ratio: Image aspect ratio
Returns:
Enhanced prompt optimized for LinkedIn
"""
topic = content_context.get('topic', 'business')
industry = content_context.get('industry', 'business')
content_type = content_context.get('content_type', 'post')
# Base LinkedIn optimization
linkedin_optimizations = [
f"Create a professional LinkedIn {content_type} image for {topic}",
f"Industry: {industry}",
f"Professional business aesthetic suitable for LinkedIn audience",
f"Mobile-optimized design for LinkedIn feed viewing",
f"Aspect ratio: {aspect_ratio}",
"High-quality, modern design with clear visual hierarchy",
"Professional color scheme and typography",
"Suitable for business and professional networking"
]
# Style-specific enhancements
if style_preference == "professional":
style_enhancements = [
"Corporate aesthetics with clean lines and geometric shapes",
"Professional color palette (blues, grays, whites)",
"Modern business environment or abstract business concepts",
"Clean, minimalist design approach"
]
elif style_preference == "creative":
style_enhancements = [
"Eye-catching and engaging visual style",
"Vibrant colors while maintaining professional appeal",
"Creative composition that encourages social media engagement",
"Modern design elements with business context"
]
else: # industry-specific
style_enhancements = [
f"Industry-specific visual elements for {industry}",
"Professional yet creative approach",
"Balanced design suitable for business audience",
"Industry-relevant imagery and color schemes"
]
# Combine all enhancements
enhanced_prompt = f"{prompt}\n\n"
enhanced_prompt += "\n".join(linkedin_optimizations)
enhanced_prompt += "\n" + "\n".join(style_enhancements)
logger.info(f"Enhanced prompt for LinkedIn: {enhanced_prompt[:200]}...")
return enhanced_prompt
def _enhance_edit_prompt_for_linkedin(
self,
edit_prompt: str,
content_context: Dict[str, Any]
) -> str:
"""
Enhance edit prompt for LinkedIn optimization.
Args:
edit_prompt: Original edit prompt
content_context: LinkedIn content context
Returns:
Enhanced edit prompt
"""
industry = content_context.get('industry', 'business')
linkedin_edit_enhancements = [
f"Maintain professional business aesthetic for {industry} industry",
"Ensure mobile-optimized composition for LinkedIn feed",
"Keep professional color scheme and typography",
"Maintain brand consistency and visual hierarchy"
]
enhanced_edit_prompt = f"{edit_prompt}\n\n"
enhanced_edit_prompt += "\n".join(linkedin_edit_enhancements)
return enhanced_edit_prompt
async def _generate_with_gemini(self, prompt: str, aspect_ratio: str) -> Dict[str, Any]:
"""
Generate image using existing Gemini infrastructure.
Args:
prompt: Enhanced prompt for image generation
aspect_ratio: Desired aspect ratio
Returns:
Generation result from Gemini
"""
try:
# Use existing Gemini image generation function
# This integrates with the current infrastructure
result = generate_gemini_image(prompt, aspect_ratio=aspect_ratio)
if result and os.path.exists(result):
# Read the generated image
with open(result, 'rb') as f:
image_data = f.read()
return {
'success': True,
'image_data': image_data,
'image_path': result
}
else:
return {
'success': False,
'error': 'Gemini image generation returned no result'
}
except Exception as e:
logger.error(f"Error in Gemini image generation: {str(e)}")
return {
'success': False,
'error': f"Gemini generation failed: {str(e)}"
}
async def _process_generated_image(
self,
image_data: bytes,
content_context: Dict[str, Any],
aspect_ratio: str
) -> Dict[str, Any]:
"""
Process and validate generated image for LinkedIn use.
Args:
image_data: Raw image data
content_context: LinkedIn content context
aspect_ratio: Image aspect ratio
Returns:
Processed image information
"""
try:
# Open image for processing
image = Image.open(BytesIO(image_data))
# Get image information
width, height = image.size
format_name = image.format or "PNG"
# Validate resolution
if width < self.min_resolution[0] or height < self.min_resolution[1]:
logger.warning(f"Generated image resolution {width}x{height} below minimum {self.min_resolution}")
# Validate file size
image_size_mb = len(image_data) / (1024 * 1024)
if image_size_mb > self.max_file_size_mb:
logger.warning(f"Generated image size {image_size_mb:.2f}MB exceeds maximum {self.max_file_size_mb}MB")
# LinkedIn-specific optimizations
optimized_image = self._optimize_for_linkedin(image, content_context)
# Convert back to bytes
output_buffer = BytesIO()
optimized_image.save(output_buffer, format=format_name, optimize=True)
optimized_data = output_buffer.getvalue()
return {
'image_data': optimized_data,
'format': format_name,
'size': len(optimized_data),
'resolution': (width, height),
'aspect_ratio': f"{width}:{height}"
}
except Exception as e:
logger.error(f"Error processing generated image: {str(e)}")
# Return original image data if processing fails
return {
'image_data': image_data,
'format': 'PNG',
'size': len(image_data),
'resolution': (1024, 1024),
'aspect_ratio': aspect_ratio
}
def _optimize_for_linkedin(self, image: Image.Image, content_context: Dict[str, Any]) -> Image.Image:
"""
Optimize image specifically for LinkedIn display.
Args:
image: PIL Image object
content_context: LinkedIn content context
Returns:
Optimized image
"""
try:
# Ensure minimum resolution
width, height = image.size
if width < self.min_resolution[0] or height < self.min_resolution[1]:
# Resize to minimum resolution while maintaining aspect ratio
ratio = max(self.min_resolution[0] / width, self.min_resolution[1] / height)
new_width = int(width * ratio)
new_height = int(height * ratio)
image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
logger.info(f"Resized image to {new_width}x{new_height} for LinkedIn optimization")
# Convert to RGB if necessary (for JPEG compatibility)
if image.mode in ('RGBA', 'LA', 'P'):
# Create white background for transparent images
background = Image.new('RGB', image.size, (255, 255, 255))
if image.mode == 'P':
image = image.convert('RGBA')
background.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None)
image = background
return image
except Exception as e:
logger.error(f"Error optimizing image for LinkedIn: {str(e)}")
return image # Return original if optimization fails
async def validate_image_for_linkedin(self, image_data: bytes) -> Dict[str, Any]:
"""
Validate image for LinkedIn compliance and quality standards.
Args:
image_data: Image data to validate
Returns:
Validation results
"""
try:
image = Image.open(BytesIO(image_data))
width, height = image.size
validation_results = {
'resolution_ok': width >= self.min_resolution[0] and height >= self.min_resolution[1],
'aspect_ratio_suitable': self._is_aspect_ratio_suitable(width, height),
'file_size_ok': len(image_data) <= self.max_file_size_mb * 1024 * 1024,
'format_supported': image.format in self.supported_formats,
'professional_aesthetic': True, # Placeholder for future AI-based validation
'overall_score': 0
}
# Calculate overall score
score = 0
if validation_results['resolution_ok']: score += 25
if validation_results['aspect_ratio_suitable']: score += 25
if validation_results['file_size_ok']: score += 20
if validation_results['format_supported']: score += 20
if validation_results['professional_aesthetic']: score += 10
validation_results['overall_score'] = score
return validation_results
except Exception as e:
logger.error(f"Error validating image: {str(e)}")
return {
'resolution_ok': False,
'aspect_ratio_suitable': False,
'file_size_ok': False,
'format_supported': False,
'professional_aesthetic': False,
'overall_score': 0,
'error': str(e)
}
def _is_aspect_ratio_suitable(self, width: int, height: int) -> bool:
"""
Check if image aspect ratio is suitable for LinkedIn.
Args:
width: Image width
height: Image height
Returns:
True if aspect ratio is suitable for LinkedIn
"""
ratio = width / height
# LinkedIn-optimized aspect ratios
suitable_ratios = [
(0.9, 1.1), # 1:1 (square)
(1.6, 1.8), # 16:9 (landscape)
(0.7, 0.8), # 4:3 (portrait)
(1.2, 1.4), # 5:4 (landscape)
]
for min_ratio, max_ratio in suitable_ratios:
if min_ratio <= ratio <= max_ratio:
return True
return False

View File

@@ -0,0 +1,536 @@
"""
LinkedIn Image Storage Service
This service handles image storage, retrieval, and management for LinkedIn image generation.
It provides secure storage, efficient retrieval, and metadata management for generated images.
"""
import os
import hashlib
import json
from typing import Dict, Any, Optional, List, Tuple
from datetime import datetime, timedelta
from pathlib import Path
from PIL import Image
from io import BytesIO
from loguru import logger
# Import existing infrastructure
from ...api_key_manager import APIKeyManager
class LinkedInImageStorage:
"""
Handles storage and management of LinkedIn generated images.
This service provides secure storage, efficient retrieval, metadata management,
and cleanup functionality for LinkedIn image generation.
"""
def __init__(self, storage_path: Optional[str] = None, api_key_manager: Optional[APIKeyManager] = None):
"""
Initialize the LinkedIn Image Storage service.
Args:
storage_path: Base path for image storage
api_key_manager: API key manager for authentication
"""
self.api_key_manager = api_key_manager or APIKeyManager()
# Set up storage paths
if storage_path:
self.base_storage_path = Path(storage_path)
else:
# Default to project-relative path
self.base_storage_path = Path(__file__).parent.parent.parent.parent / "linkedin_images"
# Create storage directories
self.images_path = self.base_storage_path / "images"
self.metadata_path = self.base_storage_path / "metadata"
self.temp_path = self.base_storage_path / "temp"
# Ensure directories exist
self._create_storage_directories()
# Storage configuration
self.max_storage_size_gb = 10 # Maximum storage size in GB
self.image_retention_days = 30 # Days to keep images
self.max_image_size_mb = 10 # Maximum individual image size in MB
logger.info(f"LinkedIn Image Storage initialized at {self.base_storage_path}")
def _create_storage_directories(self):
"""Create necessary storage directories."""
try:
self.images_path.mkdir(parents=True, exist_ok=True)
self.metadata_path.mkdir(parents=True, exist_ok=True)
self.temp_path.mkdir(parents=True, exist_ok=True)
# Create subdirectories for organization
(self.images_path / "posts").mkdir(exist_ok=True)
(self.images_path / "articles").mkdir(exist_ok=True)
(self.images_path / "carousels").mkdir(exist_ok=True)
(self.images_path / "video_scripts").mkdir(exist_ok=True)
logger.info("Storage directories created successfully")
except Exception as e:
logger.error(f"Error creating storage directories: {str(e)}")
raise
async def store_image(
self,
image_data: bytes,
metadata: Dict[str, Any],
content_type: str = "post"
) -> Dict[str, Any]:
"""
Store generated image with metadata.
Args:
image_data: Image data in bytes
image_metadata: Image metadata and context
content_type: Type of LinkedIn content (post, article, carousel, video_script)
Returns:
Dict containing storage result and image ID
"""
try:
start_time = datetime.now()
# Generate unique image ID
image_id = self._generate_image_id(image_data, metadata)
# Validate image data
validation_result = await self._validate_image_for_storage(image_data)
if not validation_result['valid']:
return {
'success': False,
'error': f"Image validation failed: {validation_result['error']}"
}
# Determine storage path based on content type
storage_path = self._get_storage_path(content_type, image_id)
# Store image file
image_stored = await self._store_image_file(image_data, storage_path)
if not image_stored:
return {
'success': False,
'error': 'Failed to store image file'
}
# Store metadata
metadata_stored = await self._store_metadata(image_id, metadata, storage_path)
if not metadata_stored:
# Clean up image file if metadata storage fails
await self._cleanup_failed_storage(storage_path)
return {
'success': False,
'error': 'Failed to store image metadata'
}
# Update storage statistics
await self._update_storage_stats()
storage_time = (datetime.now() - start_time).total_seconds()
return {
'success': True,
'image_id': image_id,
'storage_path': str(storage_path),
'metadata': {
'stored_at': datetime.now().isoformat(),
'storage_time': storage_time,
'file_size': len(image_data),
'content_type': content_type
}
}
except Exception as e:
logger.error(f"Error storing LinkedIn image: {str(e)}")
return {
'success': False,
'error': f"Image storage failed: {str(e)}"
}
async def retrieve_image(self, image_id: str) -> Dict[str, Any]:
"""
Retrieve stored image by ID.
Args:
image_id: Unique image identifier
Returns:
Dict containing image data and metadata
"""
try:
# Find image file
image_path = await self._find_image_by_id(image_id)
if not image_path:
return {
'success': False,
'error': f'Image not found: {image_id}'
}
# Load metadata
metadata = await self._load_metadata(image_id)
if not metadata:
return {
'success': False,
'error': f'Metadata not found for image: {image_id}'
}
# Read image data
with open(image_path, 'rb') as f:
image_data = f.read()
return {
'success': True,
'image_data': image_data,
'metadata': metadata,
'image_path': str(image_path)
}
except Exception as e:
logger.error(f"Error retrieving LinkedIn image {image_id}: {str(e)}")
return {
'success': False,
'error': f"Image retrieval failed: {str(e)}"
}
async def delete_image(self, image_id: str) -> Dict[str, Any]:
"""
Delete stored image and metadata.
Args:
image_id: Unique image identifier
Returns:
Dict containing deletion result
"""
try:
# Find image file
image_path = await self._find_image_by_id(image_id)
if not image_path:
return {
'success': False,
'error': f'Image not found: {image_id}'
}
# Delete image file
if image_path.exists():
image_path.unlink()
logger.info(f"Deleted image file: {image_path}")
# Delete metadata
metadata_path = self.metadata_path / f"{image_id}.json"
if metadata_path.exists():
metadata_path.unlink()
logger.info(f"Deleted metadata file: {metadata_path}")
# Update storage statistics
await self._update_storage_stats()
return {
'success': True,
'message': f'Image {image_id} deleted successfully'
}
except Exception as e:
logger.error(f"Error deleting LinkedIn image {image_id}: {str(e)}")
return {
'success': False,
'error': f"Image deletion failed: {str(e)}"
}
async def list_images(
self,
content_type: Optional[str] = None,
limit: int = 50,
offset: int = 0
) -> Dict[str, Any]:
"""
List stored images with optional filtering.
Args:
content_type: Filter by content type
limit: Maximum number of images to return
offset: Number of images to skip
Returns:
Dict containing list of images and metadata
"""
try:
images = []
# Scan metadata directory
metadata_files = list(self.metadata_path.glob("*.json"))
for metadata_file in metadata_files[offset:offset + limit]:
try:
with open(metadata_file, 'r') as f:
metadata = json.load(f)
# Apply content type filter
if content_type and metadata.get('content_type') != content_type:
continue
# Check if image file still exists
image_id = metadata_file.stem
image_path = await self._find_image_by_id(image_id)
if image_path and image_path.exists():
# Add file size and last modified info
stat = image_path.stat()
metadata['file_size'] = stat.st_size
metadata['last_modified'] = datetime.fromtimestamp(stat.st_mtime).isoformat()
images.append(metadata)
except Exception as e:
logger.warning(f"Error reading metadata file {metadata_file}: {str(e)}")
continue
return {
'success': True,
'images': images,
'total_count': len(images),
'limit': limit,
'offset': offset
}
except Exception as e:
logger.error(f"Error listing LinkedIn images: {str(e)}")
return {
'success': False,
'error': f"Image listing failed: {str(e)}"
}
async def cleanup_old_images(self, days_old: Optional[int] = None) -> Dict[str, Any]:
"""
Clean up old images based on retention policy.
Args:
days_old: Minimum age in days for cleanup (defaults to retention policy)
Returns:
Dict containing cleanup results
"""
try:
if days_old is None:
days_old = self.image_retention_days
cutoff_date = datetime.now() - timedelta(days=days_old)
deleted_count = 0
errors = []
# Scan metadata directory
metadata_files = list(self.metadata_path.glob("*.json"))
for metadata_file in metadata_files:
try:
with open(metadata_file, 'r') as f:
metadata = json.load(f)
# Check creation date
created_at = metadata.get('stored_at')
if created_at:
created_date = datetime.fromisoformat(created_at)
if created_date < cutoff_date:
# Delete old image
image_id = metadata_file.stem
delete_result = await self.delete_image(image_id)
if delete_result['success']:
deleted_count += 1
else:
errors.append(f"Failed to delete {image_id}: {delete_result['error']}")
except Exception as e:
logger.warning(f"Error processing metadata file {metadata_file}: {str(e)}")
continue
return {
'success': True,
'deleted_count': deleted_count,
'errors': errors,
'cutoff_date': cutoff_date.isoformat()
}
except Exception as e:
logger.error(f"Error cleaning up old LinkedIn images: {str(e)}")
return {
'success': False,
'error': f"Cleanup failed: {str(e)}"
}
async def get_storage_stats(self) -> Dict[str, Any]:
"""
Get storage statistics and usage information.
Returns:
Dict containing storage statistics
"""
try:
total_size = 0
total_files = 0
content_type_counts = {}
# Calculate storage usage
for content_type_dir in self.images_path.iterdir():
if content_type_dir.is_dir():
content_type = content_type_dir.name
content_type_counts[content_type] = 0
for image_file in content_type_dir.glob("*"):
if image_file.is_file():
total_size += image_file.stat().st_size
total_files += 1
content_type_counts[content_type] += 1
# Check storage limits
total_size_gb = total_size / (1024 ** 3)
storage_limit_exceeded = total_size_gb > self.max_storage_size_gb
return {
'success': True,
'total_size_bytes': total_size,
'total_size_gb': round(total_size_gb, 2),
'total_files': total_files,
'content_type_counts': content_type_counts,
'storage_limit_gb': self.max_storage_size_gb,
'storage_limit_exceeded': storage_limit_exceeded,
'retention_days': self.image_retention_days
}
except Exception as e:
logger.error(f"Error getting storage stats: {str(e)}")
return {
'success': False,
'error': f"Failed to get storage stats: {str(e)}"
}
def _generate_image_id(self, image_data: bytes, metadata: Dict[str, Any]) -> str:
"""Generate unique image ID based on content and metadata."""
# Create hash from image data and key metadata
hash_input = f"{image_data[:1000]}{metadata.get('topic', '')}{metadata.get('industry', '')}{datetime.now().isoformat()}"
return hashlib.sha256(hash_input.encode()).hexdigest()[:16]
async def _validate_image_for_storage(self, image_data: bytes) -> Dict[str, Any]:
"""Validate image data before storage."""
try:
# Check file size
if len(image_data) > self.max_image_size_mb * 1024 * 1024:
return {
'valid': False,
'error': f'Image size {len(image_data) / (1024*1024):.2f}MB exceeds maximum {self.max_image_size_mb}MB'
}
# Validate image format
try:
image = Image.open(BytesIO(image_data))
if image.format not in ['PNG', 'JPEG', 'JPG']:
return {
'valid': False,
'error': f'Unsupported image format: {image.format}'
}
except Exception as e:
return {
'valid': False,
'error': f'Invalid image data: {str(e)}'
}
return {'valid': True}
except Exception as e:
return {
'valid': False,
'error': f'Validation error: {str(e)}'
}
def _get_storage_path(self, content_type: str, image_id: str) -> Path:
"""Get storage path for image based on content type."""
# Map content types to directory names
content_type_map = {
'post': 'posts',
'article': 'articles',
'carousel': 'carousels',
'video_script': 'video_scripts'
}
directory = content_type_map.get(content_type, 'posts')
return self.images_path / directory / f"{image_id}.png"
async def _store_image_file(self, image_data: bytes, storage_path: Path) -> bool:
"""Store image file to disk."""
try:
# Ensure directory exists
storage_path.parent.mkdir(parents=True, exist_ok=True)
# Write image data
with open(storage_path, 'wb') as f:
f.write(image_data)
logger.info(f"Stored image file: {storage_path}")
return True
except Exception as e:
logger.error(f"Error storing image file: {str(e)}")
return False
async def _store_metadata(self, image_id: str, metadata: Dict[str, Any], storage_path: Path) -> bool:
"""Store image metadata to JSON file."""
try:
# Add storage metadata
metadata['image_id'] = image_id
metadata['storage_path'] = str(storage_path)
metadata['stored_at'] = datetime.now().isoformat()
# Write metadata file
metadata_path = self.metadata_path / f"{image_id}.json"
with open(metadata_path, 'w') as f:
json.dump(metadata, f, indent=2, default=str)
logger.info(f"Stored metadata: {metadata_path}")
return True
except Exception as e:
logger.error(f"Error storing metadata: {str(e)}")
return False
async def _find_image_by_id(self, image_id: str) -> Optional[Path]:
"""Find image file by ID across all content type directories."""
for content_dir in self.images_path.iterdir():
if content_dir.is_dir():
image_path = content_dir / f"{image_id}.png"
if image_path.exists():
return image_path
return None
async def _load_metadata(self, image_id: str) -> Optional[Dict[str, Any]]:
"""Load metadata for image ID."""
try:
metadata_path = self.metadata_path / f"{image_id}.json"
if metadata_path.exists():
with open(metadata_path, 'r') as f:
return json.load(f)
except Exception as e:
logger.error(f"Error loading metadata for {image_id}: {str(e)}")
return None
async def _cleanup_failed_storage(self, storage_path: Path):
"""Clean up files if storage operation fails."""
try:
if storage_path.exists():
storage_path.unlink()
logger.info(f"Cleaned up failed storage: {storage_path}")
except Exception as e:
logger.error(f"Error cleaning up failed storage: {str(e)}")
async def _update_storage_stats(self):
"""Update storage statistics (placeholder for future implementation)."""
# This could be implemented to track storage usage over time
pass

View File

@@ -0,0 +1,18 @@
"""
LinkedIn Image Prompts Package
This package provides AI-powered image prompt generation for LinkedIn content
using Google's Gemini API. It creates three distinct prompt styles optimized
for professional business image generation.
"""
from .linkedin_prompt_generator import LinkedInPromptGenerator
__all__ = [
'LinkedInPromptGenerator'
]
# Version information
__version__ = "1.0.0"
__author__ = "Alwrity Team"
__description__ = "LinkedIn AI Image Prompt Generation Services"

View File

@@ -0,0 +1,812 @@
"""
LinkedIn Image Prompt Generator Service
This service generates AI-optimized image prompts for LinkedIn content using Gemini's
capabilities. It creates three distinct prompt styles (professional, creative, industry-specific)
following best practices for image generation.
"""
import asyncio
from typing import Dict, Any, List, Optional
from datetime import datetime
from loguru import logger
# Import existing infrastructure
from ...api_key_manager import APIKeyManager
from ...llm_providers.gemini_provider import gemini_text_response
class LinkedInPromptGenerator:
"""
Generates AI-optimized image prompts for LinkedIn content.
This service creates three distinct prompt styles following Gemini API best practices:
1. Professional Style - Corporate aesthetics, clean lines, business colors
2. Creative Style - Engaging visuals, vibrant colors, social media appeal
3. Industry-Specific Style - Tailored to specific business sectors
"""
def __init__(self, api_key_manager: Optional[APIKeyManager] = None):
"""
Initialize the LinkedIn Prompt Generator.
Args:
api_key_manager: API key manager for Gemini authentication
"""
self.api_key_manager = api_key_manager or APIKeyManager()
self.model = "gemini-2.0-flash-exp"
# Prompt generation configuration
self.max_prompt_length = 500
self.style_variations = {
'professional': 'corporate, clean, business, professional',
'creative': 'engaging, vibrant, creative, social media',
'industry_specific': 'industry-tailored, specialized, contextual'
}
logger.info("LinkedIn Prompt Generator initialized")
async def generate_three_prompts(
self,
linkedin_content: Dict[str, Any],
aspect_ratio: str = "1:1"
) -> List[Dict[str, Any]]:
"""
Generate three AI-optimized image prompts for LinkedIn content.
Args:
linkedin_content: LinkedIn content context (topic, industry, content_type, content)
aspect_ratio: Desired image aspect ratio
Returns:
List of three prompt objects with style, prompt, and description
"""
try:
start_time = datetime.now()
logger.info(f"Generating image prompts for LinkedIn content: {linkedin_content.get('topic', 'Unknown')}")
# Generate prompts using Gemini
prompts = await self._generate_prompts_with_gemini(linkedin_content, aspect_ratio)
if not prompts or len(prompts) < 3:
logger.warning("Gemini prompt generation failed, using fallback prompts")
prompts = self._get_fallback_prompts(linkedin_content, aspect_ratio)
# Ensure exactly 3 prompts
prompts = prompts[:3]
# Validate and enhance prompts
enhanced_prompts = []
for i, prompt in enumerate(prompts):
enhanced_prompt = self._enhance_prompt_for_linkedin(
prompt, linkedin_content, aspect_ratio, i
)
enhanced_prompts.append(enhanced_prompt)
generation_time = (datetime.now() - start_time).total_seconds()
logger.info(f"Generated {len(enhanced_prompts)} image prompts in {generation_time:.2f}s")
return enhanced_prompts
except Exception as e:
logger.error(f"Error generating LinkedIn image prompts: {str(e)}")
return self._get_fallback_prompts(linkedin_content, aspect_ratio)
async def _generate_prompts_with_gemini(
self,
linkedin_content: Dict[str, Any],
aspect_ratio: str
) -> List[Dict[str, Any]]:
"""
Generate image prompts using Gemini AI.
Args:
linkedin_content: LinkedIn content context
aspect_ratio: Image aspect ratio
Returns:
List of generated prompts
"""
try:
# Build the prompt for Gemini
gemini_prompt = self._build_gemini_prompt(linkedin_content, aspect_ratio)
# Generate response using Gemini
response = gemini_text_response(
prompt=gemini_prompt,
temperature=0.7,
top_p=0.8,
n=1,
max_tokens=1000,
system_prompt="You are an expert AI image prompt engineer specializing in LinkedIn content optimization."
)
if not response:
logger.warning("No response from Gemini prompt generation")
return []
# Parse Gemini response into structured prompts
prompts = self._parse_gemini_response(response, linkedin_content)
return prompts
except Exception as e:
logger.error(f"Error in Gemini prompt generation: {str(e)}")
return []
def _build_gemini_prompt(
self,
linkedin_content: Dict[str, Any],
aspect_ratio: str
) -> str:
"""
Build comprehensive prompt for Gemini to generate image prompts.
Args:
linkedin_content: LinkedIn content context
aspect_ratio: Image aspect ratio
Returns:
Formatted prompt for Gemini
"""
topic = linkedin_content.get('topic', 'business')
industry = linkedin_content.get('industry', 'business')
content_type = linkedin_content.get('content_type', 'post')
content = linkedin_content.get('content', '')
# Extract key content elements for better context
content_analysis = self._analyze_content_for_image_context(content, content_type)
prompt = f"""
As an expert AI image prompt engineer specializing in LinkedIn content, generate 3 distinct image generation prompts for the following LinkedIn {content_type}:
TOPIC: {topic}
INDUSTRY: {industry}
CONTENT TYPE: {content_type}
ASPECT RATIO: {aspect_ratio}
GENERATED CONTENT:
{content}
CONTENT ANALYSIS:
- Key Themes: {content_analysis['key_themes']}
- Tone: {content_analysis['tone']}
- Visual Elements: {content_analysis['visual_elements']}
- Target Audience: {content_analysis['target_audience']}
- Content Purpose: {content_analysis['content_purpose']}
Generate exactly 3 image prompts that directly relate to and enhance the generated content above:
1. PROFESSIONAL STYLE:
- Corporate aesthetics with clean lines and geometric shapes
- Professional color palette (blues, grays, whites)
- Modern business environment or abstract business concepts
- Clean, minimalist design approach
- Suitable for B2B and professional networking
- MUST directly relate to the specific content themes and industry context above
2. CREATIVE STYLE:
- Eye-catching and engaging visual style
- Vibrant colors while maintaining professional appeal
- Creative composition that encourages social media engagement
- Modern design elements with business context
- Optimized for LinkedIn feed visibility
- MUST visually represent the key themes and messages from the content above
3. INDUSTRY-SPECIFIC STYLE:
- Tailored specifically to the {industry} industry
- Industry-relevant imagery, colors, and visual elements
- Professional yet creative approach
- Balanced design suitable for business audience
- Industry-specific symbolism and aesthetics
- MUST incorporate visual elements that directly support the content's industry context
Each prompt should:
- Be specific and detailed (50-100 words)
- Include visual composition guidance
- Specify color schemes and lighting
- Mention LinkedIn optimization
- Follow image generation best practices
- Be suitable for the {aspect_ratio} aspect ratio
- DIRECTLY reference and visualize the key themes, messages, and context from the generated content above
- Create images that would naturally accompany and enhance the specific LinkedIn content provided
Return the prompts in this exact JSON format:
[
{{
"style": "Professional",
"prompt": "Detailed prompt description that directly relates to the content above...",
"description": "Brief description of the visual style and how it relates to the content"
}},
{{
"style": "Creative",
"prompt": "Detailed prompt description that directly relates to the content above...",
"description": "Brief description of the visual style and how it relates to the content"
}},
{{
"style": "Industry-Specific",
"prompt": "Detailed prompt description that directly relates to the content above...",
"description": "Brief description of the visual style and how it relates to the content"
}}
]
Focus on creating prompts that will generate high-quality, LinkedIn-optimized images that directly enhance and complement the specific content provided above.
"""
return prompt.strip()
def _analyze_content_for_image_context(self, content: str, content_type: str) -> Dict[str, Any]:
"""
Analyze the generated LinkedIn content to extract key elements for image context.
Args:
content: The generated LinkedIn content
content_type: Type of content (post, article, carousel, etc.)
Returns:
Dictionary containing content analysis for image generation
"""
try:
# Basic content analysis
content_lower = content.lower()
word_count = len(content.split())
# Extract key themes based on content analysis
key_themes = self._extract_key_themes(content_lower, content_type)
# Determine tone based on content analysis
tone = self._determine_content_tone(content_lower)
# Identify visual elements that could be represented
visual_elements = self._identify_visual_elements(content_lower, content_type)
# Determine target audience
target_audience = self._determine_target_audience(content_lower, content_type)
# Determine content purpose
content_purpose = self._determine_content_purpose(content_lower, content_type)
return {
'key_themes': ', '.join(key_themes),
'tone': tone,
'visual_elements': ', '.join(visual_elements),
'target_audience': target_audience,
'content_purpose': content_purpose,
'word_count': word_count,
'content_type': content_type
}
except Exception as e:
logger.error(f"Error analyzing content for image context: {str(e)}")
return {
'key_themes': 'business, professional',
'tone': 'professional',
'visual_elements': 'business concepts',
'target_audience': 'professionals',
'content_purpose': 'informational',
'word_count': len(content.split()) if content else 0,
'content_type': content_type
}
def _extract_key_themes(self, content_lower: str, content_type: str) -> List[str]:
"""Extract key themes from the content for image generation context."""
themes = []
# Industry and business themes
if any(word in content_lower for word in ['ai', 'artificial intelligence', 'machine learning']):
themes.append('AI & Technology')
if any(word in content_lower for word in ['marketing', 'branding', 'advertising']):
themes.append('Marketing & Branding')
if any(word in content_lower for word in ['leadership', 'management', 'strategy']):
themes.append('Leadership & Strategy')
if any(word in content_lower for word in ['innovation', 'growth', 'transformation']):
themes.append('Innovation & Growth')
if any(word in content_lower for word in ['data', 'analytics', 'insights']):
themes.append('Data & Analytics')
if any(word in content_lower for word in ['customer', 'user experience', 'engagement']):
themes.append('Customer Experience')
if any(word in content_lower for word in ['team', 'collaboration', 'workplace']):
themes.append('Team & Collaboration')
if any(word in content_lower for word in ['sustainability', 'environmental', 'green']):
themes.append('Sustainability')
if any(word in content_lower for word in ['finance', 'investment', 'economy']):
themes.append('Finance & Economy')
if any(word in content_lower for word in ['healthcare', 'medical', 'wellness']):
themes.append('Healthcare & Wellness')
# Content type specific themes
if content_type == 'post':
if any(word in content_lower for word in ['tip', 'advice', 'insight']):
themes.append('Tips & Advice')
if any(word in content_lower for word in ['story', 'experience', 'journey']):
themes.append('Personal Story')
if any(word in content_lower for word in ['trend', 'future', 'prediction']):
themes.append('Trends & Future')
elif content_type == 'article':
if any(word in content_lower for word in ['research', 'study', 'analysis']):
themes.append('Research & Analysis')
if any(word in content_lower for word in ['case study', 'example', 'success']):
themes.append('Case Studies')
if any(word in content_lower for word in ['guide', 'tutorial', 'how-to']):
themes.append('Educational Content')
elif content_type == 'carousel':
if any(word in content_lower for word in ['steps', 'process', 'framework']):
themes.append('Process & Framework')
if any(word in content_lower for word in ['comparison', 'vs', 'difference']):
themes.append('Comparison & Analysis')
if any(word in content_lower for word in ['checklist', 'tips', 'best practices']):
themes.append('Checklists & Best Practices')
# Default theme if none identified
if not themes:
themes.append('Business & Professional')
return themes[:3] # Limit to top 3 themes
def _determine_content_tone(self, content_lower: str) -> str:
"""Determine the tone of the content for appropriate image styling."""
if any(word in content_lower for word in ['excited', 'amazing', 'incredible', 'revolutionary']):
return 'Enthusiastic & Dynamic'
elif any(word in content_lower for word in ['challenge', 'problem', 'issue', 'difficult']):
return 'Thoughtful & Analytical'
elif any(word in content_lower for word in ['success', 'achievement', 'win', 'victory']):
return 'Celebratory & Positive'
elif any(word in content_lower for word in ['guide', 'tutorial', 'how-to', 'steps']):
return 'Educational & Helpful'
elif any(word in content_lower for word in ['trend', 'future', 'prediction', 'forecast']):
return 'Forward-looking & Innovative'
else:
return 'Professional & Informative'
def _identify_visual_elements(self, content_lower: str, content_type: str) -> List[str]:
"""Identify visual elements that could be represented in images."""
visual_elements = []
# Technology and digital elements
if any(word in content_lower for word in ['ai', 'robot', 'computer', 'digital']):
visual_elements.extend(['Digital interfaces', 'Technology symbols', 'Abstract tech patterns'])
# Business and professional elements
if any(word in content_lower for word in ['business', 'corporate', 'office', 'meeting']):
visual_elements.extend(['Business environments', 'Professional settings', 'Corporate aesthetics'])
# Growth and progress elements
if any(word in content_lower for word in ['growth', 'progress', 'improvement', 'success']):
visual_elements.extend(['Growth charts', 'Progress indicators', 'Success symbols'])
# Data and analytics elements
if any(word in content_lower for word in ['data', 'analytics', 'charts', 'metrics']):
visual_elements.extend(['Data visualizations', 'Charts and graphs', 'Analytics dashboards'])
# Team and collaboration elements
if any(word in content_lower for word in ['team', 'collaboration', 'partnership', 'network']):
visual_elements.extend(['Team dynamics', 'Collaboration symbols', 'Network connections'])
# Industry-specific elements
if 'healthcare' in content_lower:
visual_elements.extend(['Medical symbols', 'Healthcare imagery', 'Wellness elements'])
elif 'finance' in content_lower:
visual_elements.extend(['Financial symbols', 'Money concepts', 'Investment imagery'])
elif 'education' in content_lower:
visual_elements.extend(['Learning symbols', 'Educational elements', 'Knowledge imagery'])
# Default visual elements
if not visual_elements:
visual_elements = ['Professional business concepts', 'Modern design elements', 'Corporate aesthetics']
return visual_elements[:4] # Limit to top 4 elements
def _determine_target_audience(self, content_lower: str, content_type: str) -> str:
"""Determine the target audience for the content."""
if any(word in content_lower for word in ['ceo', 'executive', 'leader', 'manager']):
return 'C-Suite & Executives'
elif any(word in content_lower for word in ['entrepreneur', 'startup', 'founder', 'business owner']):
return 'Entrepreneurs & Business Owners'
elif any(word in content_lower for word in ['marketer', 'sales', 'business development']):
return 'Marketing & Sales Professionals'
elif any(word in content_lower for word in ['developer', 'engineer', 'technical', 'it']):
return 'Technical Professionals'
elif any(word in content_lower for word in ['student', 'learner', 'aspiring', 'career']):
return 'Students & Career Changers'
else:
return 'General Business Professionals'
def _determine_content_purpose(self, content_lower: str, content_type: str) -> str:
"""Determine the primary purpose of the content."""
if any(word in content_lower for word in ['tip', 'advice', 'how-to', 'guide']):
return 'Educational & Instructional'
elif any(word in content_lower for word in ['story', 'experience', 'journey', 'case study']):
return 'Storytelling & Experience Sharing'
elif any(word in content_lower for word in ['trend', 'prediction', 'future', 'insight']):
return 'Trend Analysis & Forecasting'
elif any(word in content_lower for word in ['challenge', 'problem', 'solution', 'strategy']):
return 'Problem Solving & Strategy'
elif any(word in content_lower for word in ['success', 'achievement', 'result', 'outcome']):
return 'Success Showcase & Results'
else:
return 'Informational & Awareness'
def _parse_gemini_response(
self,
response: str,
linkedin_content: Dict[str, Any]
) -> List[Dict[str, Any]]:
"""
Parse Gemini response into structured prompt objects.
Args:
response: Raw response from Gemini
linkedin_content: LinkedIn content context
Returns:
List of parsed prompt objects
"""
try:
# Try to extract JSON from response
import json
import re
# Look for JSON array in the response
json_match = re.search(r'\[.*\]', response, re.DOTALL)
if json_match:
json_str = json_match.group(0)
prompts = json.loads(json_str)
# Validate prompt structure
if isinstance(prompts, list) and len(prompts) >= 3:
return prompts[:3]
# Fallback: parse response manually
return self._parse_response_manually(response, linkedin_content)
except Exception as e:
logger.error(f"Error parsing Gemini response: {str(e)}")
return self._parse_response_manually(response, linkedin_content)
def _parse_response_manually(
self,
response: str,
linkedin_content: Dict[str, Any]
) -> List[Dict[str, Any]]:
"""
Manually parse response if JSON parsing fails.
Args:
response: Raw response from Gemini
linkedin_content: LinkedIn content context
Returns:
List of parsed prompt objects
"""
try:
prompts = []
lines = response.split('\n')
current_style = None
current_prompt = []
current_description = None
for line in lines:
line = line.strip()
if 'professional' in line.lower() and 'style' in line.lower():
if current_style and current_prompt:
prompts.append({
'style': current_style,
'prompt': ' '.join(current_prompt),
'description': current_description or f'{current_style} style for LinkedIn'
})
current_style = 'Professional'
current_prompt = []
current_description = None
elif 'creative' in line.lower() and 'style' in line.lower():
if current_style and current_prompt:
prompts.append({
'style': current_style,
'prompt': ' '.join(current_prompt),
'description': current_description or f'{current_style} style for LinkedIn'
})
current_style = 'Creative'
current_prompt = []
current_description = None
elif 'industry' in line.lower() and 'specific' in line.lower():
if current_style and current_prompt:
prompts.append({
'style': current_style,
'prompt': ' '.join(current_prompt),
'description': current_description or f'{current_style} style for LinkedIn'
})
current_style = 'Industry-Specific'
current_prompt = []
current_description = None
elif line and not line.startswith('-') and current_style:
current_prompt.append(line)
elif line.startswith('description:') and current_style:
current_description = line.replace('description:', '').strip()
# Add the last prompt
if current_style and current_prompt:
prompts.append({
'style': current_style,
'prompt': ' '.join(current_prompt),
'description': current_description or f'{current_style} style for LinkedIn'
})
# Ensure we have exactly 3 prompts
while len(prompts) < 3:
style_name = ['Professional', 'Creative', 'Industry-Specific'][len(prompts)]
prompts.append({
'style': style_name,
'prompt': f"Create a {style_name.lower()} LinkedIn image for {linkedin_content.get('topic', 'business')}",
'description': f'{style_name} style for LinkedIn content'
})
return prompts[:3]
except Exception as e:
logger.error(f"Error in manual response parsing: {str(e)}")
return self._get_fallback_prompts(linkedin_content, "1:1")
def _enhance_prompt_for_linkedin(
self,
prompt: Dict[str, Any],
linkedin_content: Dict[str, Any],
aspect_ratio: str,
prompt_index: int
) -> Dict[str, Any]:
"""
Enhance individual prompt with LinkedIn-specific optimizations.
Args:
prompt: Individual prompt object
linkedin_content: LinkedIn content context
aspect_ratio: Image aspect ratio
prompt_index: Index of the prompt (0-2)
Returns:
Enhanced prompt object
"""
try:
topic = linkedin_content.get('topic', 'business')
industry = linkedin_content.get('industry', 'business')
content_type = linkedin_content.get('content_type', 'post')
# Get the base prompt text
base_prompt = prompt.get('prompt', '')
style = prompt.get('style', 'Professional')
# LinkedIn-specific enhancements based on style
if style == 'Professional':
enhancements = [
f"Professional LinkedIn {content_type} image for {topic}",
"Corporate aesthetics with clean lines and geometric shapes",
"Professional color palette (blues, grays, whites)",
"Modern business environment or abstract business concepts",
f"Aspect ratio: {aspect_ratio}",
"Mobile-optimized for LinkedIn feed viewing",
"High-quality, professional business aesthetic"
]
elif style == 'Creative':
enhancements = [
f"Creative LinkedIn {content_type} image for {topic}",
"Eye-catching and engaging visual style",
"Vibrant colors while maintaining professional appeal",
"Creative composition that encourages social media engagement",
f"Aspect ratio: {aspect_ratio}",
"Optimized for LinkedIn feed visibility and sharing",
"Modern design elements with business context"
]
else: # Industry-Specific
enhancements = [
f"{industry} industry-specific LinkedIn {content_type} image for {topic}",
f"Industry-relevant imagery and colors for {industry}",
"Professional yet creative approach",
"Balanced design suitable for business audience",
f"Aspect ratio: {aspect_ratio}",
f"Industry-specific symbolism and {industry} aesthetics",
"Professional business appeal for LinkedIn"
]
# Combine base prompt with enhancements
enhanced_prompt_text = f"{base_prompt}\n\n"
enhanced_prompt_text += "\n".join(enhancements)
# Ensure prompt length is within limits
if len(enhanced_prompt_text) > self.max_prompt_length:
enhanced_prompt_text = enhanced_prompt_text[:self.max_prompt_length] + "..."
return {
'style': style,
'prompt': enhanced_prompt_text,
'description': prompt.get('description', f'{style} style for LinkedIn'),
'prompt_index': prompt_index,
'enhanced_at': datetime.now().isoformat(),
'linkedin_optimized': True
}
except Exception as e:
logger.error(f"Error enhancing prompt: {str(e)}")
return prompt
def _get_fallback_prompts(
self,
linkedin_content: Dict[str, Any],
aspect_ratio: str
) -> List[Dict[str, Any]]:
"""
Generate fallback prompts if AI generation fails.
Args:
linkedin_content: LinkedIn content context
aspect_ratio: Image aspect ratio
Returns:
List of fallback prompt objects
"""
topic = linkedin_content.get('topic', 'business')
industry = linkedin_content.get('industry', 'business')
content_type = linkedin_content.get('content_type', 'post')
content = linkedin_content.get('content', '')
# Analyze content for better context
content_analysis = self._analyze_content_for_image_context(content, content_type)
# Create context-aware fallback prompts
fallback_prompts = [
{
'style': 'Professional',
'prompt': f"""Create a professional LinkedIn {content_type} image for {topic} in the {industry} industry.
Key Content Themes: {content_analysis['key_themes']}
Content Tone: {content_analysis['tone']}
Visual Elements: {content_analysis['visual_elements']}
Corporate aesthetics with clean lines and geometric shapes
Professional color palette (blues, grays, whites)
Modern business environment or abstract business concepts
Aspect ratio: {aspect_ratio}
Mobile-optimized for LinkedIn feed viewing
High-quality, professional business aesthetic
Directly represents the content themes: {content_analysis['key_themes']}""",
'description': f'Clean, business-appropriate visual for LinkedIn {content_type} about {topic}',
'prompt_index': 0,
'fallback': True,
'content_context': content_analysis
},
{
'style': 'Creative',
'prompt': f"""Generate a creative LinkedIn {content_type} image for {topic} in {industry}.
Key Content Themes: {content_analysis['key_themes']}
Content Purpose: {content_analysis['content_purpose']}
Target Audience: {content_analysis['target_audience']}
Eye-catching and engaging visual style
Vibrant colors while maintaining professional appeal
Creative composition that encourages social media engagement
Aspect ratio: {aspect_ratio}
Optimized for LinkedIn feed visibility and sharing
Modern design elements with business context
Visually represents: {content_analysis['visual_elements']}""",
'description': f'Eye-catching, shareable design for LinkedIn {content_type} about {topic}',
'prompt_index': 1,
'fallback': True,
'content_context': content_analysis
},
{
'style': 'Industry-Specific',
'prompt': f"""Design a {industry} industry-specific LinkedIn {content_type} image for {topic}.
Key Content Themes: {content_analysis['key_themes']}
Content Tone: {content_analysis['tone']}
Visual Elements: {content_analysis['visual_elements']}
Industry-relevant imagery and colors for {industry}
Professional yet creative approach
Balanced design suitable for business audience
Aspect ratio: {aspect_ratio}
Industry-specific symbolism and {industry} aesthetics
Professional business appeal for LinkedIn
Incorporates visual elements: {content_analysis['visual_elements']}""",
'description': f'Industry-tailored professional design for {industry} {content_type} about {topic}',
'prompt_index': 2,
'fallback': True,
'content_context': content_analysis
}
]
logger.info(f"Using context-aware fallback prompts for LinkedIn {content_type} about {topic}")
return fallback_prompts
async def validate_prompt_quality(
self,
prompt: Dict[str, Any]
) -> Dict[str, Any]:
"""
Validate the quality of a generated prompt.
Args:
prompt: Prompt object to validate
Returns:
Validation results
"""
try:
prompt_text = prompt.get('prompt', '')
style = prompt.get('style', '')
# Quality metrics
length_score = min(len(prompt_text) / 100, 1.0) # Optimal length around 100 words
specificity_score = self._calculate_specificity_score(prompt_text)
linkedin_optimization_score = self._calculate_linkedin_optimization_score(prompt_text)
# Overall quality score
overall_score = (length_score + specificity_score + linkedin_optimization_score) / 3
return {
'valid': overall_score >= 0.7,
'overall_score': round(overall_score, 2),
'metrics': {
'length_score': round(length_score, 2),
'specificity_score': round(specificity_score, 2),
'linkedin_optimization_score': round(linkedin_optimization_score, 2)
},
'recommendations': self._get_quality_recommendations(overall_score, prompt_text)
}
except Exception as e:
logger.error(f"Error validating prompt quality: {str(e)}")
return {
'valid': False,
'overall_score': 0.0,
'error': str(e)
}
def _calculate_specificity_score(self, prompt_text: str) -> float:
"""Calculate how specific and detailed the prompt is."""
# Count specific visual elements, colors, styles mentioned
specific_elements = [
'wide-angle', 'close-up', 'low-angle', 'aerial',
'blue', 'gray', 'white', 'red', 'green', 'yellow',
'modern', 'minimalist', 'corporate', 'professional',
'geometric', 'clean lines', 'sharp focus', 'soft lighting'
]
element_count = sum(1 for element in specific_elements if element.lower() in prompt_text.lower())
return min(element_count / 8, 1.0) # Normalize to 0-1
def _calculate_linkedin_optimization_score(self, prompt_text: str) -> float:
"""Calculate how well the prompt is optimized for LinkedIn."""
linkedin_keywords = [
'linkedin', 'professional', 'business', 'corporate',
'mobile', 'feed', 'social media', 'engagement',
'networking', 'professional audience'
]
keyword_count = sum(1 for keyword in linkedin_keywords if keyword.lower() in prompt_text.lower())
return min(keyword_count / 5, 1.0) # Normalize to 0-1
def _get_quality_recommendations(self, score: float, prompt_text: str) -> List[str]:
"""Get recommendations for improving prompt quality."""
recommendations = []
if score < 0.7:
if len(prompt_text) < 100:
recommendations.append("Add more specific visual details and composition guidance")
if 'linkedin' not in prompt_text.lower():
recommendations.append("Include LinkedIn-specific optimization terms")
if 'aspect ratio' not in prompt_text.lower():
recommendations.append("Specify the desired aspect ratio")
if 'professional' not in prompt_text.lower() and 'business' not in prompt_text.lower():
recommendations.append("Include professional business aesthetic guidance")
return recommendations

View File

@@ -54,7 +54,8 @@ class QualityHandler:
citation_coverage=quality_analysis.get('metrics', {}).get('citation_coverage', 0.0),
content_length=quality_analysis.get('content_length', 0),
word_count=quality_analysis.get('word_count', 0),
analysis_timestamp=quality_analysis.get('analysis_timestamp', '')
analysis_timestamp=quality_analysis.get('analysis_timestamp', ''),
recommendations=quality_analysis.get('recommendations', [])
)
except Exception as e:
logger.warning(f"Quality metrics creation failed: {e}")

View File

@@ -178,7 +178,8 @@ class LinkedInService:
research_sources=research_sources
)
else:
content_result = await content_generator.generate_fallback_article_content(request)
logger.error("Grounding not enabled - cannot generate LinkedIn article without AI provider")
raise Exception("Grounding not enabled - cannot generate LinkedIn article without AI provider")
# Step 3-5: Use content generator for processing and response building
return await content_generator.generate_article(
@@ -235,7 +236,8 @@ class LinkedInService:
research_sources=research_sources
)
else:
content_result = await content_generator.generate_fallback_carousel_content(request)
logger.error("Grounding not enabled - cannot generate LinkedIn carousel without AI provider")
raise Exception("Grounding not enabled - cannot generate LinkedIn carousel without AI provider")
# Step 3-5: Use content generator for processing and response building
@@ -280,7 +282,7 @@ class LinkedInService:
success=False,
error=result['error']
)
except Exception as e:
logger.error(f"Error generating LinkedIn carousel: {str(e)}")
return LinkedInCarouselResponse(
@@ -327,7 +329,8 @@ class LinkedInService:
research_sources=research_sources
)
else:
content_result = await content_generator.generate_fallback_video_script_content(request)
logger.error("Grounding not enabled - cannot generate LinkedIn video script without AI provider")
raise Exception("Grounding not enabled - cannot generate LinkedIn video script without AI provider")
# Step 3-5: Use content generator for processing and response building
@@ -410,7 +413,8 @@ class LinkedInService:
research_sources=research_sources
)
else:
response_result = await content_generator.generate_fallback_comment_response(request)
logger.error("Grounding not enabled - cannot generate LinkedIn comment response without AI provider")
raise Exception("Grounding not enabled - cannot generate LinkedIn comment response without AI provider")
# Step 3-5: Use content generator for processing and response building
@@ -423,11 +427,18 @@ class LinkedInService:
)
if result['success']:
return LinkedInCommentResponseResult(
success=True,
# Convert to LinkedInCommentResponseResult
from models.linkedin_models import CommentResponse
comment_response = CommentResponse(
response=result['response'],
alternative_responses=result.get('alternative_responses', []),
tone_analysis=result.get('tone_analysis'),
tone_analysis=result.get('tone_analysis')
)
return LinkedInCommentResponseResult(
success=True,
data=comment_response,
research_sources=result['research_sources'],
generation_metadata=result['generation_metadata'],
grounding_status=result['grounding_status']
)

View File

@@ -111,11 +111,11 @@ class GeminiGroundedProvider:
Enhanced prompt for grounded generation
"""
content_type_instructions = {
"linkedin_post": "Generate a professional LinkedIn post that is factually accurate and cites current sources. Include engaging hashtags and a call-to-action.",
"linkedin_article": "Generate a comprehensive LinkedIn article with proper structure, factual accuracy, and source citations. Include an engaging title and conclusion.",
"linkedin_carousel": "Generate LinkedIn carousel content with multiple slides, each containing factual information with proper source attribution.",
"linkedin_video_script": "Generate a video script with hook, main content, and conclusion. Ensure all claims are factually grounded.",
"linkedin_comment_response": "Generate a professional comment response that adds value to the conversation."
"linkedin_post": "You are an expert LinkedIn content strategist. Generate a highly engaging, professional LinkedIn post that drives meaningful engagement, establishes thought leadership, and includes compelling hooks, actionable insights, and strategic hashtags. Every element should be optimized for maximum engagement and shareability.",
"linkedin_article": "You are a senior content strategist and industry thought leader. Generate a comprehensive, SEO-optimized LinkedIn article with compelling headlines, structured content, data-driven insights, and practical takeaways. Include proper source citations and engagement elements throughout.",
"linkedin_carousel": "You are a visual content strategist specializing in LinkedIn carousels. Generate compelling, story-driven carousel content with clear visual hierarchy, actionable insights per slide, and strategic engagement elements. Each slide should provide immediate value while building anticipation for the next.",
"linkedin_video_script": "You are a video content strategist and LinkedIn engagement expert. Generate a compelling video script optimized for LinkedIn's algorithm with attention-grabbing hooks, strategic timing, and engagement-driven content. Include specific visual and audio recommendations for maximum impact.",
"linkedin_comment_response": "You are a LinkedIn engagement specialist and industry expert. Generate thoughtful, value-adding comment responses that encourage further discussion, demonstrate expertise, and build meaningful professional relationships. Focus on genuine engagement over generic responses."
}
instruction = content_type_instructions.get(content_type, "Generate professional content with factual accuracy.")
@@ -123,15 +123,29 @@ class GeminiGroundedProvider:
grounded_prompt = f"""
{instruction}
IMPORTANT: Use current, factual information from reliable sources. Cite specific sources for any claims, statistics, or recent developments.
CRITICAL REQUIREMENTS FOR LINKEDIN CONTENT:
- Use ONLY current, factual information from reliable sources (2024-2025)
- Cite specific sources for ALL claims, statistics, and recent developments
- Ensure content is optimized for LinkedIn's algorithm and engagement patterns
- Include strategic hashtags and engagement elements throughout
User Request: {prompt}
Requirements:
- Ensure all factual claims are backed by current sources
- Use professional, engaging language appropriate for LinkedIn
- Include relevant industry insights and trends
- Make content shareable and valuable for the target audience
CONTENT QUALITY STANDARDS:
- All factual claims must be backed by current, authoritative sources
- Use professional yet conversational language that encourages engagement
- Include relevant industry insights, trends, and data points
- Make content highly shareable with clear value proposition
- Optimize for LinkedIn's professional audience and engagement metrics
ENGAGEMENT OPTIMIZATION:
- Include thought-provoking questions and calls-to-action
- Use storytelling elements and real-world examples
- Ensure content provides immediate, actionable value
- Optimize for comments, shares, and professional networking
- Include industry-specific terminology and insights
REMEMBER: This content will be displayed on LinkedIn with full source attribution and grounding data. Every claim must be verifiable, and the content should position the author as a thought leader in their industry.
"""
return grounded_prompt.strip()

View File

@@ -3,6 +3,7 @@ import sys
import time
import datetime
import base64
import random
from typing import List, Optional, Tuple
from PIL import Image
from io import BytesIO
@@ -12,8 +13,8 @@ import logging
from ...api_key_manager import APIKeyManager
try:
import google.generativeai as genai
from google.generativeai import types
from google import genai
from google.genai import types
except ImportError:
genai = None
logging.getLogger('gemini_image_generator').warning(
@@ -30,6 +31,24 @@ logging.basicConfig(
)
logger = logging.getLogger('gemini_image_generator')
# Imagen fallback configuration
IMAGEN_FALLBACK_CONFIG = {
'enabled': os.getenv('IMAGEN_FALLBACK_ENABLED', 'true').lower() == 'true', # Master switch for Imagen fallback
'auto_fallback': os.getenv('IMAGEN_AUTO_FALLBACK', 'true').lower() == 'true', # Automatically fall back on Gemini failures
'preferred_model': os.getenv('IMAGEN_MODEL', 'imagen-4.0-generate-001'), # Fast model for quick generation
'fallback_aspect_ratios': {
'1:1': '1:1',
'3:4': '3:4',
'4:3': '4:3',
'9:16': '9:16',
'16:9': '16:9'
},
'max_images': int(os.getenv('IMAGEN_MAX_IMAGES', '1')), # Generate 1 image for LinkedIn posts
}
# Log configuration on startup
logger.info(f"🔄 Imagen fallback configuration: {IMAGEN_FALLBACK_CONFIG}")
# With image generation in Gemini, your imagination is the limit.
# Follow Google AI best practices for detailed prompts and iterative refinement.
@@ -173,13 +192,137 @@ def _ensure_client() -> Optional[object]:
api_key_manager = APIKeyManager()
api_key = api_key_manager.get_api_key("gemini")
if not api_key or genai is None:
if not api_key:
logger.warning("No Gemini API key found")
if genai is None:
logger.warning("Google Generative AI library not available")
return None
try:
return genai.Client(api_key=api_key)
except Exception:
logger.info("Creating Gemini client...")
# Create a client using the correct API pattern
# The API key is passed directly to the Client constructor
client = genai.Client(api_key=api_key)
logger.info("Gemini client created successfully")
return client
except Exception as e:
logger.error(f"Failed to create Gemini client: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
return None
def _generate_imagen_images_base64(prompt: str, aspect_ratio: str = "1:1") -> List[str]:
"""
Generate images using Imagen API as a fallback method.
This function implements the Imagen API following the official documentation:
https://ai.google.dev/gemini-api/docs/imagen
Args:
prompt: Text prompt for image generation
aspect_ratio: Desired aspect ratio (1:1, 3:4, 4:3, 9:16, 16:9)
Returns:
List of base64-encoded PNG images
"""
logger = logging.getLogger('gemini_image_generator')
logger.info("🔄 Falling back to Imagen API for image generation")
try:
# Get API key for Imagen (can use same Gemini API key)
api_key_manager = APIKeyManager()
api_key = api_key_manager.get_api_key("gemini") # Imagen uses same API key
if not api_key:
logger.error("No API key available for Imagen fallback")
return []
# Create Imagen client
client = genai.Client(api_key=api_key)
# Map aspect ratio to Imagen format using configuration
imagen_aspect_ratio = IMAGEN_FALLBACK_CONFIG['fallback_aspect_ratios'].get(aspect_ratio, "1:1")
# Optimize prompt for Imagen (remove Gemini-specific formatting)
imagen_prompt = _optimize_prompt_for_imagen(prompt)
logger.info(f"Generating Imagen images with prompt: {imagen_prompt[:100]}...")
logger.info(f"Using aspect ratio: {imagen_aspect_ratio}")
logger.info(f"Using model: {IMAGEN_FALLBACK_CONFIG['preferred_model']}")
# Generate images using configured Imagen model
# Note: sample_image_size is not supported in current library version
config_params = {
'number_of_images': IMAGEN_FALLBACK_CONFIG['max_images'],
'aspect_ratio': imagen_aspect_ratio,
}
# Add additional configuration options if needed
# config_params['guidance_scale'] = 7.5 # Optional: control image generation quality
# config_params['person_generation'] = 'allow_adult' # Optional: control person generation
response = client.models.generate_images(
model=IMAGEN_FALLBACK_CONFIG['preferred_model'],
prompt=imagen_prompt,
config=types.GenerateImagesConfig(**config_params)
)
# Extract base64 images from response
images_b64: List[str] = []
for generated_image in response.generated_images:
if hasattr(generated_image, 'image') and hasattr(generated_image.image, 'image_bytes'):
# Convert image bytes to base64
image_bytes = generated_image.image.image_bytes
if isinstance(image_bytes, bytes):
images_b64.append(base64.b64encode(image_bytes).decode('utf-8'))
else:
# If already base64 string
images_b64.append(str(image_bytes))
if images_b64:
logger.info(f"✅ Imagen fallback successful! Generated {len(images_b64)} images")
return images_b64
else:
logger.warning("Imagen fallback returned no images")
return []
except Exception as e:
logger.error(f"❌ Imagen fallback failed: {e}")
import traceback
logger.error(f"Imagen error traceback: {traceback.format_exc()}")
return []
def _optimize_prompt_for_imagen(prompt: str) -> str:
"""
Optimize prompt for Imagen API by removing Gemini-specific formatting
and enhancing it with Imagen best practices.
Based on Imagen prompt guide: https://ai.google.dev/gemini-api/docs/imagen
"""
# Remove Gemini-specific formatting
prompt = prompt.replace('\n\nEnhanced prompt:', '')
prompt = prompt.replace('\n\nAspect ratio:', '')
# Clean up extra whitespace
prompt = ' '.join(prompt.split())
# Add Imagen-specific enhancements if not present
if 'professional' in prompt.lower() and 'linkedin' in prompt.lower():
# Enhance for LinkedIn professional content
prompt += ", high quality, professional photography, business appropriate"
if 'digital transformation' in prompt.lower() or 'technology' in prompt.lower():
# Enhance for tech content
prompt += ", modern, innovative, clean design, corporate aesthetic"
# Ensure prompt doesn't exceed Imagen's 480 token limit
if len(prompt) > 400: # Leave some buffer
prompt = prompt[:400] + "..."
return prompt
def generate_gemini_images_base64(
prompt: str,
*,
@@ -190,17 +333,23 @@ def generate_gemini_images_base64(
aspect_ratio: str = "9:16",
max_retries: int = 2,
initial_retry_delay: float = 1.0,
enable_imagen_fallback: bool = True,
) -> List[str]:
"""
Return list of base64 PNG images generated from a prompt.
Primary method: Gemini API for image generation
Fallback method: Imagen API when Gemini fails (quota limits, API errors, etc.)
Implements best practices per Gemini docs: send text prompt, parse inline image parts,
and return base64 data suitable for API responses. No Streamlit, no printing.
Docs: https://ai.google.dev/gemini-api/docs/image-generation
Docs:
- Gemini: https://ai.google.dev/gemini-api/docs/image-generation
- Imagen: https://ai.google.dev/gemini-api/docs/imagen
"""
logger = logging.getLogger('gemini_image_generator')
logger.info("Generating image (base64) with Gemini")
logger.info("Generating image (base64) with Gemini (with Imagen fallback)")
if enhance_prompt and keywords:
pg = AIPromptGenerator()
@@ -215,9 +364,13 @@ def generate_gemini_images_base64(
if aspect_ratio:
prompt = f"{prompt}\n\nAspect ratio: {aspect_ratio}"
# Try Gemini first
client = _ensure_client()
if client is None:
logger.warning("Gemini client not available or API key missing")
if enable_imagen_fallback and IMAGEN_FALLBACK_CONFIG['enabled']:
logger.info("Falling back to Imagen API")
return _generate_imagen_images_base64(prompt, aspect_ratio)
return []
retry = 0
@@ -225,9 +378,10 @@ def generate_gemini_images_base64(
while retry <= max_retries:
try:
response = client.models.generate_content(
model="gemini-2.5-flash-image-preview",
model="gemini-2.0-flash-exp-image-generation",
contents=[prompt],
)
images_b64: List[str] = []
for part in response.candidates[0].content.parts:
if getattr(part, 'inline_data', None) is not None:
@@ -239,16 +393,47 @@ def generate_gemini_images_base64(
else:
# Some SDKs may already present base64 str
images_b64.append(str(raw))
return images_b64
if images_b64:
logger.info(f"✅ Gemini generated {len(images_b64)} images successfully")
return images_b64
else:
logger.warning("Gemini returned no images, falling back to Imagen")
if enable_imagen_fallback and IMAGEN_FALLBACK_CONFIG['enabled']:
return _generate_imagen_images_base64(prompt, aspect_ratio)
return []
except Exception as e:
msg = str(e)
logger.warning(f"Gemini image gen error: {msg}")
# Check if this is a quota/API error that warrants fallback
if any(error_type in msg.lower() for error_type in [
'quota', 'resource_exhausted', 'rate_limit', 'billing', 'api_key', '403', '429'
]):
logger.info("Gemini quota/API error detected, falling back to Imagen")
if enable_imagen_fallback and IMAGEN_FALLBACK_CONFIG['enabled']:
return _generate_imagen_images_base64(prompt, aspect_ratio)
return []
# For other errors, retry if possible
if "503" in msg and retry < max_retries:
time.sleep(delay)
delay *= 2
retry += 1
continue
# Final fallback for any other errors
if enable_imagen_fallback and IMAGEN_FALLBACK_CONFIG['enabled']:
logger.info("Final fallback to Imagen due to Gemini error")
return _generate_imagen_images_base64(prompt, aspect_ratio)
return []
# If all retries exhausted, fall back to Imagen
if enable_imagen_fallback and IMAGEN_FALLBACK_CONFIG['enabled']:
logger.info("All Gemini retries exhausted, falling back to Imagen")
return _generate_imagen_images_base64(prompt, aspect_ratio)
return []
def generate_gemini_image(
@@ -260,9 +445,12 @@ def generate_gemini_image(
max_retries=2,
initial_retry_delay=1.0,
aspect_ratio="9:16",
enable_imagen_fallback=True,
):
"""
Backward-compatible wrapper that generates a single image file on disk and returns path.
Now includes Imagen fallback for improved reliability.
Prefer generate_gemini_images_base64 in new code paths.
"""
logger = logging.getLogger('gemini_image_generator')
@@ -275,20 +463,31 @@ def generate_gemini_image(
aspect_ratio=aspect_ratio,
max_retries=max_retries,
initial_retry_delay=initial_retry_delay,
enable_imagen_fallback=enable_imagen_fallback,
)
if not images:
return None
# Persist first image to file for legacy callers
img_b64 = images[0]
img_bytes = base64.b64decode(img_b64)
img = Image.open(BytesIO(img_bytes))
out_name = f'gemini-native-image-{datetime.datetime.now().strftime("%Y%m%d-%H%M%S")}.png'
# Update filename to indicate which API was used
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
if 'imagen' in prompt.lower() or 'fallback' in prompt.lower():
out_name = f'imagen-fallback-image-{timestamp}.png'
else:
out_name = f'gemini-native-image-{timestamp}.png'
try:
img.save(out_name)
# Also call save_generated_image to reuse existing pipeline
save_generated_image({"artifacts": [{"base64": img_b64}]})
logger.info(f"✅ Image saved successfully: {out_name}")
return out_name
except Exception:
except Exception as e:
logger.error(f"❌ Failed to save image: {e}")
return None

View File

@@ -0,0 +1,228 @@
#!/usr/bin/env python3
"""
Test Script for Enhanced LinkedIn Prompt Generation
This script demonstrates how the enhanced LinkedIn prompt generator analyzes
generated content and creates context-aware image prompts.
"""
import asyncio
import sys
import os
from pathlib import Path
# Add the backend directory to the Python path
backend_path = Path(__file__).parent
sys.path.insert(0, str(backend_path))
from loguru import logger
# Configure logging
logger.remove()
logger.add(sys.stdout, colorize=True, format="<level>{level}</level>| {message}")
async def test_enhanced_prompt_generation():
"""Test the enhanced LinkedIn prompt generation with content analysis."""
logger.info("🧪 Testing Enhanced LinkedIn Prompt Generation")
logger.info("=" * 70)
try:
# Import the enhanced prompt generator
from services.linkedin.image_prompts import LinkedInPromptGenerator
# Initialize the service
prompt_generator = LinkedInPromptGenerator()
logger.success("✅ LinkedIn Prompt Generator initialized successfully")
# Test cases with different types of LinkedIn content
test_cases = [
{
'name': 'AI Marketing Post',
'content': {
'topic': 'AI in Marketing',
'industry': 'Technology',
'content_type': 'post',
'content': """🚀 Exciting news! Artificial Intelligence is revolutionizing how we approach marketing strategies.
Here are 3 game-changing ways AI is transforming the industry:
1⃣ **Predictive Analytics**: AI algorithms can now predict customer behavior with 95% accuracy, allowing marketers to create hyper-personalized campaigns.
2⃣ **Content Optimization**: Machine learning models analyze engagement patterns to optimize content timing, format, and messaging for maximum impact.
3⃣ **Automated Personalization**: AI-powered tools automatically adjust marketing messages based on individual user preferences and behavior.
The future of marketing is here, and it's powered by AI! 🎯
What's your experience with AI in marketing? Share your thoughts below! 👇
#AIMarketing #DigitalTransformation #MarketingInnovation #TechTrends #FutureOfMarketing"""
}
},
{
'name': 'Leadership Article',
'content': {
'topic': 'Building High-Performance Teams',
'industry': 'Business',
'content_type': 'article',
'content': """Building High-Performance Teams: A Comprehensive Guide
In today's competitive business landscape, the ability to build and lead high-performance teams is not just a skill—it's a strategic imperative. After 15 years of leading teams across various industries, I've identified the key principles that consistently drive exceptional results.
**The Foundation: Clear Vision and Purpose**
Every high-performance team starts with a crystal-clear understanding of their mission. Team members need to know not just what they're doing, but why it matters. This creates intrinsic motivation that external rewards simply cannot match.
**Communication: The Lifeblood of Success**
Effective communication in high-performance teams goes beyond regular meetings. It involves creating an environment where feedback flows freely, ideas are shared without fear, and every voice is heard and valued.
**Trust and Psychological Safety**
High-performance teams operate in environments where team members feel safe to take risks, make mistakes, and learn from failures. This psychological safety is the bedrock of innovation and continuous improvement.
**Continuous Learning and Adaptation**
The best teams never rest on their laurels. They continuously seek new knowledge, adapt to changing circumstances, and evolve their approaches based on results and feedback.
**Results and Accountability**
While process matters, high-performance teams are ultimately measured by their results. Clear metrics, regular check-ins, and a culture of accountability ensure that the team stays focused on delivering value.
Building high-performance teams is both an art and a science. It requires patience, persistence, and a genuine commitment to developing people. The investment pays dividends not just in results, but in the satisfaction of seeing individuals grow and teams achieve what once seemed impossible.
What strategies have you found most effective in building high-performance teams? Share your insights in the comments below."""
}
},
{
'name': 'Data Analytics Carousel',
'content': {
'topic': 'Data-Driven Decision Making',
'industry': 'Finance',
'content_type': 'carousel',
'content': """📊 Data-Driven Decision Making: Your Competitive Advantage
Slide 1: The Power of Data
• 73% of companies using data-driven decision making report improved performance
• Data-driven organizations are 23x more likely to acquire customers
• 58% of executives say data analytics has improved their decision-making process
Slide 2: Key Metrics to Track
• Customer Acquisition Cost (CAC)
• Customer Lifetime Value (CLV)
• Conversion Rates
• Churn Rate
• Revenue Growth
Slide 3: Implementation Steps
1. Define clear objectives
2. Identify relevant data sources
3. Establish data quality standards
4. Build analytical capabilities
5. Create feedback loops
Slide 4: Common Pitfalls
• Analysis paralysis
• Ignoring qualitative insights
• Not validating assumptions
• Over-relying on historical data
• Poor data visualization
Slide 5: Success Stories
• Netflix: 75% of viewing decisions influenced by data
• Amazon: Dynamic pricing increases revenue by 25%
• Spotify: Personalized recommendations drive 40% of listening time
Slide 6: Getting Started
• Start small with key metrics
• Invest in data literacy training
• Use visualization tools
• Establish regular review cycles
• Celebrate data-driven wins
Ready to transform your decision-making process? Let's discuss your data strategy! 💬
#DataDriven #Analytics #BusinessIntelligence #DecisionMaking #Finance #Strategy"""
}
}
]
# Test each case
for i, test_case in enumerate(test_cases, 1):
logger.info(f"\n📝 Test Case {i}: {test_case['name']}")
logger.info("-" * 50)
# Generate prompts using the enhanced generator
prompts = await prompt_generator.generate_three_prompts(
test_case['content'],
aspect_ratio="1:1"
)
if prompts and len(prompts) >= 3:
logger.success(f"✅ Generated {len(prompts)} context-aware prompts")
# Display each prompt
for j, prompt in enumerate(prompts, 1):
logger.info(f"\n🎨 Prompt {j}: {prompt['style']}")
logger.info(f" Description: {prompt['description']}")
logger.info(f" Content Context: {prompt.get('content_context', 'N/A')}")
# Show a preview of the prompt
prompt_text = prompt['prompt']
if len(prompt_text) > 200:
prompt_text = prompt_text[:200] + "..."
logger.info(f" Prompt Preview: {prompt_text}")
# Validate prompt quality
quality_result = await prompt_generator.validate_prompt_quality(prompt)
if quality_result.get('valid'):
logger.success(f" ✅ Quality Score: {quality_result['overall_score']}/100")
else:
logger.warning(f" ⚠️ Quality Score: {quality_result.get('overall_score', 'N/A')}/100")
else:
logger.error(f"❌ Failed to generate prompts for {test_case['name']}")
# Test content analysis functionality directly
logger.info(f"\n🔍 Testing Content Analysis Functionality")
logger.info("-" * 50)
test_content = test_cases[0]['content']['content']
content_analysis = prompt_generator._analyze_content_for_image_context(
test_content,
test_cases[0]['content']['content_type']
)
logger.info("Content Analysis Results:")
for key, value in content_analysis.items():
logger.info(f" {key}: {value}")
logger.info("=" * 70)
logger.success("🎉 Enhanced LinkedIn Prompt Generation Test Completed Successfully!")
return True
except ImportError as e:
logger.error(f"❌ Import Error: {e}")
return False
except Exception as e:
logger.error(f"❌ Test Failed: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
return False
async def main():
"""Main test function."""
logger.info("🚀 Starting Enhanced LinkedIn Prompt Generation Tests")
success = await test_enhanced_prompt_generation()
if success:
logger.success("✅ All tests passed! The enhanced prompt generation is working correctly.")
sys.exit(0)
else:
logger.error("❌ Some tests failed. Please check the errors above.")
sys.exit(1)
if __name__ == "__main__":
# Run the async test
asyncio.run(main())

95
backend/test_image_api.py Normal file
View File

@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""
Test script for LinkedIn Image Generation API endpoints
"""
import asyncio
import aiohttp
import json
async def test_image_generation_api():
"""Test the LinkedIn image generation API endpoints"""
base_url = "http://localhost:8000"
print("🧪 Testing LinkedIn Image Generation API...")
print("=" * 50)
# Test 1: Health Check
print("\n1⃣ Testing Health Check...")
async with aiohttp.ClientSession() as session:
async with session.get(f"{base_url}/api/linkedin/image-generation-health") as response:
if response.status == 200:
health_data = await response.json()
print(f"✅ Health Check: {health_data['status']}")
print(f" Services: {health_data['services']}")
print(f" Test Prompts: {health_data['test_prompts_generated']}")
else:
print(f"❌ Health Check Failed: {response.status}")
return
# Test 2: Generate Image Prompts
print("\n2⃣ Testing Image Prompt Generation...")
prompt_data = {
"content_type": "post",
"topic": "AI in Marketing",
"industry": "Technology",
"content": "This is a test LinkedIn post about AI in marketing. It demonstrates the image generation capabilities."
}
async with aiohttp.ClientSession() as session:
async with session.post(
f"{base_url}/api/linkedin/generate-image-prompts",
json=prompt_data
) as response:
if response.status == 200:
prompts = await response.json()
print(f"✅ Generated {len(prompts)} image prompts:")
for i, prompt in enumerate(prompts, 1):
print(f" {i}. {prompt['style']}: {prompt['description']}")
# Test 3: Generate Image from First Prompt
print("\n3⃣ Testing Image Generation...")
image_data = {
"prompt": prompts[0]['prompt'],
"content_context": {
"topic": prompt_data["topic"],
"industry": prompt_data["industry"],
"content_type": prompt_data["content_type"],
"content": prompt_data["content"],
"style": prompts[0]['style']
},
"aspect_ratio": "1:1"
}
async with session.post(
f"{base_url}/api/linkedin/generate-image",
json=image_data
) as img_response:
if img_response.status == 200:
result = await img_response.json()
if result.get('success'):
print(f"✅ Image Generated Successfully!")
print(f" Image ID: {result.get('image_id')}")
print(f" Style: {result.get('style')}")
print(f" Aspect Ratio: {result.get('aspect_ratio')}")
else:
print(f"❌ Image Generation Failed: {result.get('error')}")
else:
print(f"❌ Image Generation Request Failed: {img_response.status}")
error_text = await img_response.text()
print(f" Error: {error_text}")
else:
print(f"❌ Prompt Generation Failed: {response.status}")
error_text = await response.text()
print(f" Error: {error_text}")
if __name__ == "__main__":
print("🚀 Starting LinkedIn Image Generation API Tests...")
try:
asyncio.run(test_image_generation_api())
print("\n🎉 All tests completed!")
except Exception as e:
print(f"\n💥 Test failed with error: {e}")
import traceback
traceback.print_exc()

View File

@@ -0,0 +1,191 @@
#!/usr/bin/env python3
"""
Test Script for LinkedIn Image Generation Infrastructure
This script tests the basic functionality of the LinkedIn image generation services
to ensure they are properly initialized and can perform basic operations.
"""
import asyncio
import sys
import os
from pathlib import Path
# Add the backend directory to the Python path
backend_path = Path(__file__).parent
sys.path.insert(0, str(backend_path))
from loguru import logger
# Configure logging
logger.remove()
logger.add(sys.stdout, colorize=True, format="<level>{level}</level>| {message}")
async def test_linkedin_image_infrastructure():
"""Test the LinkedIn image generation infrastructure."""
logger.info("🧪 Testing LinkedIn Image Generation Infrastructure")
logger.info("=" * 60)
try:
# Test 1: Import LinkedIn Image Services
logger.info("📦 Test 1: Importing LinkedIn Image Services...")
from services.linkedin.image_generation import (
LinkedInImageGenerator,
LinkedInImageEditor,
LinkedInImageStorage
)
from services.linkedin.image_prompts import LinkedInPromptGenerator
logger.success("✅ All LinkedIn image services imported successfully")
# Test 2: Initialize Services
logger.info("🔧 Test 2: Initializing LinkedIn Image Services...")
# Initialize services (without API keys for testing)
image_generator = LinkedInImageGenerator()
image_editor = LinkedInImageEditor()
image_storage = LinkedInImageStorage()
prompt_generator = LinkedInPromptGenerator()
logger.success("✅ All LinkedIn image services initialized successfully")
# Test 3: Test Prompt Generation (without API calls)
logger.info("📝 Test 3: Testing Prompt Generation Logic...")
# Test content context
test_content = {
'topic': 'AI in Marketing',
'industry': 'Technology',
'content_type': 'post',
'content': 'Exploring how artificial intelligence is transforming modern marketing strategies.'
}
# Test fallback prompt generation
fallback_prompts = prompt_generator._get_fallback_prompts(test_content, "1:1")
if len(fallback_prompts) == 3:
logger.success(f"✅ Fallback prompt generation working: {len(fallback_prompts)} prompts created")
for i, prompt in enumerate(fallback_prompts):
logger.info(f" Prompt {i+1}: {prompt['style']} - {prompt['description']}")
else:
logger.error(f"❌ Fallback prompt generation failed: expected 3, got {len(fallback_prompts)}")
# Test 4: Test Image Storage Directory Creation
logger.info("📁 Test 4: Testing Image Storage Directory Creation...")
# Check if storage directories were created
storage_path = image_storage.base_storage_path
if storage_path.exists():
logger.success(f"✅ Storage base directory created: {storage_path}")
# Check subdirectories
for subdir in ['images', 'metadata', 'temp']:
subdir_path = storage_path / subdir
if subdir_path.exists():
logger.info(f"{subdir} directory exists: {subdir_path}")
else:
logger.warning(f" ⚠️ {subdir} directory missing: {subdir_path}")
else:
logger.error(f"❌ Storage base directory not created: {storage_path}")
# Test 5: Test Service Methods
logger.info("⚙️ Test 5: Testing Service Method Signatures...")
# Test image generator methods
if hasattr(image_generator, 'generate_image'):
logger.success("✅ LinkedInImageGenerator.generate_image method exists")
else:
logger.error("❌ LinkedInImageGenerator.generate_image method missing")
if hasattr(image_editor, 'edit_image_conversationally'):
logger.success("✅ LinkedInImageEditor.edit_image_conversationally method exists")
else:
logger.error("❌ LinkedInImageEditor.edit_image_conversationally method missing")
if hasattr(image_storage, 'store_image'):
logger.success("✅ LinkedInImageStorage.store_image method exists")
else:
logger.error("❌ LinkedInImageStorage.store_image method missing")
if hasattr(prompt_generator, 'generate_three_prompts'):
logger.success("✅ LinkedInPromptGenerator.generate_three_prompts method exists")
else:
logger.error("❌ LinkedInPromptGenerator.generate_three_prompts method missing")
# Test 6: Test Prompt Enhancement
logger.info("🎨 Test 6: Testing Prompt Enhancement Logic...")
test_prompt = {
'style': 'Professional',
'prompt': 'Create a business image',
'description': 'Professional style'
}
enhanced_prompt = prompt_generator._enhance_prompt_for_linkedin(
test_prompt, test_content, "1:1", 0
)
if enhanced_prompt and 'enhanced_at' in enhanced_prompt:
logger.success("✅ Prompt enhancement working")
logger.info(f" Enhanced prompt length: {len(enhanced_prompt['prompt'])} characters")
else:
logger.error("❌ Prompt enhancement failed")
# Test 7: Test Image Validation Logic
logger.info("🔍 Test 7: Testing Image Validation Logic...")
# Test aspect ratio validation
valid_ratios = [(1024, 1024), (1600, 900), (1200, 1600)]
invalid_ratios = [(500, 500), (2000, 500)]
for width, height in valid_ratios:
if image_generator._is_aspect_ratio_suitable(width, height):
logger.info(f" ✅ Valid ratio {width}:{height} correctly identified")
else:
logger.warning(f" ⚠️ Valid ratio {width}:{height} incorrectly rejected")
for width, height in invalid_ratios:
if not image_generator._is_aspect_ratio_suitable(width, height):
logger.info(f" ✅ Invalid ratio {width}:{height} correctly rejected")
else:
logger.warning(f" ⚠️ Invalid ratio {width}:{height} incorrectly accepted")
logger.info("=" * 60)
logger.success("🎉 LinkedIn Image Generation Infrastructure Test Completed Successfully!")
return True
except ImportError as e:
logger.error(f"❌ Import Error: {e}")
logger.error("This usually means there's an issue with the module structure or dependencies")
return False
except Exception as e:
logger.error(f"❌ Test Failed: {e}")
logger.error(f"Error type: {type(e).__name__}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
return False
async def main():
"""Main test function."""
logger.info("🚀 Starting LinkedIn Image Generation Infrastructure Tests")
success = await test_linkedin_image_infrastructure()
if success:
logger.success("✅ All tests passed! The infrastructure is ready for use.")
sys.exit(0)
else:
logger.error("❌ Some tests failed. Please check the errors above.")
sys.exit(1)
if __name__ == "__main__":
# Run the async test
asyncio.run(main())