ALwrity Prompts - AI Integration Plan
This commit is contained in:
220
backend/api/linkedin_image_generation.py
Normal file
220
backend/api/linkedin_image_generation.py
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
@@ -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'
|
||||
]
|
||||
@@ -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()
|
||||
@@ -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)}"
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)}"
|
||||
}
|
||||
@@ -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()
|
||||
22
backend/services/linkedin/image_generation/__init__.py
Normal file
22
backend/services/linkedin/image_generation/__init__.py
Normal 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"
|
||||
@@ -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 []
|
||||
@@ -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
|
||||
@@ -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
|
||||
18
backend/services/linkedin/image_prompts/__init__.py
Normal file
18
backend/services/linkedin/image_prompts/__init__.py
Normal 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"
|
||||
@@ -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
|
||||
@@ -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}")
|
||||
|
||||
@@ -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']
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
228
backend/test_enhanced_prompt_generation.py
Normal file
228
backend/test_enhanced_prompt_generation.py
Normal 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
95
backend/test_image_api.py
Normal 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()
|
||||
191
backend/test_linkedin_image_infrastructure.py
Normal file
191
backend/test_linkedin_image_infrastructure.py
Normal 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())
|
||||
Reference in New Issue
Block a user