AI Researcher and Video Studio implementation complete

This commit is contained in:
ajaysi
2026-01-05 15:49:51 +05:30
parent b134e9dc7e
commit 0b63ae7fc1
200 changed files with 39535 additions and 1375 deletions

View File

@@ -7,6 +7,9 @@ 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
__all__ = [
"ProductMarketingOrchestrator",
@@ -16,5 +19,11 @@ __all__ = [
"ChannelPackService",
"CampaignStorageService",
"ProductImageService",
"ProductAnimationService",
"ProductAnimationRequest",
"ProductVideoService",
"ProductVideoRequest",
"ProductAvatarService",
"ProductAvatarRequest",
]

View File

@@ -163,6 +163,7 @@ class ProductMarketingOrchestrator:
"asset_id": asset_node.asset_id,
"asset_type": asset_node.asset_type,
"channel": asset_node.channel,
"campaign_id": blueprint.campaign_id, # Include campaign_id for tracking
"proposed_prompt": enhanced_prompt,
"recommended_template": recommended_template.get('id') if recommended_template else None,
"recommended_provider": recommended_template.get('recommended_provider', 'wavespeed') if recommended_template else 'wavespeed',
@@ -170,6 +171,67 @@ class ProductMarketingOrchestrator:
"concept_summary": self._generate_concept_summary(enhanced_prompt),
}
elif asset_node.asset_type == "video":
# Video asset proposals - determine if animation (image-to-video) or demo (text-to-video)
# Default to animation if we have product image, otherwise demo
video_subtype = asset_proposal.get('video_subtype', 'animation') if 'asset_proposal' in locals() else 'demo'
# For demo videos (text-to-video), we need product description
if video_subtype == "demo" or not product_context or not product_context.get('product_image_base64'):
# Text-to-video demo video
video_type = "demo" # Default, can be customized
if asset_node.channel in ["tiktok", "instagram"]:
video_type = "storytelling" # Storytelling for social media
elif asset_node.channel in ["linkedin", "youtube"]:
video_type = "feature_highlight" # Feature highlights for professional
# Estimate cost for text-to-video (WAN 2.5: $0.05-$0.15/second)
duration = 10 # Default 10s for demo videos
resolution = "720p" # Default
cost_per_second = 0.10 if resolution == "720p" else (0.15 if resolution == "1080p" else 0.05)
cost_estimate = duration * cost_per_second
proposals[asset_node.asset_id] = {
"asset_id": asset_node.asset_id,
"asset_type": asset_node.asset_type,
"video_subtype": "demo", # Text-to-video
"channel": asset_node.channel,
"campaign_id": blueprint.campaign_id,
"video_type": video_type,
"duration": duration,
"resolution": resolution,
"cost_estimate": cost_estimate,
"concept_summary": f"Product {video_type} video optimized for {asset_node.channel}",
"note": "Text-to-video demo - requires product description",
}
else:
# Image-to-video animation
animation_type = "reveal" # Default
if asset_node.channel in ["tiktok", "instagram", "youtube"]:
animation_type = "demo" # Demo animations for social media
elif asset_node.channel in ["linkedin", "facebook"]:
animation_type = "reveal" # Professional reveal for B2B
# Estimate cost for image-to-video (WAN 2.5: $0.05-$0.15/second)
duration = 5 # Default 5s for animations
resolution = "720p" # Default
cost_per_second = 0.10 if resolution == "720p" else (0.15 if resolution == "1080p" else 0.05)
cost_estimate = duration * cost_per_second
proposals[asset_node.asset_id] = {
"asset_id": asset_node.asset_id,
"asset_type": asset_node.asset_type,
"video_subtype": "animation", # Image-to-video
"channel": asset_node.channel,
"campaign_id": blueprint.campaign_id,
"animation_type": animation_type,
"duration": duration,
"resolution": resolution,
"cost_estimate": cost_estimate,
"concept_summary": f"Product {animation_type} animation optimized for {asset_node.channel}",
"note": "Requires product image - will be provided during generation",
}
elif asset_node.asset_type == "text":
base_request = f"Write {asset_node.channel} {asset_node.asset_type} for product launch"
enhanced_prompt = self.prompt_builder.build_marketing_copy_prompt(
@@ -184,6 +246,7 @@ class ProductMarketingOrchestrator:
"asset_id": asset_node.asset_id,
"asset_type": asset_node.asset_type,
"channel": asset_node.channel,
"campaign_id": blueprint.campaign_id, # Include campaign_id for tracking
"proposed_prompt": enhanced_prompt,
"cost_estimate": 0.0, # Text generation cost is minimal
"concept_summary": "Marketing copy optimized for channel and persona",
@@ -242,6 +305,124 @@ class ProductMarketingOrchestrator:
],
}
elif asset_type == "video":
# Check video subtype: "animation" (image-to-video) or "demo" (text-to-video)
video_subtype = asset_proposal.get('video_subtype', 'animation')
if video_subtype == "demo":
# Text-to-video: Product demo video from description
from .product_video_service import ProductVideoService, ProductVideoRequest
# Get product info from context
product_name = product_context.get('product_name', 'Product') if product_context else 'Product'
product_description = product_context.get('product_description', '') if product_context else ''
if not product_description:
raise ValueError("Product description required for text-to-video demo generation")
# Get brand context
brand_dna = self.brand_dna_sync.get_brand_dna_tokens(user_id)
brand_context = {
"visual_identity": brand_dna.get("visual_identity", {}),
"persona": brand_dna.get("persona", {}),
}
# Get video type from proposal or default
video_type = asset_proposal.get('video_type', 'demo')
# Create video service
video_service = ProductVideoService()
# Create video request
video_request = ProductVideoRequest(
product_name=product_name,
product_description=product_description,
video_type=video_type,
resolution=asset_proposal.get('resolution', '720p'),
duration=asset_proposal.get('duration', 10),
audio_base64=asset_proposal.get('audio_base64'),
brand_context=brand_context,
additional_context=asset_proposal.get('additional_context'),
)
# Generate video using unified ai_video_generate()
result = await video_service.generate_product_video(video_request, user_id)
# Extract campaign_id for metadata
campaign_id = asset_proposal.get('campaign_id')
asset_id = asset_proposal.get('asset_id', '')
return {
"success": True,
"asset_type": "video",
"video_subtype": "demo",
"video_url": result.get('file_url'),
"video_filename": result.get('filename'),
"cost": result.get('cost', 0.0),
"video_type": video_type,
"campaign_id": campaign_id,
"asset_id": asset_id,
}
else:
# Image-to-video: Product animation
from .product_animation_service import ProductAnimationService, ProductAnimationRequest
# Get product image from proposal or product context
product_image_base64 = asset_proposal.get('product_image_base64')
if not product_image_base64 and product_context:
product_image_base64 = product_context.get('product_image_base64')
if not product_image_base64:
raise ValueError("Product image required for image-to-video animation generation")
# Get animation type from proposal or default to "reveal"
animation_type = asset_proposal.get('animation_type', 'reveal')
product_name = product_context.get('product_name', 'Product') if product_context else 'Product'
product_description = product_context.get('product_description') if product_context else None
# Get brand context
brand_dna = self.brand_dna_sync.get_brand_dna_tokens(user_id)
brand_context = {
"visual_identity": brand_dna.get("visual_identity", {}),
"persona": brand_dna.get("persona", {}),
}
# Create animation service
animation_service = ProductAnimationService()
# Create animation request
animation_request = ProductAnimationRequest(
product_image_base64=product_image_base64,
animation_type=animation_type,
product_name=product_name,
product_description=product_description,
resolution=asset_proposal.get('resolution', '720p'),
duration=asset_proposal.get('duration', 5),
audio_base64=asset_proposal.get('audio_base64'),
brand_context=brand_context,
additional_context=asset_proposal.get('additional_context'),
)
# Generate video
result = await animation_service.animate_product(animation_request, user_id)
# Extract campaign_id for metadata
campaign_id = asset_proposal.get('campaign_id')
asset_id = asset_proposal.get('asset_id', '')
return {
"success": True,
"asset_type": "video",
"video_subtype": "animation",
"video_url": result.get('video_url'),
"video_filename": result.get('filename'),
"cost": result.get('cost', 0.0),
"animation_type": animation_type,
"campaign_id": campaign_id,
"asset_id": asset_id,
}
elif asset_type == "text":
# Import text generation service and tracker
import asyncio
@@ -457,6 +638,10 @@ Return only the final copy text without explanations or markdown formatting."""
if asset_type == "image":
# Premium quality image: ~5-6 credits
return 5.0
elif asset_type == "video":
# WAN 2.5 Image-to-Video: $0.05-$0.15/second
# Default: 5 seconds at 720p = $0.50
return 0.50
elif asset_type == "text":
return 0.0 # Text generation is typically included
else:

View File

@@ -0,0 +1,221 @@
"""
Product Animation Service
Handles product animation workflows using Transform Studio (WAN 2.5 Image-to-Video).
"""
from typing import Dict, Any, Optional
from loguru import logger
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
logger = get_service_logger("product_marketing.animation")
@dataclass
class ProductAnimationRequest:
"""Request for product animation."""
product_image_base64: str
animation_type: str # "reveal", "rotation", "demo", "lifestyle"
product_name: str
product_description: Optional[str] = None
resolution: str = "720p" # 480p, 720p, 1080p
duration: int = 5 # 5 or 10 seconds
audio_base64: Optional[str] = None
brand_context: Optional[Dict[str, Any]] = None
additional_context: Optional[str] = None
class ProductAnimationService:
"""Service for product animation workflows."""
def __init__(self):
"""Initialize Product Animation Service."""
self.transform_service = TransformStudioService()
self.image_studio = ImageStudioManager()
logger.info("[Product Animation Service] Initialized")
def _build_animation_prompt(
self,
animation_type: str,
product_name: str,
product_description: Optional[str],
brand_context: Optional[Dict[str, Any]],
additional_context: Optional[str]
) -> str:
"""
Build animation prompt based on animation type and product context.
Args:
animation_type: Type of animation (reveal, rotation, demo, lifestyle)
product_name: Product name
product_description: Product description
brand_context: Brand DNA context
additional_context: Additional context
Returns:
Animation prompt
"""
base_prompt = f"{product_name}"
if product_description:
base_prompt += f": {product_description}"
# Animation-specific prompts
animation_prompts = {
"reveal": f"{base_prompt} elegantly revealing, smooth camera movement, professional product showcase, cinematic lighting",
"rotation": f"{base_prompt} slowly rotating 360 degrees, smooth rotation, professional product photography, studio lighting, clean background",
"demo": f"{base_prompt} in use, demonstrating features, dynamic movement, engaging presentation, professional product demo",
"lifestyle": f"{base_prompt} in realistic lifestyle setting, natural environment, authentic use case, relatable scenario",
}
prompt = animation_prompts.get(animation_type, base_prompt)
# Add brand context if available
if brand_context:
visual_identity = brand_context.get("visual_identity", {})
if visual_identity.get("color_palette"):
colors = ", ".join(visual_identity["color_palette"][:3]) # First 3 colors
prompt += f", {colors} color scheme"
if visual_identity.get("style_guidelines"):
style = visual_identity["style_guidelines"].get("aesthetic", "")
if style:
prompt += f", {style} style"
# Add additional context
if additional_context:
prompt += f", {additional_context}"
return prompt
async def animate_product(
self,
request: ProductAnimationRequest,
user_id: str
) -> Dict[str, Any]:
"""
Animate a product image into a video.
Args:
request: Product animation request
user_id: User ID for tracking
Returns:
Animation result with video URL and metadata
"""
try:
logger.info(
f"[Product Animation] Animating product '{request.product_name}' "
f"with type '{request.animation_type}' for user {user_id}"
)
# Build animation prompt
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
)
# Create transform request
transform_request = TransformImageToVideoRequest(
image_base64=request.product_image_base64,
prompt=animation_prompt,
audio_base64=request.audio_base64,
resolution=request.resolution,
duration=request.duration,
enable_prompt_expansion=True, # Expand prompt for better results
)
# Generate video using Transform Studio
result = await self.transform_service.transform_image_to_video(
request=transform_request,
user_id=user_id
)
# Add product-specific metadata
result["product_name"] = request.product_name
result["animation_type"] = request.animation_type
result["source_module"] = "product_marketing"
logger.info(
f"[Product Animation] ✅ Product animation completed: "
f"cost=${result.get('cost', 0):.2f}, video_url={result.get('video_url', 'N/A')}"
)
return result
except Exception as e:
logger.error(f"[Product Animation] ❌ Error animating product: {str(e)}", exc_info=True)
raise
async def create_product_reveal(
self,
product_image_base64: str,
product_name: str,
product_description: Optional[str],
user_id: str,
resolution: str = "720p",
duration: int = 5,
brand_context: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Create product reveal animation."""
request = ProductAnimationRequest(
product_image_base64=product_image_base64,
animation_type="reveal",
product_name=product_name,
product_description=product_description,
resolution=resolution,
duration=duration,
brand_context=brand_context
)
return await self.animate_product(request, user_id)
async def create_product_rotation(
self,
product_image_base64: str,
product_name: str,
product_description: Optional[str],
user_id: str,
resolution: str = "720p",
duration: int = 10, # Longer for full rotation
brand_context: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Create 360° product rotation animation."""
request = ProductAnimationRequest(
product_image_base64=product_image_base64,
animation_type="rotation",
product_name=product_name,
product_description=product_description,
resolution=resolution,
duration=duration,
brand_context=brand_context
)
return await self.animate_product(request, user_id)
async def create_product_demo(
self,
product_image_base64: str,
product_name: str,
product_description: Optional[str],
user_id: str,
resolution: str = "720p",
duration: int = 10,
audio_base64: Optional[str] = None,
brand_context: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Create product demo video."""
request = ProductAnimationRequest(
product_image_base64=product_image_base64,
animation_type="demo",
product_name=product_name,
product_description=product_description,
resolution=resolution,
duration=duration,
audio_base64=audio_base64,
brand_context=brand_context
)
return await self.animate_product(request, user_id)

View File

@@ -0,0 +1,380 @@
"""
Product Avatar Service
Handles product explainer video generation using InfiniteTalk (talking avatars).
"""
from typing import Dict, Any, Optional
from loguru import logger
from dataclasses import dataclass
from pathlib import Path
import uuid
import os
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
logger = get_service_logger("product_marketing.avatar")
@dataclass
class ProductAvatarRequest:
"""Request for product explainer video with talking avatar."""
avatar_image_base64: str # Product image, brand spokesperson, or brand mascot
script_text: Optional[str] = None # Text script to convert to audio
audio_base64: Optional[str] = None # Pre-generated audio (alternative to script_text)
product_name: str = "Product"
product_description: Optional[str] = None
explainer_type: str = "product_overview" # product_overview, feature_explainer, tutorial, brand_message
resolution: str = "720p" # 480p or 720p
prompt: Optional[str] = None # Optional prompt for expression/style
mask_image_base64: Optional[str] = None # Optional mask for animatable regions
seed: Optional[int] = None
brand_context: Optional[Dict[str, Any]] = None
additional_context: Optional[str] = None
class ProductAvatarService:
"""Service for product explainer video generation using InfiniteTalk."""
def __init__(self):
"""Initialize Product Avatar Service."""
self.infinitetalk_service = InfiniteTalkService()
self.audio_service = StoryAudioGenerationService()
logger.info("[Product Avatar Service] Initialized")
def _build_avatar_prompt(
self,
explainer_type: str,
product_name: str,
product_description: Optional[str],
brand_context: Optional[Dict[str, Any]],
additional_context: Optional[str]
) -> str:
"""
Build avatar prompt based on explainer type and product context.
Args:
explainer_type: Type of explainer (product_overview, feature_explainer, tutorial, brand_message)
product_name: Product name
product_description: Product description
brand_context: Brand DNA context
additional_context: Additional context
Returns:
Avatar animation prompt
"""
base_description = f"{product_name}"
if product_description:
base_description += f": {product_description}"
# Explainer type-specific prompts
explainer_prompts = {
"product_overview": (
f"Professional product presentation of {base_description}, "
f"engaging and informative, clear communication, confident expression, "
f"professional setting, modern and clean aesthetic"
),
"feature_explainer": (
f"Demonstrating features of {base_description}, "
f"detailed explanation, pointing gestures, clear visual communication, "
f"educational and informative, professional presentation"
),
"tutorial": (
f"Tutorial presentation for {base_description}, "
f"step-by-step explanation, instructional and clear, "
f"friendly and approachable, educational setting"
),
"brand_message": (
f"Brand message delivery for {base_description}, "
f"authentic and compelling, brand storytelling, "
f"emotional connection, professional brand representation"
),
}
prompt = explainer_prompts.get(explainer_type, base_description)
# Add brand context if available
if brand_context:
visual_identity = brand_context.get("visual_identity", {})
if visual_identity.get("style_guidelines"):
style = visual_identity["style_guidelines"].get("aesthetic", "")
if style:
prompt += f", {style} style"
# Add brand values if available
if visual_identity.get("brand_values"):
values = ", ".join(visual_identity["brand_values"][:2]) # First 2 values
prompt += f", embodying {values}"
# Add additional context
if additional_context:
prompt += f", {additional_context}"
return prompt
def _generate_audio_from_script(
self,
script_text: str,
user_id: str,
output_dir: Path
) -> bytes:
"""
Generate audio from script text using TTS.
Args:
script_text: Text to convert to speech
user_id: User ID for tracking
output_dir: Directory to save temporary audio file
Returns:
Audio bytes
"""
try:
# Create temporary audio file
audio_filename = f"avatar_audio_{uuid.uuid4().hex[:8]}.mp3"
audio_path = output_dir / audio_filename
# Generate audio using gTTS (free, always available)
# Note: For premium voices, we could integrate Minimax voice clone here
success = self.audio_service._generate_audio_gtts(
text=script_text,
output_path=audio_path,
lang="en",
slow=False
)
if not success:
raise RuntimeError("Failed to generate audio from script")
# Read audio bytes
with open(audio_path, 'rb') as f:
audio_bytes = f.read()
# Clean up temporary file
try:
os.remove(audio_path)
except Exception:
pass
logger.info(f"[Product Avatar] Generated audio from script: {len(audio_bytes)} bytes")
return audio_bytes
except Exception as e:
logger.error(f"[Product Avatar] Error generating audio: {str(e)}", exc_info=True)
raise
async def generate_product_explainer(
self,
request: ProductAvatarRequest,
user_id: str
) -> Dict[str, Any]:
"""
Generate product explainer video using InfiniteTalk.
Args:
request: Product avatar request
user_id: User ID for tracking
Returns:
Explainer video result with video URL and metadata
"""
try:
logger.info(
f"[Product Avatar] Generating {request.explainer_type} explainer for product '{request.product_name}' "
f"for user {user_id}"
)
# Prepare audio
audio_base64 = request.audio_base64
if not audio_base64 and request.script_text:
# Generate audio from script
base_dir = Path(__file__).parent.parent.parent.parent
temp_dir = base_dir / "temp_audio"
temp_dir.mkdir(parents=True, exist_ok=True)
audio_bytes = self._generate_audio_from_script(
script_text=request.script_text,
user_id=user_id,
output_dir=temp_dir
)
# Convert to base64
audio_base64 = base64.b64encode(audio_bytes).decode('utf-8')
audio_base64 = f"data:audio/mpeg;base64,{audio_base64}"
if not audio_base64:
raise ValueError("Either audio_base64 or script_text must be provided")
# Build avatar prompt
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
)
# Generate video using InfiniteTalk
result = await self.infinitetalk_service.create_talking_avatar(
image_base64=request.avatar_image_base64,
audio_base64=audio_base64,
resolution=request.resolution,
prompt=avatar_prompt,
mask_image_base64=request.mask_image_base64,
seed=request.seed,
user_id=user_id,
)
# Extract video bytes and save to user directory
video_bytes = result.get("video_bytes")
if not video_bytes:
raise ValueError("Avatar generation returned no video bytes")
# Save video file
base_dir = Path(__file__).parent.parent.parent.parent
output_dir = base_dir / "product_avatars"
output_dir.mkdir(parents=True, exist_ok=True)
# Create user-specific directory
user_dir = output_dir / user_id
user_dir.mkdir(parents=True, exist_ok=True)
# Generate filename
safe_product_name = "".join(c for c in request.product_name if c.isalnum() or c in (' ', '-', '_')).strip()[:30]
filename = f"explainer_{safe_product_name}_{request.explainer_type}_{uuid.uuid4().hex[:8]}.mp4"
filename = filename.replace(" ", "_").replace("/", "_").replace("\\", "_")
# Save file
file_path = user_dir / filename
with open(file_path, 'wb') as f:
f.write(video_bytes)
# Check file size (500MB max)
file_size = os.path.getsize(file_path)
if file_size > 500 * 1024 * 1024:
os.remove(file_path)
raise RuntimeError(f"Video file too large: {file_size / (1024*1024):.2f}MB (max 500MB)")
file_url = f"/api/product-marketing/avatars/{user_id}/{filename}"
# Add product-specific metadata
result["product_name"] = request.product_name
result["explainer_type"] = request.explainer_type
result["source_module"] = "product_marketing"
result["filename"] = filename
result["file_path"] = str(file_path)
result["file_url"] = file_url
result["file_size"] = file_size
result["duration"] = result.get("duration", 0.0)
logger.info(
f"[Product Avatar] ✅ Product explainer video generated successfully: "
f"cost=${result.get('cost', 0):.2f}, duration={result.get('duration', 0):.1f}s, "
f"video_url={file_url}"
)
return result
except Exception as e:
logger.error(f"[Product Avatar] ❌ Error generating product explainer: {str(e)}", exc_info=True)
raise
async def create_product_overview(
self,
avatar_image_base64: str,
script_text: str,
product_name: str,
product_description: Optional[str],
user_id: str,
resolution: str = "720p",
audio_base64: Optional[str] = None,
brand_context: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Create product overview explainer video."""
request = ProductAvatarRequest(
avatar_image_base64=avatar_image_base64,
script_text=script_text,
audio_base64=audio_base64,
product_name=product_name,
product_description=product_description,
explainer_type="product_overview",
resolution=resolution,
brand_context=brand_context
)
return await self.generate_product_explainer(request, user_id)
async def create_feature_explainer(
self,
avatar_image_base64: str,
script_text: str,
product_name: str,
product_description: Optional[str],
user_id: str,
resolution: str = "720p",
audio_base64: Optional[str] = None,
brand_context: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Create product feature explainer video."""
request = ProductAvatarRequest(
avatar_image_base64=avatar_image_base64,
script_text=script_text,
audio_base64=audio_base64,
product_name=product_name,
product_description=product_description,
explainer_type="feature_explainer",
resolution=resolution,
brand_context=brand_context
)
return await self.generate_product_explainer(request, user_id)
async def create_tutorial(
self,
avatar_image_base64: str,
script_text: str,
product_name: str,
product_description: Optional[str],
user_id: str,
resolution: str = "720p",
audio_base64: Optional[str] = None,
brand_context: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Create product tutorial video."""
request = ProductAvatarRequest(
avatar_image_base64=avatar_image_base64,
script_text=script_text,
audio_base64=audio_base64,
product_name=product_name,
product_description=product_description,
explainer_type="tutorial",
resolution=resolution,
brand_context=brand_context
)
return await self.generate_product_explainer(request, user_id)
async def create_brand_message(
self,
avatar_image_base64: str,
script_text: str,
product_name: str,
product_description: Optional[str],
user_id: str,
resolution: str = "720p",
audio_base64: Optional[str] = None,
brand_context: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Create brand message video."""
request = ProductAvatarRequest(
avatar_image_base64=avatar_image_base64,
script_text=script_text,
audio_base64=audio_base64,
product_name=product_name,
product_description=product_description,
explainer_type="brand_message",
resolution=resolution,
brand_context=brand_context
)
return await self.generate_product_explainer(request, user_id)

View File

@@ -0,0 +1,312 @@
"""
Product Video Service
Handles product demo video generation using WAN 2.5 Text-to-Video via main_video_generation.
"""
from typing import Dict, Any, Optional
from loguru import logger
from dataclasses import dataclass
from services.llm_providers.main_video_generation import ai_video_generate
from utils.logger_utils import get_service_logger
logger = get_service_logger("product_marketing.video")
@dataclass
class ProductVideoRequest:
"""Request for product demo video generation."""
product_name: str
product_description: str
video_type: str # "demo", "storytelling", "feature_highlight", "launch"
resolution: str = "720p" # 480p, 720p, 1080p
duration: int = 10 # 5 or 10 seconds
audio_base64: Optional[str] = None
brand_context: Optional[Dict[str, Any]] = None
additional_context: Optional[str] = None
negative_prompt: Optional[str] = None
seed: Optional[int] = None
class ProductVideoService:
"""Service for product demo video generation using WAN 2.5 Text-to-Video."""
def __init__(self):
"""Initialize Product Video Service."""
logger.info("[Product Video Service] Initialized")
def _build_video_prompt(
self,
video_type: str,
product_name: str,
product_description: str,
brand_context: Optional[Dict[str, Any]],
additional_context: Optional[str]
) -> str:
"""
Build video prompt based on video type and product context.
Args:
video_type: Type of video (demo, storytelling, feature_highlight, launch)
product_name: Product name
product_description: Product description
brand_context: Brand DNA context
additional_context: Additional context
Returns:
Video generation prompt
"""
base_description = f"{product_name}"
if product_description:
base_description += f": {product_description}"
# Video type-specific prompts
video_prompts = {
"demo": (
f"{base_description} being demonstrated in use, showcasing key features and benefits, "
f"professional product demonstration, dynamic camera movement, engaging presentation, "
f"clear product visibility, modern and clean aesthetic"
),
"storytelling": (
f"Story of {base_description}, narrative-driven product showcase, emotional connection, "
f"cinematic storytelling, compelling visual narrative, professional cinematography, "
f"engaging product story"
),
"feature_highlight": (
f"{base_description} highlighting key features, close-up shots of important details, "
f"feature-focused presentation, professional product photography, clear feature visibility, "
f"modern and sleek aesthetic"
),
"launch": (
f"{base_description} product launch reveal, exciting unveiling, dynamic presentation, "
f"professional product showcase, launch event aesthetic, engaging and energetic, "
f"modern and premium feel"
),
}
prompt = video_prompts.get(video_type, base_description)
# Add brand context if available
if brand_context:
visual_identity = brand_context.get("visual_identity", {})
if visual_identity.get("color_palette"):
colors = ", ".join(visual_identity["color_palette"][:3]) # First 3 colors
prompt += f", {colors} color scheme"
if visual_identity.get("style_guidelines"):
style = visual_identity["style_guidelines"].get("aesthetic", "")
if style:
prompt += f", {style} style"
# Add brand values if available
if visual_identity.get("brand_values"):
values = ", ".join(visual_identity["brand_values"][:2]) # First 2 values
prompt += f", embodying {values}"
# Add additional context
if additional_context:
prompt += f", {additional_context}"
return prompt
async def generate_product_video(
self,
request: ProductVideoRequest,
user_id: str
) -> Dict[str, Any]:
"""
Generate product demo video using WAN 2.5 Text-to-Video.
This method uses the unified ai_video_generate() entry point which handles:
- Pre-flight validation
- Usage tracking
- Cost tracking
- Error handling
Args:
request: Product video request
user_id: User ID for tracking
Returns:
Video generation result with video URL and metadata
"""
try:
logger.info(
f"[Product Video] Generating {request.video_type} video for product '{request.product_name}' "
f"for user {user_id}"
)
# Build video prompt
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
)
# Build negative prompt (default to avoid common issues)
negative_prompt = request.negative_prompt or (
"blurry, low quality, distorted, deformed, ugly, bad anatomy, "
"watermark, text overlay, logo, signature"
)
# Generate video using unified entry point
# This handles pre-flight validation, usage tracking, and cost tracking automatically
result = await ai_video_generate(
prompt=video_prompt,
operation_type="text-to-video",
provider="wavespeed",
user_id=user_id,
model="alibaba/wan-2.5/text-to-video", # WAN 2.5 Text-to-Video
duration=request.duration,
resolution=request.resolution,
audio_base64=request.audio_base64,
negative_prompt=negative_prompt,
seed=request.seed,
enable_prompt_expansion=True, # Enable prompt optimization
)
# Extract video bytes and save to user directory
video_bytes = result.get("video_bytes")
if not video_bytes:
raise ValueError("Video generation returned no video bytes")
# Save video file (similar to Transform Studio)
from pathlib import Path
import uuid
import os
base_dir = Path(__file__).parent.parent.parent.parent
output_dir = base_dir / "product_videos"
output_dir.mkdir(parents=True, exist_ok=True)
# Create user-specific directory
user_dir = output_dir / user_id
user_dir.mkdir(parents=True, exist_ok=True)
# Generate filename (sanitize to avoid issues)
safe_product_name = "".join(c for c in request.product_name if c.isalnum() or c in (' ', '-', '_')).strip()[:30]
filename = f"product_{safe_product_name}_{request.video_type}_{uuid.uuid4().hex[:8]}.mp4"
filename = filename.replace(" ", "_").replace("/", "_").replace("\\", "_")
# Save file
file_path = user_dir / filename
with open(file_path, 'wb') as f:
f.write(video_bytes)
# Check file size (500MB max)
file_size = os.path.getsize(file_path)
if file_size > 500 * 1024 * 1024:
os.remove(file_path)
raise RuntimeError(f"Video file too large: {file_size / (1024*1024):.2f}MB (max 500MB)")
file_url = f"/api/product-marketing/videos/{user_id}/{filename}"
# Add product-specific metadata
result["product_name"] = request.product_name
result["video_type"] = request.video_type
result["source_module"] = "product_marketing"
result["filename"] = filename
result["file_path"] = str(file_path)
result["file_url"] = file_url
result["file_size"] = len(video_bytes)
logger.info(
f"[Product Video] ✅ Product video generated successfully: "
f"cost=${result.get('cost', 0):.2f}, video_url={file_url}"
)
return result
except Exception as e:
logger.error(f"[Product Video] ❌ Error generating product video: {str(e)}", exc_info=True)
raise
async def create_product_demo(
self,
product_name: str,
product_description: str,
user_id: str,
resolution: str = "720p",
duration: int = 10,
audio_base64: Optional[str] = None,
brand_context: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Create product demo video (product in use, demonstrating features)."""
request = ProductVideoRequest(
product_name=product_name,
product_description=product_description,
video_type="demo",
resolution=resolution,
duration=duration,
audio_base64=audio_base64,
brand_context=brand_context
)
return await self.generate_product_video(request, user_id)
async def create_product_storytelling(
self,
product_name: str,
product_description: str,
user_id: str,
resolution: str = "720p",
duration: int = 10,
audio_base64: Optional[str] = None,
brand_context: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Create product storytelling video (narrative-driven product showcase)."""
request = ProductVideoRequest(
product_name=product_name,
product_description=product_description,
video_type="storytelling",
resolution=resolution,
duration=duration,
audio_base64=audio_base64,
brand_context=brand_context
)
return await self.generate_product_video(request, user_id)
async def create_product_feature_highlight(
self,
product_name: str,
product_description: str,
user_id: str,
resolution: str = "720p",
duration: int = 10,
audio_base64: Optional[str] = None,
brand_context: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Create product feature highlight video (close-up shots of key features)."""
request = ProductVideoRequest(
product_name=product_name,
product_description=product_description,
video_type="feature_highlight",
resolution=resolution,
duration=duration,
audio_base64=audio_base64,
brand_context=brand_context
)
return await self.generate_product_video(request, user_id)
async def create_product_launch(
self,
product_name: str,
product_description: str,
user_id: str,
resolution: str = "1080p", # Higher quality for launch
duration: int = 10,
audio_base64: Optional[str] = None,
brand_context: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Create product launch video (exciting unveiling, launch event aesthetic)."""
request = ProductVideoRequest(
product_name=product_name,
product_description=product_description,
video_type="launch",
resolution=resolution,
duration=duration,
audio_base64=audio_base64,
brand_context=brand_context
)
return await self.generate_product_video(request, user_id)