AI Analysis and Content Strategy fixes. Enhanced Strategy Routes refactoring.
This commit is contained in:
@@ -1,23 +1,15 @@
|
||||
"""Product Marketing Suite service package."""
|
||||
"""Product Marketing Suite service package - Product asset creation only."""
|
||||
|
||||
from .orchestrator import ProductMarketingOrchestrator
|
||||
from .brand_dna_sync import BrandDNASyncService
|
||||
from .prompt_builder import ProductMarketingPromptBuilder
|
||||
from .asset_audit import AssetAuditService
|
||||
from .channel_pack import ChannelPackService
|
||||
from .campaign_storage import CampaignStorageService
|
||||
from .product_image_service import ProductImageService
|
||||
from .product_animation_service import ProductAnimationService, ProductAnimationRequest
|
||||
from .product_video_service import ProductVideoService, ProductVideoRequest
|
||||
from .product_avatar_service import ProductAvatarService, ProductAvatarRequest
|
||||
from .intelligent_prompt_builder import IntelligentPromptBuilder
|
||||
from .personalization_service import PersonalizationService
|
||||
|
||||
__all__ = [
|
||||
"ProductMarketingOrchestrator",
|
||||
"BrandDNASyncService",
|
||||
"ProductMarketingPromptBuilder",
|
||||
"AssetAuditService",
|
||||
"ChannelPackService",
|
||||
"CampaignStorageService",
|
||||
"ProductImageService",
|
||||
"ProductAnimationService",
|
||||
"ProductAnimationRequest",
|
||||
@@ -25,5 +17,7 @@ __all__ = [
|
||||
"ProductVideoRequest",
|
||||
"ProductAvatarService",
|
||||
"ProductAvatarRequest",
|
||||
"IntelligentPromptBuilder",
|
||||
"PersonalizationService",
|
||||
]
|
||||
|
||||
|
||||
454
backend/services/product_marketing/intelligent_prompt_builder.py
Normal file
454
backend/services/product_marketing/intelligent_prompt_builder.py
Normal file
@@ -0,0 +1,454 @@
|
||||
"""
|
||||
Intelligent Prompt Builder
|
||||
Infers complete requirements from minimal user input using onboarding data.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, List
|
||||
from loguru import logger
|
||||
import json
|
||||
|
||||
from services.onboarding.database_service import OnboardingDatabaseService
|
||||
from services.database import SessionLocal
|
||||
from services.llm_providers.main_text_generation import llm_text_gen
|
||||
from .product_marketing_templates import (
|
||||
ProductMarketingTemplates,
|
||||
TemplateCategory,
|
||||
ProductImageTemplate,
|
||||
ProductVideoTemplate,
|
||||
ProductAvatarTemplate,
|
||||
)
|
||||
|
||||
|
||||
class IntelligentPromptBuilder:
|
||||
"""
|
||||
Intelligent prompt builder that infers requirements from minimal user input.
|
||||
|
||||
Example:
|
||||
Input: "iPhone case for my store"
|
||||
Output: Complete configuration with all fields pre-filled
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize Intelligent Prompt Builder."""
|
||||
self.logger = logger
|
||||
logger.info("[Intelligent Prompt Builder] Initialized")
|
||||
|
||||
def infer_requirements(
|
||||
self,
|
||||
user_input: str,
|
||||
user_id: str,
|
||||
asset_type: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Infer complete requirements from minimal user input.
|
||||
|
||||
Args:
|
||||
user_input: Minimal user input (e.g., "iPhone case for my store")
|
||||
user_id: User ID to fetch onboarding data
|
||||
asset_type: Optional asset type hint (image, video, animation, avatar)
|
||||
|
||||
Returns:
|
||||
Complete configuration dictionary with all fields pre-filled
|
||||
"""
|
||||
try:
|
||||
# 1. Parse user input
|
||||
parsed_input = self._parse_user_input(user_input, asset_type)
|
||||
|
||||
# 2. Get onboarding data
|
||||
onboarding_data = self._get_onboarding_data(user_id)
|
||||
|
||||
# 3. Infer requirements from context
|
||||
requirements = self._infer_from_context(parsed_input, onboarding_data, asset_type)
|
||||
|
||||
# 4. Match template
|
||||
template = self._match_template(requirements, asset_type)
|
||||
|
||||
# 5. Generate smart defaults
|
||||
defaults = self._generate_defaults(requirements, template, onboarding_data)
|
||||
|
||||
logger.info(f"[Intelligent Prompt Builder] Inferred requirements: {defaults.get('product_name', 'Unknown')}")
|
||||
return defaults
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Intelligent Prompt Builder] Error inferring requirements: {str(e)}", exc_info=True)
|
||||
# Return basic defaults on error
|
||||
return self._get_basic_defaults(user_input, asset_type)
|
||||
|
||||
def _parse_user_input(
|
||||
self,
|
||||
user_input: str,
|
||||
asset_type: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse minimal user input to extract entities.
|
||||
|
||||
Uses LLM with few-shot prompting to extract:
|
||||
- Product name
|
||||
- Product type
|
||||
- Use case (e-commerce, marketing, social media, etc.)
|
||||
- Platform hints (store, Instagram, Shopify, Amazon, etc.)
|
||||
- Style preferences
|
||||
"""
|
||||
try:
|
||||
# Build system prompt for entity extraction
|
||||
system_prompt = """You are an expert at parsing product marketing requests.
|
||||
Extract key information from user input and return structured JSON.
|
||||
|
||||
Extract:
|
||||
- product_name: The product name or description
|
||||
- product_type: Type of product (phone_case, clothing, electronics, food, etc.)
|
||||
- use_case: Primary use case (ecommerce, social_media, marketing_campaign, documentation, etc.)
|
||||
- platform_hints: Platforms mentioned (shopify, amazon, instagram, facebook, etc.)
|
||||
- style_hints: Style preferences mentioned (professional, casual, luxury, minimalist, etc.)
|
||||
- asset_type_hint: Type of asset needed (image, video, animation, avatar) if mentioned
|
||||
|
||||
Return JSON only, no explanations."""
|
||||
|
||||
# Few-shot examples
|
||||
examples = """
|
||||
Examples:
|
||||
Input: "iPhone case for my store"
|
||||
Output: {"product_name": "iPhone case", "product_type": "phone_case", "use_case": "ecommerce", "platform_hints": ["shopify"], "style_hints": [], "asset_type_hint": "image"}
|
||||
|
||||
Input: "Create a video for my new product launch on Instagram"
|
||||
Output: {"product_name": "new product", "product_type": "unknown", "use_case": "social_media", "platform_hints": ["instagram"], "style_hints": [], "asset_type_hint": "video"}
|
||||
|
||||
Input: "Luxury watch photoshoot"
|
||||
Output: {"product_name": "luxury watch", "product_type": "watch", "use_case": "marketing_campaign", "platform_hints": [], "style_hints": ["luxury"], "asset_type_hint": "image"}
|
||||
"""
|
||||
|
||||
prompt = f"{examples}\n\nInput: {user_input}\nOutput:"
|
||||
|
||||
# Call LLM for parsing
|
||||
json_struct = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"product_name": {"type": "string"},
|
||||
"product_type": {"type": "string"},
|
||||
"use_case": {"type": "string"},
|
||||
"platform_hints": {"type": "array", "items": {"type": "string"}},
|
||||
"style_hints": {"type": "array", "items": {"type": "string"}},
|
||||
"asset_type_hint": {"type": "string"}
|
||||
},
|
||||
"required": ["product_name", "use_case"]
|
||||
}
|
||||
|
||||
# Call LLM synchronously (llm_text_gen is synchronous)
|
||||
result_text = llm_text_gen(
|
||||
prompt=prompt,
|
||||
system_prompt=system_prompt,
|
||||
json_struct=json_struct,
|
||||
user_id=None # No user_id needed for parsing
|
||||
)
|
||||
|
||||
# Parse JSON response
|
||||
try:
|
||||
parsed = json.loads(result_text) if isinstance(result_text, str) else result_text
|
||||
except json.JSONDecodeError:
|
||||
# Fallback: try to extract JSON from text
|
||||
import re
|
||||
json_match = re.search(r'\{[^}]+\}', result_text)
|
||||
if json_match:
|
||||
parsed = json.loads(json_match.group())
|
||||
else:
|
||||
# Ultimate fallback: basic extraction
|
||||
parsed = {
|
||||
"product_name": user_input,
|
||||
"product_type": "unknown",
|
||||
"use_case": "marketing_campaign",
|
||||
"platform_hints": [],
|
||||
"style_hints": [],
|
||||
"asset_type_hint": asset_type or "image"
|
||||
}
|
||||
|
||||
# Override asset_type_hint if provided
|
||||
if asset_type:
|
||||
parsed["asset_type_hint"] = asset_type
|
||||
|
||||
logger.info(f"[Intelligent Prompt Builder] Parsed input: {parsed}")
|
||||
return parsed
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Intelligent Prompt Builder] Error parsing input: {str(e)}")
|
||||
# Fallback: basic extraction
|
||||
return {
|
||||
"product_name": user_input,
|
||||
"product_type": "unknown",
|
||||
"use_case": "marketing_campaign",
|
||||
"platform_hints": [],
|
||||
"style_hints": [],
|
||||
"asset_type_hint": asset_type or "image"
|
||||
}
|
||||
|
||||
def _get_onboarding_data(self, user_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get all onboarding data for user.
|
||||
|
||||
Returns:
|
||||
Dictionary with website_analysis, persona_data, competitor_analyses
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
onboarding_db = OnboardingDatabaseService(db)
|
||||
website_analysis = onboarding_db.get_website_analysis(user_id, db)
|
||||
persona_data = onboarding_db.get_persona_data(user_id, db)
|
||||
competitor_analyses = onboarding_db.get_competitor_analysis(user_id, db)
|
||||
|
||||
return {
|
||||
"website_analysis": website_analysis or {},
|
||||
"persona_data": persona_data or {},
|
||||
"competitor_analyses": competitor_analyses or [],
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"[Intelligent Prompt Builder] Error getting onboarding data: {str(e)}")
|
||||
return {
|
||||
"website_analysis": {},
|
||||
"persona_data": {},
|
||||
"competitor_analyses": [],
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def _infer_from_context(
|
||||
self,
|
||||
parsed_input: Dict[str, Any],
|
||||
onboarding_data: Dict[str, Any],
|
||||
asset_type: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Infer requirements from parsed input and onboarding context.
|
||||
|
||||
Uses onboarding data to fill in missing information:
|
||||
- Platform from onboarding (if user has e-commerce setup)
|
||||
- Style from brand DNA
|
||||
- Target audience from onboarding
|
||||
"""
|
||||
requirements = parsed_input.copy()
|
||||
|
||||
website_analysis = onboarding_data.get("website_analysis", {})
|
||||
persona_data = onboarding_data.get("persona_data", {})
|
||||
|
||||
# Infer platform from onboarding
|
||||
if not requirements.get("platform_hints"):
|
||||
# Check if user has e-commerce setup (from website analysis)
|
||||
brand_analysis = website_analysis.get("brand_analysis", {})
|
||||
# Try to infer platform from website URL or other hints
|
||||
# For now, default to e-commerce if no hints
|
||||
if requirements.get("use_case") == "ecommerce":
|
||||
requirements["platform_hints"] = ["shopify"] # Default e-commerce platform
|
||||
|
||||
# Infer style from brand DNA
|
||||
if not requirements.get("style_hints"):
|
||||
if brand_analysis:
|
||||
style_guidelines = brand_analysis.get("style_guidelines", {})
|
||||
aesthetic = style_guidelines.get("aesthetic", "")
|
||||
if aesthetic:
|
||||
requirements["style_hints"] = [aesthetic.lower()]
|
||||
|
||||
# Infer target audience from onboarding
|
||||
target_audience = website_analysis.get("target_audience", {})
|
||||
if target_audience:
|
||||
requirements["target_audience"] = target_audience
|
||||
|
||||
# Infer brand colors
|
||||
if brand_analysis:
|
||||
color_palette = brand_analysis.get("color_palette", [])
|
||||
if color_palette:
|
||||
requirements["brand_colors"] = color_palette[:5] # Top 5 colors
|
||||
|
||||
# Infer writing style
|
||||
writing_style = website_analysis.get("writing_style", {})
|
||||
if writing_style:
|
||||
requirements["tone"] = writing_style.get("tone", "professional")
|
||||
requirements["voice"] = writing_style.get("voice", "authoritative")
|
||||
|
||||
return requirements
|
||||
|
||||
def _match_template(
|
||||
self,
|
||||
requirements: Dict[str, Any],
|
||||
asset_type: Optional[str] = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Match requirements to appropriate template.
|
||||
|
||||
Returns:
|
||||
Template dictionary or None
|
||||
"""
|
||||
asset_type_hint = asset_type or requirements.get("asset_type_hint", "image")
|
||||
use_case = requirements.get("use_case", "marketing_campaign")
|
||||
style_hints = requirements.get("style_hints", [])
|
||||
|
||||
if asset_type_hint == "image":
|
||||
templates = ProductMarketingTemplates.get_product_image_templates()
|
||||
|
||||
# Match by use case
|
||||
if use_case == "ecommerce":
|
||||
# Match e-commerce template
|
||||
for template in templates:
|
||||
if "ecommerce" in template.id.lower() or "e-commerce" in template.name.lower():
|
||||
return {
|
||||
"id": template.id,
|
||||
"name": template.name,
|
||||
"category": template.category.value,
|
||||
"environment": template.environment,
|
||||
"background_style": template.background_style,
|
||||
"lighting": template.lighting,
|
||||
"style": template.style,
|
||||
"angle": template.angle,
|
||||
"recommended_resolution": template.recommended_resolution,
|
||||
}
|
||||
|
||||
# Match by style
|
||||
if style_hints:
|
||||
style_lower = style_hints[0].lower()
|
||||
for template in templates:
|
||||
if style_lower in template.style.lower() or style_lower in template.name.lower():
|
||||
return {
|
||||
"id": template.id,
|
||||
"name": template.name,
|
||||
"category": template.category.value,
|
||||
"environment": template.environment,
|
||||
"background_style": template.background_style,
|
||||
"lighting": template.lighting,
|
||||
"style": template.style,
|
||||
"angle": template.angle,
|
||||
"recommended_resolution": template.recommended_resolution,
|
||||
}
|
||||
|
||||
# Default: e-commerce product shot
|
||||
default_template = templates[0] # ecommerce_product_shot
|
||||
return {
|
||||
"id": default_template.id,
|
||||
"name": default_template.name,
|
||||
"category": default_template.category.value,
|
||||
"environment": default_template.environment,
|
||||
"background_style": default_template.background_style,
|
||||
"lighting": default_template.lighting,
|
||||
"style": default_template.style,
|
||||
"angle": default_template.angle,
|
||||
"recommended_resolution": default_template.recommended_resolution,
|
||||
}
|
||||
|
||||
elif asset_type_hint == "video":
|
||||
templates = ProductMarketingTemplates.get_product_video_templates()
|
||||
# Default: product demo video
|
||||
default_template = templates[0]
|
||||
return {
|
||||
"id": default_template.id,
|
||||
"name": default_template.name,
|
||||
"category": default_template.category.value,
|
||||
"video_type": default_template.video_type,
|
||||
"resolution": default_template.resolution,
|
||||
"duration": default_template.duration,
|
||||
}
|
||||
|
||||
elif asset_type_hint == "avatar":
|
||||
templates = ProductMarketingTemplates.get_product_avatar_templates()
|
||||
# Default: product overview
|
||||
default_template = templates[0]
|
||||
return {
|
||||
"id": default_template.id,
|
||||
"name": default_template.name,
|
||||
"category": default_template.category.value,
|
||||
"explainer_type": default_template.explainer_type,
|
||||
"resolution": default_template.resolution,
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def _generate_defaults(
|
||||
self,
|
||||
requirements: Dict[str, Any],
|
||||
template: Optional[Dict[str, Any]],
|
||||
onboarding_data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate complete configuration with smart defaults.
|
||||
|
||||
Combines:
|
||||
- Parsed requirements
|
||||
- Matched template
|
||||
- Onboarding data
|
||||
"""
|
||||
defaults = {}
|
||||
|
||||
# Product information
|
||||
defaults["product_name"] = requirements.get("product_name", "Product")
|
||||
defaults["product_description"] = requirements.get("product_description", f"Professional {requirements.get('product_name', 'product')}")
|
||||
|
||||
# Asset type
|
||||
asset_type = requirements.get("asset_type_hint", "image")
|
||||
defaults["asset_type"] = asset_type
|
||||
|
||||
# Template information
|
||||
if template:
|
||||
defaults["template_id"] = template.get("id")
|
||||
defaults["template_name"] = template.get("name")
|
||||
|
||||
# Image-specific defaults
|
||||
if asset_type == "image" and template:
|
||||
defaults["environment"] = template.get("environment", "studio")
|
||||
defaults["background_style"] = template.get("background_style", "white")
|
||||
defaults["lighting"] = template.get("lighting", "studio")
|
||||
defaults["style"] = template.get("style", "photorealistic")
|
||||
defaults["angle"] = template.get("angle", "front")
|
||||
defaults["resolution"] = template.get("recommended_resolution", "1024x1024")
|
||||
defaults["num_variations"] = 1
|
||||
|
||||
# Override with style hints if available
|
||||
if requirements.get("style_hints"):
|
||||
style_hint = requirements["style_hints"][0].lower()
|
||||
if "luxury" in style_hint:
|
||||
defaults["style"] = "luxury"
|
||||
defaults["lighting"] = "dramatic"
|
||||
elif "minimalist" in style_hint:
|
||||
defaults["style"] = "minimalist"
|
||||
defaults["background_style"] = "white"
|
||||
elif "lifestyle" in style_hint:
|
||||
defaults["environment"] = "lifestyle"
|
||||
defaults["background_style"] = "lifestyle"
|
||||
|
||||
# Video-specific defaults
|
||||
elif asset_type == "video" and template:
|
||||
defaults["video_type"] = template.get("video_type", "demo")
|
||||
defaults["resolution"] = template.get("resolution", "720p")
|
||||
defaults["duration"] = template.get("duration", 10)
|
||||
|
||||
# Avatar-specific defaults
|
||||
elif asset_type == "avatar" and template:
|
||||
defaults["explainer_type"] = template.get("explainer_type", "product_overview")
|
||||
defaults["resolution"] = template.get("resolution", "720p")
|
||||
|
||||
# Brand colors from onboarding
|
||||
if requirements.get("brand_colors"):
|
||||
defaults["brand_colors"] = requirements["brand_colors"]
|
||||
|
||||
# Additional context
|
||||
defaults["additional_context"] = requirements.get("additional_context", "")
|
||||
|
||||
# Confidence score (how well we matched)
|
||||
defaults["confidence"] = 0.8 if template else 0.5
|
||||
defaults["inferred_fields"] = list(defaults.keys())
|
||||
|
||||
return defaults
|
||||
|
||||
def _get_basic_defaults(
|
||||
self,
|
||||
user_input: str,
|
||||
asset_type: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Get basic defaults when parsing fails."""
|
||||
return {
|
||||
"product_name": user_input,
|
||||
"product_description": f"Professional {user_input}",
|
||||
"asset_type": asset_type or "image",
|
||||
"environment": "studio",
|
||||
"background_style": "white",
|
||||
"lighting": "studio",
|
||||
"style": "photorealistic",
|
||||
"resolution": "1024x1024",
|
||||
"num_variations": 1,
|
||||
"confidence": 0.3,
|
||||
"inferred_fields": ["product_name", "product_description"],
|
||||
}
|
||||
413
backend/services/product_marketing/personalization_service.py
Normal file
413
backend/services/product_marketing/personalization_service.py
Normal file
@@ -0,0 +1,413 @@
|
||||
"""
|
||||
Personalization Service
|
||||
Extracts ALL onboarding data and provides personalized defaults for forms and recommendations.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, List
|
||||
from loguru import logger
|
||||
|
||||
from services.onboarding.database_service import OnboardingDatabaseService
|
||||
from services.database import SessionLocal
|
||||
|
||||
|
||||
class PersonalizationService:
|
||||
"""
|
||||
Service for extracting user preferences from onboarding data
|
||||
and providing personalized defaults and recommendations.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize Personalization Service."""
|
||||
self.logger = logger
|
||||
logger.info("[Personalization Service] Initialized")
|
||||
|
||||
def get_user_preferences(self, user_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get comprehensive user preferences from ALL onboarding data.
|
||||
|
||||
Returns:
|
||||
Dictionary with personalized preferences:
|
||||
- industry: User's industry
|
||||
- target_audience: Demographics, expertise level
|
||||
- platform_preferences: Preferred platforms from persona data
|
||||
- content_preferences: Preferred content types
|
||||
- style_preferences: Visual style, tone, voice
|
||||
- brand_colors: Brand color palette
|
||||
- templates: Recommended templates for user's industry
|
||||
- channels: Recommended channels based on platform personas
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
onboarding_db = OnboardingDatabaseService(db)
|
||||
website_analysis = onboarding_db.get_website_analysis(user_id, db)
|
||||
persona_data = onboarding_db.get_persona_data(user_id, db)
|
||||
competitor_analyses = onboarding_db.get_competitor_analysis(user_id, db)
|
||||
|
||||
preferences = {
|
||||
"industry": None,
|
||||
"target_audience": {},
|
||||
"platform_preferences": [],
|
||||
"content_preferences": [],
|
||||
"style_preferences": {},
|
||||
"brand_colors": [],
|
||||
"recommended_templates": [],
|
||||
"recommended_channels": [],
|
||||
"writing_style": {},
|
||||
"brand_values": [],
|
||||
}
|
||||
|
||||
# Extract from website_analysis
|
||||
if website_analysis:
|
||||
# Industry
|
||||
target_audience = website_analysis.get("target_audience", {})
|
||||
preferences["industry"] = target_audience.get("industry_focus")
|
||||
|
||||
# Target audience
|
||||
preferences["target_audience"] = {
|
||||
"demographics": target_audience.get("demographics", []),
|
||||
"expertise_level": target_audience.get("expertise_level", "intermediate"),
|
||||
"industry_focus": target_audience.get("industry_focus"),
|
||||
}
|
||||
|
||||
# Writing style
|
||||
writing_style = website_analysis.get("writing_style", {})
|
||||
preferences["writing_style"] = {
|
||||
"tone": writing_style.get("tone", "professional"),
|
||||
"voice": writing_style.get("voice", "authoritative"),
|
||||
"complexity": writing_style.get("complexity", "intermediate"),
|
||||
"engagement_level": writing_style.get("engagement_level", "moderate"),
|
||||
}
|
||||
|
||||
# Brand colors
|
||||
brand_analysis = website_analysis.get("brand_analysis", {})
|
||||
if brand_analysis:
|
||||
preferences["brand_colors"] = brand_analysis.get("color_palette", [])
|
||||
preferences["brand_values"] = brand_analysis.get("brand_values", [])
|
||||
|
||||
# Style preferences
|
||||
style_guidelines = website_analysis.get("style_guidelines", {})
|
||||
if style_guidelines:
|
||||
preferences["style_preferences"] = {
|
||||
"aesthetic": style_guidelines.get("aesthetic", "modern"),
|
||||
"visual_style": style_guidelines.get("visual_style", "clean"),
|
||||
}
|
||||
|
||||
# Extract from persona_data
|
||||
if persona_data:
|
||||
core_persona = persona_data.get("corePersona", {})
|
||||
platform_personas = persona_data.get("platformPersonas", {})
|
||||
selected_platforms = persona_data.get("selectedPlatforms", [])
|
||||
|
||||
# Platform preferences from selected platforms
|
||||
if selected_platforms:
|
||||
preferences["platform_preferences"] = selected_platforms
|
||||
elif platform_personas:
|
||||
# Extract platforms from platform personas
|
||||
preferences["platform_preferences"] = list(platform_personas.keys())
|
||||
|
||||
# Recommended channels based on platform personas
|
||||
if platform_personas:
|
||||
# Prioritize platforms with active personas
|
||||
preferences["recommended_channels"] = list(platform_personas.keys())[:5] # Top 5
|
||||
|
||||
# Content preferences from persona
|
||||
if core_persona:
|
||||
content_format_rules = core_persona.get("content_format_rules", {})
|
||||
if content_format_rules:
|
||||
preferred_formats = content_format_rules.get("preferred_formats", [])
|
||||
preferences["content_preferences"] = preferred_formats
|
||||
|
||||
# Infer content preferences from industry
|
||||
if preferences["industry"]:
|
||||
industry_content_map = {
|
||||
"ecommerce": ["product_images", "product_videos", "lifestyle_content"],
|
||||
"saas": ["feature_highlights", "tutorials", "demo_videos"],
|
||||
"education": ["tutorials", "educational_content", "explainer_videos"],
|
||||
"healthcare": ["informational_content", "patient_stories", "educational_videos"],
|
||||
"finance": ["informational_content", "trust_building", "expert_content"],
|
||||
"fashion": ["lifestyle_images", "fashion_shows", "style_guides"],
|
||||
"food": ["food_photography", "recipe_videos", "lifestyle_content"],
|
||||
}
|
||||
industry_lower = preferences["industry"].lower()
|
||||
for key, content_types in industry_content_map.items():
|
||||
if key in industry_lower:
|
||||
preferences["content_preferences"] = content_types
|
||||
break
|
||||
|
||||
# Recommend templates based on industry
|
||||
preferences["recommended_templates"] = self._get_recommended_templates(
|
||||
preferences.get("industry"),
|
||||
preferences.get("style_preferences", {}).get("aesthetic")
|
||||
)
|
||||
|
||||
# Recommend channels if not already set
|
||||
if not preferences["recommended_channels"]:
|
||||
preferences["recommended_channels"] = self._get_recommended_channels(
|
||||
preferences.get("industry"),
|
||||
preferences.get("target_audience", {}).get("demographics", [])
|
||||
)
|
||||
|
||||
logger.info(f"[Personalization] Extracted preferences for user {user_id}: industry={preferences.get('industry')}")
|
||||
return preferences
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Personalization] Error getting user preferences: {str(e)}", exc_info=True)
|
||||
return self._get_default_preferences()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def get_personalized_defaults(
|
||||
self,
|
||||
user_id: str,
|
||||
form_type: str = "product_photoshoot"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get personalized defaults for a specific form.
|
||||
|
||||
Args:
|
||||
user_id: User ID
|
||||
form_type: Type of form (product_photoshoot, campaign_creator, product_video, etc.)
|
||||
|
||||
Returns:
|
||||
Dictionary with pre-filled form values
|
||||
"""
|
||||
preferences = self.get_user_preferences(user_id)
|
||||
defaults = {}
|
||||
|
||||
if form_type == "product_photoshoot":
|
||||
defaults = {
|
||||
"environment": self._infer_environment(preferences),
|
||||
"background_style": self._infer_background_style(preferences),
|
||||
"lighting": self._infer_lighting(preferences),
|
||||
"style": self._infer_style(preferences),
|
||||
"resolution": "1024x1024",
|
||||
"num_variations": 1,
|
||||
"brand_colors": preferences.get("brand_colors", []),
|
||||
}
|
||||
|
||||
elif form_type == "campaign_creator":
|
||||
defaults = {
|
||||
"channels": preferences.get("recommended_channels", ["instagram", "linkedin"]),
|
||||
"goal": self._infer_campaign_goal(preferences),
|
||||
}
|
||||
|
||||
elif form_type == "product_video":
|
||||
defaults = {
|
||||
"video_type": self._infer_video_type(preferences),
|
||||
"resolution": "720p",
|
||||
"duration": 10,
|
||||
}
|
||||
|
||||
elif form_type == "product_avatar":
|
||||
defaults = {
|
||||
"explainer_type": self._infer_explainer_type(preferences),
|
||||
"resolution": "720p",
|
||||
}
|
||||
|
||||
return defaults
|
||||
|
||||
def get_recommendations(self, user_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get personalized recommendations for user.
|
||||
|
||||
Returns:
|
||||
Dictionary with:
|
||||
- recommended_templates: Templates matching user's industry
|
||||
- recommended_channels: Channels matching user's platform personas
|
||||
- recommended_asset_types: Asset types matching user's content preferences
|
||||
"""
|
||||
preferences = self.get_user_preferences(user_id)
|
||||
|
||||
return {
|
||||
"templates": preferences.get("recommended_templates", []),
|
||||
"channels": preferences.get("recommended_channels", []),
|
||||
"asset_types": preferences.get("content_preferences", []),
|
||||
"industry": preferences.get("industry"),
|
||||
"reasoning": self._generate_recommendation_reasoning(preferences),
|
||||
}
|
||||
|
||||
def _get_recommended_templates(
|
||||
self,
|
||||
industry: Optional[str],
|
||||
aesthetic: Optional[str] = None
|
||||
) -> List[str]:
|
||||
"""Get recommended template IDs based on industry and aesthetic."""
|
||||
templates = []
|
||||
|
||||
if not industry:
|
||||
return ["ecommerce_product_shot", "lifestyle_product"]
|
||||
|
||||
industry_lower = industry.lower() if industry else ""
|
||||
|
||||
# Industry-based template recommendations
|
||||
if "ecommerce" in industry_lower or "retail" in industry_lower:
|
||||
templates.extend(["ecommerce_product_shot", "lifestyle_product"])
|
||||
elif "saas" in industry_lower or "tech" in industry_lower:
|
||||
templates.extend(["technical_product_detail", "lifestyle_product"])
|
||||
elif "luxury" in industry_lower or "premium" in industry_lower:
|
||||
templates.extend(["luxury_product_showcase", "lifestyle_product"])
|
||||
else:
|
||||
templates.extend(["ecommerce_product_shot", "lifestyle_product"])
|
||||
|
||||
# Aesthetic-based adjustments
|
||||
if aesthetic:
|
||||
aesthetic_lower = aesthetic.lower()
|
||||
if "luxury" in aesthetic_lower or "premium" in aesthetic_lower:
|
||||
templates.insert(0, "luxury_product_showcase")
|
||||
elif "minimalist" in aesthetic_lower or "clean" in aesthetic_lower:
|
||||
templates.insert(0, "ecommerce_product_shot")
|
||||
|
||||
return templates[:3] # Return top 3
|
||||
|
||||
def _get_recommended_channels(
|
||||
self,
|
||||
industry: Optional[str],
|
||||
demographics: List[str]
|
||||
) -> List[str]:
|
||||
"""Get recommended channels based on industry and demographics."""
|
||||
channels = []
|
||||
|
||||
if not industry:
|
||||
return ["instagram", "linkedin"]
|
||||
|
||||
industry_lower = industry.lower() if industry else ""
|
||||
|
||||
# Industry-based channel recommendations
|
||||
if "b2b" in industry_lower or "saas" in industry_lower or "enterprise" in industry_lower:
|
||||
channels.extend(["linkedin", "twitter", "youtube"])
|
||||
elif "b2c" in industry_lower or "ecommerce" in industry_lower or "retail" in industry_lower:
|
||||
channels.extend(["instagram", "facebook", "pinterest", "tiktok"])
|
||||
elif "fashion" in industry_lower or "lifestyle" in industry_lower:
|
||||
channels.extend(["instagram", "pinterest", "tiktok"])
|
||||
elif "education" in industry_lower:
|
||||
channels.extend(["youtube", "linkedin", "facebook"])
|
||||
else:
|
||||
channels.extend(["instagram", "linkedin", "facebook"])
|
||||
|
||||
# Demographics-based adjustments
|
||||
if demographics:
|
||||
demographics_str = " ".join(demographics).lower()
|
||||
if "young" in demographics_str or "millennial" in demographics_str or "gen z" in demographics_str:
|
||||
if "tiktok" not in channels:
|
||||
channels.insert(0, "tiktok")
|
||||
if "professional" in demographics_str or "business" in demographics_str:
|
||||
if "linkedin" not in channels:
|
||||
channels.insert(0, "linkedin")
|
||||
|
||||
return channels[:5] # Return top 5
|
||||
|
||||
def _infer_environment(self, preferences: Dict[str, Any]) -> str:
|
||||
"""Infer environment setting from preferences."""
|
||||
industry = preferences.get("industry", "").lower() if preferences.get("industry") else ""
|
||||
aesthetic = preferences.get("style_preferences", {}).get("aesthetic", "").lower()
|
||||
|
||||
if "luxury" in aesthetic or "premium" in industry:
|
||||
return "studio"
|
||||
elif "ecommerce" in industry or "retail" in industry:
|
||||
return "studio"
|
||||
elif "lifestyle" in aesthetic:
|
||||
return "lifestyle"
|
||||
else:
|
||||
return "studio"
|
||||
|
||||
def _infer_background_style(self, preferences: Dict[str, Any]) -> str:
|
||||
"""Infer background style from preferences."""
|
||||
industry = preferences.get("industry", "").lower() if preferences.get("industry") else ""
|
||||
aesthetic = preferences.get("style_preferences", {}).get("aesthetic", "").lower()
|
||||
|
||||
if "ecommerce" in industry or "retail" in industry:
|
||||
return "white"
|
||||
elif "luxury" in aesthetic:
|
||||
return "minimalist"
|
||||
elif "lifestyle" in aesthetic:
|
||||
return "lifestyle"
|
||||
else:
|
||||
return "white"
|
||||
|
||||
def _infer_lighting(self, preferences: Dict[str, Any]) -> str:
|
||||
"""Infer lighting style from preferences."""
|
||||
aesthetic = preferences.get("style_preferences", {}).get("aesthetic", "").lower()
|
||||
|
||||
if "luxury" in aesthetic or "dramatic" in aesthetic:
|
||||
return "dramatic"
|
||||
elif "natural" in aesthetic:
|
||||
return "natural"
|
||||
else:
|
||||
return "studio"
|
||||
|
||||
def _infer_style(self, preferences: Dict[str, Any]) -> str:
|
||||
"""Infer image style from preferences."""
|
||||
aesthetic = preferences.get("style_preferences", {}).get("aesthetic", "").lower()
|
||||
industry = preferences.get("industry", "").lower() if preferences.get("industry") else ""
|
||||
|
||||
if "luxury" in aesthetic or "premium" in industry:
|
||||
return "luxury"
|
||||
elif "minimalist" in aesthetic:
|
||||
return "minimalist"
|
||||
elif "technical" in industry or "saas" in industry:
|
||||
return "technical"
|
||||
else:
|
||||
return "photorealistic"
|
||||
|
||||
def _infer_campaign_goal(self, preferences: Dict[str, Any]) -> str:
|
||||
"""Infer campaign goal from preferences."""
|
||||
industry = preferences.get("industry", "").lower() if preferences.get("industry") else ""
|
||||
|
||||
if "saas" in industry or "tech" in industry:
|
||||
return "conversion"
|
||||
elif "ecommerce" in industry or "retail" in industry:
|
||||
return "conversion"
|
||||
else:
|
||||
return "awareness"
|
||||
|
||||
def _infer_video_type(self, preferences: Dict[str, Any]) -> str:
|
||||
"""Infer video type from preferences."""
|
||||
content_prefs = preferences.get("content_preferences", [])
|
||||
|
||||
if "demo" in str(content_prefs).lower():
|
||||
return "demo"
|
||||
elif "tutorial" in str(content_prefs).lower():
|
||||
return "feature_highlight"
|
||||
else:
|
||||
return "demo"
|
||||
|
||||
def _infer_explainer_type(self, preferences: Dict[str, Any]) -> str:
|
||||
"""Infer explainer type from preferences."""
|
||||
content_prefs = preferences.get("content_preferences", [])
|
||||
|
||||
if "tutorial" in str(content_prefs).lower():
|
||||
return "tutorial"
|
||||
elif "feature" in str(content_prefs).lower():
|
||||
return "feature_explainer"
|
||||
else:
|
||||
return "product_overview"
|
||||
|
||||
def _generate_recommendation_reasoning(self, preferences: Dict[str, Any]) -> str:
|
||||
"""Generate human-readable reasoning for recommendations."""
|
||||
industry = preferences.get("industry", "your industry")
|
||||
channels = preferences.get("recommended_channels", [])
|
||||
|
||||
reasoning = f"Based on your {industry} industry"
|
||||
if channels:
|
||||
reasoning += f" and platform preferences, we recommend focusing on {', '.join(channels[:3])}"
|
||||
reasoning += "."
|
||||
|
||||
return reasoning
|
||||
|
||||
def _get_default_preferences(self) -> Dict[str, Any]:
|
||||
"""Get default preferences when onboarding data is unavailable."""
|
||||
return {
|
||||
"industry": None,
|
||||
"target_audience": {},
|
||||
"platform_preferences": ["instagram", "linkedin"],
|
||||
"content_preferences": [],
|
||||
"style_preferences": {},
|
||||
"brand_colors": [],
|
||||
"recommended_templates": ["ecommerce_product_shot", "lifestyle_product"],
|
||||
"recommended_channels": ["instagram", "linkedin", "facebook"],
|
||||
"writing_style": {
|
||||
"tone": "professional",
|
||||
"voice": "authoritative",
|
||||
},
|
||||
"brand_values": [],
|
||||
}
|
||||
@@ -10,6 +10,8 @@ from dataclasses import dataclass
|
||||
from services.image_studio.transform_service import TransformStudioService, TransformImageToVideoRequest
|
||||
from services.image_studio.studio_manager import ImageStudioManager
|
||||
from utils.logger_utils import get_service_logger
|
||||
from utils.asset_tracker import save_asset_to_library
|
||||
from services.database import SessionLocal
|
||||
|
||||
logger = get_service_logger("product_marketing.animation")
|
||||
|
||||
@@ -141,6 +143,63 @@ class ProductAnimationService:
|
||||
result["animation_type"] = request.animation_type
|
||||
result["source_module"] = "product_marketing"
|
||||
|
||||
# Save to Asset Library
|
||||
if result.get("file_url") and result.get("filename"):
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Build animation prompt for metadata
|
||||
animation_prompt = self._build_animation_prompt(
|
||||
animation_type=request.animation_type,
|
||||
product_name=request.product_name,
|
||||
product_description=request.product_description,
|
||||
brand_context=request.brand_context,
|
||||
additional_context=request.additional_context
|
||||
)
|
||||
|
||||
asset_id = save_asset_to_library(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
asset_type="video",
|
||||
source_module="product_marketing",
|
||||
filename=result.get("filename"),
|
||||
file_url=result.get("file_url"),
|
||||
file_path=result.get("file_path"),
|
||||
file_size=result.get("file_size"),
|
||||
mime_type="video/mp4",
|
||||
title=f"{request.product_name} - {request.animation_type.title()} Animation",
|
||||
description=f"Product animation: {request.product_description or request.product_name}",
|
||||
prompt=animation_prompt,
|
||||
tags=["product_marketing", "product_animation", request.animation_type, request.resolution],
|
||||
provider=result.get("provider", "wavespeed"),
|
||||
model=result.get("model_name", "alibaba/wan-2.5/image-to-video"),
|
||||
cost=result.get("cost", 0.0),
|
||||
generation_time=result.get("generation_time"),
|
||||
asset_metadata={
|
||||
"product_name": request.product_name,
|
||||
"product_description": request.product_description,
|
||||
"animation_type": request.animation_type,
|
||||
"resolution": request.resolution,
|
||||
"duration": request.duration,
|
||||
"width": result.get("width"),
|
||||
"height": result.get("height"),
|
||||
},
|
||||
)
|
||||
|
||||
if asset_id:
|
||||
logger.info(f"[Product Animation] ✅ Saved animation to Asset Library: ID={asset_id}")
|
||||
else:
|
||||
logger.warning(f"[Product Animation] ⚠️ Asset Library save returned None")
|
||||
|
||||
except Exception as db_error:
|
||||
logger.error(f"[Product Animation] Database error saving to Asset Library: {str(db_error)}", exc_info=True)
|
||||
# Video is saved, but database tracking failed - not critical
|
||||
finally:
|
||||
if db:
|
||||
try:
|
||||
db.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.info(
|
||||
f"[Product Animation] ✅ Product animation completed: "
|
||||
f"cost=${result.get('cost', 0):.2f}, video_url={result.get('video_url', 'N/A')}"
|
||||
|
||||
@@ -14,6 +14,8 @@ import base64
|
||||
from services.image_studio.infinitetalk_adapter import InfiniteTalkService
|
||||
from services.story_writer.audio_generation_service import StoryAudioGenerationService
|
||||
from utils.logger_utils import get_service_logger
|
||||
from utils.asset_tracker import save_asset_to_library
|
||||
from services.database import SessionLocal
|
||||
|
||||
logger = get_service_logger("product_marketing.avatar")
|
||||
|
||||
@@ -271,6 +273,65 @@ class ProductAvatarService:
|
||||
result["file_size"] = file_size
|
||||
result["duration"] = result.get("duration", 0.0)
|
||||
|
||||
# Save to Asset Library
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Build avatar prompt for metadata
|
||||
avatar_prompt = request.prompt
|
||||
if not avatar_prompt:
|
||||
avatar_prompt = self._build_avatar_prompt(
|
||||
explainer_type=request.explainer_type,
|
||||
product_name=request.product_name,
|
||||
product_description=request.product_description,
|
||||
brand_context=request.brand_context,
|
||||
additional_context=request.additional_context
|
||||
)
|
||||
|
||||
asset_id = save_asset_to_library(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
asset_type="video",
|
||||
source_module="product_marketing",
|
||||
filename=filename,
|
||||
file_url=file_url,
|
||||
file_path=str(file_path),
|
||||
file_size=file_size,
|
||||
mime_type="video/mp4",
|
||||
title=f"{request.product_name} - {request.explainer_type.replace('_', ' ').title()} Explainer",
|
||||
description=f"Product explainer: {request.product_description or request.product_name}",
|
||||
prompt=avatar_prompt,
|
||||
tags=["product_marketing", "product_avatar", "explainer", request.explainer_type, request.resolution],
|
||||
provider=result.get("provider", "infinitetalk"),
|
||||
model=result.get("model_name", "infinitetalk"),
|
||||
cost=result.get("cost", 0.0),
|
||||
generation_time=result.get("generation_time"),
|
||||
asset_metadata={
|
||||
"product_name": request.product_name,
|
||||
"product_description": request.product_description,
|
||||
"explainer_type": request.explainer_type,
|
||||
"resolution": request.resolution,
|
||||
"duration": result.get("duration", 0.0),
|
||||
"script_text": request.script_text,
|
||||
"width": result.get("width"),
|
||||
"height": result.get("height"),
|
||||
},
|
||||
)
|
||||
|
||||
if asset_id:
|
||||
logger.info(f"[Product Avatar] ✅ Saved explainer video to Asset Library: ID={asset_id}")
|
||||
else:
|
||||
logger.warning(f"[Product Avatar] ⚠️ Asset Library save returned None")
|
||||
|
||||
except Exception as db_error:
|
||||
logger.error(f"[Product Avatar] Database error saving to Asset Library: {str(db_error)}", exc_info=True)
|
||||
# Video is saved, but database tracking failed - not critical
|
||||
finally:
|
||||
if db:
|
||||
try:
|
||||
db.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.info(
|
||||
f"[Product Avatar] ✅ Product explainer video generated successfully: "
|
||||
f"cost=${result.get('cost', 0):.2f}, duration={result.get('duration', 0):.1f}s, "
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
"""
|
||||
Product Marketing Templates Library
|
||||
Pre-built templates for common product marketing use cases.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class TemplateCategory(str, Enum):
|
||||
"""Template categories."""
|
||||
PRODUCT_IMAGE = "product_image"
|
||||
PRODUCT_VIDEO = "product_video"
|
||||
PRODUCT_AVATAR = "product_avatar"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProductImageTemplate:
|
||||
"""Product image generation template."""
|
||||
id: str
|
||||
name: str
|
||||
category: TemplateCategory
|
||||
description: str
|
||||
environment: str # studio, lifestyle, outdoor, minimalist
|
||||
background_style: str # white, transparent, lifestyle, branded
|
||||
lighting: str # natural, studio, dramatic, soft
|
||||
style: str # photorealistic, minimalist, luxury, technical
|
||||
angle: str # front, side, top, 45_degree, 360
|
||||
use_cases: List[str]
|
||||
prompt_template: Optional[str] = None
|
||||
recommended_resolution: str = "1024x1024"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProductVideoTemplate:
|
||||
"""Product video generation template."""
|
||||
id: str
|
||||
name: str
|
||||
category: TemplateCategory
|
||||
description: str
|
||||
video_type: str # demo, storytelling, feature_highlight, launch
|
||||
resolution: str # 480p, 720p, 1080p
|
||||
duration: int # 5 or 10 seconds
|
||||
use_cases: List[str]
|
||||
prompt_template: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProductAvatarTemplate:
|
||||
"""Product avatar/explainer video template."""
|
||||
id: str
|
||||
name: str
|
||||
category: TemplateCategory
|
||||
description: str
|
||||
explainer_type: str # product_overview, feature_explainer, tutorial, brand_message
|
||||
resolution: str # 480p, 720p
|
||||
use_cases: List[str]
|
||||
script_template: Optional[str] = None
|
||||
prompt_template: Optional[str] = None
|
||||
|
||||
|
||||
class ProductMarketingTemplates:
|
||||
"""Product Marketing template definitions."""
|
||||
|
||||
@classmethod
|
||||
def get_product_image_templates(cls) -> List[ProductImageTemplate]:
|
||||
"""Get all product image templates."""
|
||||
return [
|
||||
ProductImageTemplate(
|
||||
id="ecommerce_product_shot",
|
||||
name="E-commerce Product Shot",
|
||||
category=TemplateCategory.PRODUCT_IMAGE,
|
||||
description="Professional product photography for e-commerce listings. Clean white background, studio lighting, front angle.",
|
||||
environment="studio",
|
||||
background_style="white",
|
||||
lighting="studio",
|
||||
style="photorealistic",
|
||||
angle="front",
|
||||
use_cases=["E-commerce listings", "Product catalogs", "Amazon/Shopify"],
|
||||
prompt_template="{product_name} on white background, professional product photography, studio lighting, clean and minimalist, high quality, e-commerce style",
|
||||
recommended_resolution="1024x1024",
|
||||
),
|
||||
ProductImageTemplate(
|
||||
id="lifestyle_product",
|
||||
name="Lifestyle Product Image",
|
||||
category=TemplateCategory.PRODUCT_IMAGE,
|
||||
description="Product in realistic lifestyle setting. Natural environment, authentic use case.",
|
||||
environment="lifestyle",
|
||||
background_style="lifestyle",
|
||||
lighting="natural",
|
||||
style="photorealistic",
|
||||
angle="45_degree",
|
||||
use_cases=["Social media", "Marketing campaigns", "Brand storytelling"],
|
||||
prompt_template="{product_name} in realistic lifestyle setting, natural environment, authentic use case, relatable scenario, professional photography",
|
||||
recommended_resolution="1024x1024",
|
||||
),
|
||||
ProductImageTemplate(
|
||||
id="luxury_product_showcase",
|
||||
name="Luxury Product Showcase",
|
||||
category=TemplateCategory.PRODUCT_IMAGE,
|
||||
description="Premium product presentation. Dramatic lighting, elegant composition, luxury aesthetic.",
|
||||
environment="studio",
|
||||
background_style="minimalist",
|
||||
lighting="dramatic",
|
||||
style="luxury",
|
||||
angle="45_degree",
|
||||
use_cases=["Premium brands", "Luxury products", "High-end marketing"],
|
||||
prompt_template="{product_name} luxury product showcase, dramatic lighting, elegant composition, premium aesthetic, sophisticated, high-end",
|
||||
recommended_resolution="1024x1024",
|
||||
),
|
||||
ProductImageTemplate(
|
||||
id="technical_product_detail",
|
||||
name="Technical Product Detail",
|
||||
category=TemplateCategory.PRODUCT_IMAGE,
|
||||
description="Technical product photography. Focus on details, specifications, features.",
|
||||
environment="studio",
|
||||
background_style="white",
|
||||
lighting="studio",
|
||||
style="technical",
|
||||
angle="front",
|
||||
use_cases=["Technical products", "Specification sheets", "Product documentation"],
|
||||
prompt_template="{product_name} technical product photography, detailed features visible, clean background, professional technical documentation style",
|
||||
recommended_resolution="1024x1024",
|
||||
),
|
||||
ProductImageTemplate(
|
||||
id="social_media_product",
|
||||
name="Social Media Product Post",
|
||||
category=TemplateCategory.PRODUCT_IMAGE,
|
||||
description="Product image optimized for social media. Eye-catching, shareable, engaging.",
|
||||
environment="lifestyle",
|
||||
background_style="lifestyle",
|
||||
lighting="natural",
|
||||
style="photorealistic",
|
||||
angle="45_degree",
|
||||
use_cases=["Instagram", "Facebook", "TikTok", "Pinterest"],
|
||||
prompt_template="{product_name} social media product post, eye-catching, shareable, engaging, modern aesthetic, social media optimized",
|
||||
recommended_resolution="1024x1024",
|
||||
),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_product_video_templates(cls) -> List[ProductVideoTemplate]:
|
||||
"""Get all product video templates."""
|
||||
return [
|
||||
ProductVideoTemplate(
|
||||
id="product_demo_video",
|
||||
name="Product Demo Video",
|
||||
category=TemplateCategory.PRODUCT_VIDEO,
|
||||
description="Product demonstration video showing product in use, showcasing key features and benefits.",
|
||||
video_type="demo",
|
||||
resolution="720p",
|
||||
duration=10,
|
||||
use_cases=["Product launches", "Feature showcases", "Marketing campaigns"],
|
||||
prompt_template="{product_name} being demonstrated in use, showcasing key features and benefits, professional product demonstration, dynamic camera movement, engaging presentation",
|
||||
),
|
||||
ProductVideoTemplate(
|
||||
id="product_storytelling",
|
||||
name="Product Storytelling Video",
|
||||
category=TemplateCategory.PRODUCT_VIDEO,
|
||||
description="Narrative-driven product showcase. Emotional connection, compelling visual story.",
|
||||
video_type="storytelling",
|
||||
resolution="1080p",
|
||||
duration=10,
|
||||
use_cases=["Brand storytelling", "Emotional marketing", "Campaign videos"],
|
||||
prompt_template="Story of {product_name}, narrative-driven product showcase, emotional connection, cinematic storytelling, compelling visual narrative",
|
||||
),
|
||||
ProductVideoTemplate(
|
||||
id="feature_highlight_video",
|
||||
name="Feature Highlight Video",
|
||||
category=TemplateCategory.PRODUCT_VIDEO,
|
||||
description="Close-up shots highlighting key product features. Feature-focused presentation.",
|
||||
video_type="feature_highlight",
|
||||
resolution="720p",
|
||||
duration=10,
|
||||
use_cases=["Feature announcements", "Product updates", "Technical showcases"],
|
||||
prompt_template="{product_name} highlighting key features, close-up shots of important details, feature-focused presentation, professional product photography",
|
||||
),
|
||||
ProductVideoTemplate(
|
||||
id="product_launch_video",
|
||||
name="Product Launch Video",
|
||||
category=TemplateCategory.PRODUCT_VIDEO,
|
||||
description="Exciting product launch reveal. Dynamic presentation, launch event aesthetic.",
|
||||
video_type="launch",
|
||||
resolution="1080p",
|
||||
duration=10,
|
||||
use_cases=["Product launches", "Announcements", "Launch events"],
|
||||
prompt_template="{product_name} product launch reveal, exciting unveiling, dynamic presentation, professional product showcase, launch event aesthetic",
|
||||
),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_product_avatar_templates(cls) -> List[ProductAvatarTemplate]:
|
||||
"""Get all product avatar/explainer templates."""
|
||||
return [
|
||||
ProductAvatarTemplate(
|
||||
id="product_overview_explainer",
|
||||
name="Product Overview Explainer",
|
||||
category=TemplateCategory.PRODUCT_AVATAR,
|
||||
description="Comprehensive product overview. Engaging and informative presentation.",
|
||||
explainer_type="product_overview",
|
||||
resolution="720p",
|
||||
use_cases=["Product introductions", "Landing pages", "Sales presentations"],
|
||||
script_template="Welcome! Today I'm excited to introduce {product_name}. {product_description}. This innovative product offers [key benefits]. Let me show you what makes it special...",
|
||||
prompt_template="Professional product presentation of {product_name}, engaging and informative, clear communication, confident expression",
|
||||
),
|
||||
ProductAvatarTemplate(
|
||||
id="feature_explainer",
|
||||
name="Feature Explainer Video",
|
||||
category=TemplateCategory.PRODUCT_AVATAR,
|
||||
description="Detailed feature explanation. Pointing gestures, clear visual communication.",
|
||||
explainer_type="feature_explainer",
|
||||
resolution="720p",
|
||||
use_cases=["Feature announcements", "Product tutorials", "How-to guides"],
|
||||
script_template="Let me show you the key features of {product_name}. First, [feature 1] - this allows you to [benefit]. Next, [feature 2] - which enables [benefit]. Finally, [feature 3] - giving you [benefit]...",
|
||||
prompt_template="Demonstrating features of {product_name}, detailed explanation, pointing gestures, clear visual communication",
|
||||
),
|
||||
ProductAvatarTemplate(
|
||||
id="product_tutorial",
|
||||
name="Product Tutorial Video",
|
||||
category=TemplateCategory.PRODUCT_AVATAR,
|
||||
description="Step-by-step product tutorial. Instructional and clear, friendly approach.",
|
||||
explainer_type="tutorial",
|
||||
resolution="720p",
|
||||
use_cases=["User guides", "Onboarding", "Training materials"],
|
||||
script_template="Welcome to this tutorial on {product_name}. Today I'll walk you through how to use it. Step 1: [instruction]. Step 2: [instruction]. Step 3: [instruction]...",
|
||||
prompt_template="Tutorial presentation for {product_name}, step-by-step explanation, instructional and clear, friendly and approachable",
|
||||
),
|
||||
ProductAvatarTemplate(
|
||||
id="brand_message_video",
|
||||
name="Brand Message Video",
|
||||
category=TemplateCategory.PRODUCT_AVATAR,
|
||||
description="Brand message delivery. Authentic and compelling brand storytelling.",
|
||||
explainer_type="brand_message",
|
||||
resolution="720p",
|
||||
use_cases=["Brand campaigns", "Mission statements", "Company values"],
|
||||
script_template="At [Brand Name], we believe in {product_name} because [brand values]. Our mission is [mission statement]. This product represents [brand message]...",
|
||||
prompt_template="Brand message delivery for {product_name}, authentic and compelling, brand storytelling, emotional connection",
|
||||
),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_template_by_id(cls, template_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get a specific template by ID."""
|
||||
# Search in all template types
|
||||
for template in cls.get_product_image_templates():
|
||||
if template.id == template_id:
|
||||
return {
|
||||
"id": template.id,
|
||||
"name": template.name,
|
||||
"category": template.category.value,
|
||||
"description": template.description,
|
||||
"template_data": {
|
||||
"environment": template.environment,
|
||||
"background_style": template.background_style,
|
||||
"lighting": template.lighting,
|
||||
"style": template.style,
|
||||
"angle": template.angle,
|
||||
"recommended_resolution": template.recommended_resolution,
|
||||
},
|
||||
"use_cases": template.use_cases,
|
||||
"prompt_template": template.prompt_template,
|
||||
}
|
||||
|
||||
for template in cls.get_product_video_templates():
|
||||
if template.id == template_id:
|
||||
return {
|
||||
"id": template.id,
|
||||
"name": template.name,
|
||||
"category": template.category.value,
|
||||
"description": template.description,
|
||||
"template_data": {
|
||||
"video_type": template.video_type,
|
||||
"resolution": template.resolution,
|
||||
"duration": template.duration,
|
||||
},
|
||||
"use_cases": template.use_cases,
|
||||
"prompt_template": template.prompt_template,
|
||||
}
|
||||
|
||||
for template in cls.get_product_avatar_templates():
|
||||
if template.id == template_id:
|
||||
return {
|
||||
"id": template.id,
|
||||
"name": template.name,
|
||||
"category": template.category.value,
|
||||
"description": template.description,
|
||||
"template_data": {
|
||||
"explainer_type": template.explainer_type,
|
||||
"resolution": template.resolution,
|
||||
},
|
||||
"use_cases": template.use_cases,
|
||||
"script_template": template.script_template,
|
||||
"prompt_template": template.prompt_template,
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_templates_by_category(cls, category: TemplateCategory) -> List[Dict[str, Any]]:
|
||||
"""Get all templates for a specific category."""
|
||||
if category == TemplateCategory.PRODUCT_IMAGE:
|
||||
return [
|
||||
{
|
||||
"id": t.id,
|
||||
"name": t.name,
|
||||
"description": t.description,
|
||||
"environment": t.environment,
|
||||
"background_style": t.background_style,
|
||||
"lighting": t.lighting,
|
||||
"style": t.style,
|
||||
"angle": t.angle,
|
||||
"use_cases": t.use_cases,
|
||||
"prompt_template": t.prompt_template,
|
||||
"recommended_resolution": t.recommended_resolution,
|
||||
}
|
||||
for t in cls.get_product_image_templates()
|
||||
]
|
||||
elif category == TemplateCategory.PRODUCT_VIDEO:
|
||||
return [
|
||||
{
|
||||
"id": t.id,
|
||||
"name": t.name,
|
||||
"description": t.description,
|
||||
"video_type": t.video_type,
|
||||
"resolution": t.resolution,
|
||||
"duration": t.duration,
|
||||
"use_cases": t.use_cases,
|
||||
"prompt_template": t.prompt_template,
|
||||
}
|
||||
for t in cls.get_product_video_templates()
|
||||
]
|
||||
elif category == TemplateCategory.PRODUCT_AVATAR:
|
||||
return [
|
||||
{
|
||||
"id": t.id,
|
||||
"name": t.name,
|
||||
"description": t.description,
|
||||
"explainer_type": t.explainer_type,
|
||||
"resolution": t.resolution,
|
||||
"use_cases": t.use_cases,
|
||||
"script_template": t.script_template,
|
||||
"prompt_template": t.prompt_template,
|
||||
}
|
||||
for t in cls.get_product_avatar_templates()
|
||||
]
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def apply_template(
|
||||
cls,
|
||||
template_id: str,
|
||||
product_name: str,
|
||||
product_description: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Apply a template to product data.
|
||||
|
||||
Args:
|
||||
template_id: Template ID to apply
|
||||
product_name: Product name
|
||||
product_description: Product description (optional)
|
||||
**kwargs: Additional template-specific parameters
|
||||
|
||||
Returns:
|
||||
Template configuration ready for use
|
||||
"""
|
||||
template = cls.get_template_by_id(template_id)
|
||||
if not template:
|
||||
raise ValueError(f"Template not found: {template_id}")
|
||||
|
||||
# Format prompt/script templates with product data
|
||||
result = template.copy()
|
||||
|
||||
if result.get("prompt_template"):
|
||||
result["prompt"] = result["prompt_template"].format(
|
||||
product_name=product_name,
|
||||
product_description=product_description or product_name,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
if result.get("script_template"):
|
||||
result["script"] = result["script_template"].format(
|
||||
product_name=product_name,
|
||||
product_description=product_description or product_name,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -9,6 +9,8 @@ from dataclasses import dataclass
|
||||
|
||||
from services.llm_providers.main_video_generation import ai_video_generate
|
||||
from utils.logger_utils import get_service_logger
|
||||
from utils.asset_tracker import save_asset_to_library
|
||||
from services.database import SessionLocal
|
||||
|
||||
logger = get_service_logger("product_marketing.video")
|
||||
|
||||
@@ -212,6 +214,62 @@ class ProductVideoService:
|
||||
result["file_url"] = file_url
|
||||
result["file_size"] = len(video_bytes)
|
||||
|
||||
# Save to Asset Library
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Build video prompt for metadata
|
||||
video_prompt = self._build_video_prompt(
|
||||
video_type=request.video_type,
|
||||
product_name=request.product_name,
|
||||
product_description=request.product_description,
|
||||
brand_context=request.brand_context,
|
||||
additional_context=request.additional_context
|
||||
)
|
||||
|
||||
asset_id = save_asset_to_library(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
asset_type="video",
|
||||
source_module="product_marketing",
|
||||
filename=filename,
|
||||
file_url=file_url,
|
||||
file_path=str(file_path),
|
||||
file_size=len(video_bytes),
|
||||
mime_type="video/mp4",
|
||||
title=f"{request.product_name} - {request.video_type.replace('_', ' ').title()} Video",
|
||||
description=f"Product video: {request.product_description or request.product_name}",
|
||||
prompt=video_prompt,
|
||||
tags=["product_marketing", "product_video", request.video_type, request.resolution],
|
||||
provider=result.get("provider", "wavespeed"),
|
||||
model=result.get("model_name", "alibaba/wan-2.5/text-to-video"),
|
||||
cost=result.get("cost", 0.0),
|
||||
generation_time=result.get("generation_time"),
|
||||
asset_metadata={
|
||||
"product_name": request.product_name,
|
||||
"product_description": request.product_description,
|
||||
"video_type": request.video_type,
|
||||
"resolution": request.resolution,
|
||||
"duration": request.duration,
|
||||
"width": result.get("width"),
|
||||
"height": result.get("height"),
|
||||
},
|
||||
)
|
||||
|
||||
if asset_id:
|
||||
logger.info(f"[Product Video] ✅ Saved video to Asset Library: ID={asset_id}")
|
||||
else:
|
||||
logger.warning(f"[Product Video] ⚠️ Asset Library save returned None")
|
||||
|
||||
except Exception as db_error:
|
||||
logger.error(f"[Product Video] Database error saving to Asset Library: {str(db_error)}", exc_info=True)
|
||||
# Video is saved, but database tracking failed - not critical
|
||||
finally:
|
||||
if db:
|
||||
try:
|
||||
db.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.info(
|
||||
f"[Product Video] ✅ Product video generated successfully: "
|
||||
f"cost=${result.get('cost', 0):.2f}, video_url={file_url}"
|
||||
|
||||
Reference in New Issue
Block a user