Base code

This commit is contained in:
Kunthawat Greethong
2026-01-08 22:39:53 +07:00
parent 697115c61a
commit c35fa52117
2169 changed files with 626670 additions and 0 deletions

View File

@@ -0,0 +1 @@
"""Video Studio endpoint modules."""

View File

@@ -0,0 +1,159 @@
"""
Add Audio to Video endpoints.
"""
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, BackgroundTasks
from sqlalchemy.orm import Session
from typing import Optional, Dict, Any
import uuid
from ...database import get_db
from ...models.content_asset_models import AssetSource, AssetType
from ...services.video_studio.add_audio_to_video_service import AddAudioToVideoService
from ...services.asset_service import ContentAssetService
from ...utils.auth import get_current_user, require_authenticated_user
from ...utils.logger_utils import get_service_logger
logger = get_service_logger("video_studio.endpoints.add_audio_to_video")
router = APIRouter()
@router.post("/add-audio-to-video")
async def add_audio_to_video(
background_tasks: BackgroundTasks,
video_file: UploadFile = File(..., description="Source video for audio addition"),
model: str = Form("hunyuan-video-foley", description="AI model to use: 'hunyuan-video-foley' or 'think-sound'"),
prompt: Optional[str] = Form(None, description="Optional text prompt describing desired sounds (Hunyuan Video Foley)"),
seed: Optional[int] = Form(None, description="Random seed for reproducibility (-1 for random)"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Add audio to video using AI models.
Supports:
1. Hunyuan Video Foley - Generate realistic Foley and ambient audio from video
- Optional text prompt to describe desired sounds
- Seed control for reproducibility
2. Think Sound - (To be added)
Args:
video_file: Source video file
model: AI model to use
prompt: Optional text prompt describing desired sounds
seed: Random seed for reproducibility
"""
try:
user_id = require_authenticated_user(current_user)
if not video_file.content_type.startswith('video/'):
raise HTTPException(status_code=400, detail="File must be a video")
# Initialize services
add_audio_service = AddAudioToVideoService()
asset_service = ContentAssetService(db)
logger.info(f"[AddAudioToVideo] Audio addition request: user={user_id}, model={model}, has_prompt={prompt is not None}")
# Read video file
video_data = await video_file.read()
# Add audio to video
result = await add_audio_service.add_audio(
video_data=video_data,
model=model,
prompt=prompt,
seed=seed,
user_id=user_id,
)
if not result.get("success"):
raise HTTPException(
status_code=500,
detail=f"Adding audio failed: {result.get('error', 'Unknown error')}"
)
# Store processed video in asset library
video_url = result.get("video_url")
if video_url:
asset_metadata = {
"original_file": video_file.filename,
"model": result.get("model_used", model),
"has_prompt": prompt is not None,
"prompt": prompt,
"generation_type": "add_audio",
}
asset_service.create_asset(
user_id=user_id,
filename=f"audio_added_{uuid.uuid4().hex[:8]}.mp4",
file_url=video_url,
asset_type=AssetType.VIDEO,
source_module=AssetSource.VIDEO_STUDIO,
asset_metadata=asset_metadata,
cost=result.get("cost", 0),
tags=["video_studio", "audio_addition", "ai-processed"]
)
logger.info(f"[AddAudioToVideo] Audio addition successful: user={user_id}, url={video_url}")
return {
"success": True,
"video_url": video_url,
"cost": result.get("cost", 0),
"model_used": result.get("model_used", model),
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[AddAudioToVideo] Audio addition error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Adding audio failed: {str(e)}")
@router.post("/add-audio-to-video/estimate-cost")
async def estimate_add_audio_cost(
model: str = Form("hunyuan-video-foley", description="AI model to use"),
estimated_duration: float = Form(10.0, description="Estimated video duration in seconds", ge=0.0),
current_user: Dict[str, Any] = Depends(get_current_user),
) -> Dict[str, Any]:
"""
Estimate cost for adding audio to video operation.
Returns estimated cost based on model and duration.
"""
try:
require_authenticated_user(current_user)
add_audio_service = AddAudioToVideoService()
estimated_cost = add_audio_service.calculate_cost(model, estimated_duration)
# Build response based on model pricing
if model == "think-sound":
return {
"estimated_cost": estimated_cost,
"model": model,
"estimated_duration": estimated_duration,
"pricing_model": "per_video",
"flat_rate": 0.05,
}
else:
# Hunyuan Video Foley (per-second pricing)
return {
"estimated_cost": estimated_cost,
"model": model,
"estimated_duration": estimated_duration,
"cost_per_second": 0.02, # Estimated pricing
"pricing_model": "per_second",
"min_duration": 5.0,
"max_duration": 600.0, # 10 minutes max
"min_charge": 0.02 * 5.0,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[AddAudioToVideo] Failed to estimate cost: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to estimate cost: {str(e)}")

View File

@@ -0,0 +1,293 @@
"""
Avatar generation endpoints.
"""
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, BackgroundTasks
from sqlalchemy.orm import Session
from typing import Optional, Dict, Any
import base64
import uuid
from ...database import get_db
from ...models.content_asset_models import AssetSource, AssetType
from ...services.video_studio import VideoStudioService
from ...services.video_studio.avatar_service import AvatarStudioService
from ...services.asset_service import ContentAssetService
from ...utils.auth import get_current_user, require_authenticated_user
from ...utils.logger_utils import get_service_logger
from api.story_writer.task_manager import task_manager
from ..tasks.avatar_generation import execute_avatar_generation_task
logger = get_service_logger("video_studio.endpoints.avatar")
router = APIRouter()
@router.post("/avatars")
async def generate_avatar_video(
background_tasks: BackgroundTasks,
avatar_file: UploadFile = File(..., description="Avatar/face image"),
audio_file: Optional[UploadFile] = File(None, description="Audio file for lip sync"),
video_file: Optional[UploadFile] = File(None, description="Source video for face swap"),
text: Optional[str] = Form(None, description="Text to speak (alternative to audio)"),
language: str = Form("en", description="Language for text-to-speech"),
provider: str = Form("wavespeed", description="AI provider to use"),
model: str = Form("wavespeed/mocha", description="Specific AI model to use"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Generate talking avatar video or perform face swap.
Supports both text-to-speech and audio input for natural lip sync.
"""
try:
user_id = require_authenticated_user(current_user)
# Validate inputs
if not avatar_file.content_type.startswith('image/'):
raise HTTPException(status_code=400, detail="Avatar file must be an image")
if not any([audio_file, video_file, text]):
raise HTTPException(status_code=400, detail="Must provide audio file, video file, or text")
# Initialize services
video_service = VideoStudioService()
asset_service = ContentAssetService(db)
logger.info(f"[VideoStudio] Avatar generation request: user={user_id}, model={model}")
# Read files
avatar_data = await avatar_file.read()
audio_data = await audio_file.read() if audio_file else None
video_data = await video_file.read() if video_file else None
# Generate avatar video
result = await video_service.generate_avatar_video(
avatar_data=avatar_data,
audio_data=audio_data,
video_data=video_data,
text=text,
language=language,
provider=provider,
model=model,
user_id=user_id,
)
if not result.get("success"):
raise HTTPException(
status_code=500,
detail=f"Avatar generation failed: {result.get('error', 'Unknown error')}"
)
# Store in asset library if successful
video_url = result.get("video_url")
if video_url:
asset_metadata = {
"avatar_file": avatar_file.filename,
"audio_file": audio_file.filename if audio_file else None,
"video_file": video_file.filename if video_file else None,
"text": text,
"language": language,
"provider": provider,
"model": model,
"generation_type": "avatar",
}
asset_service.create_asset(
user_id=user_id,
filename=f"avatar_{uuid.uuid4().hex[:8]}.mp4",
file_url=video_url,
asset_type=AssetType.VIDEO,
source_module=AssetSource.VIDEO_STUDIO,
asset_metadata=asset_metadata,
cost=result.get("cost", 0),
tags=["video_studio", "avatar", "ai-generated"]
)
logger.info(f"[VideoStudio] Avatar generation successful: user={user_id}, url={video_url}")
return {
"success": True,
"video_url": video_url,
"cost": result.get("cost", 0),
"model_used": model,
"provider": provider,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[VideoStudio] Avatar generation error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Avatar generation failed: {str(e)}")
@router.post("/avatar/create-async")
async def create_avatar_async(
background_tasks: BackgroundTasks,
image: UploadFile = File(..., description="Image file for avatar"),
audio: UploadFile = File(..., description="Audio file for lip-sync"),
resolution: str = Form("720p", description="Video resolution (480p or 720p)"),
prompt: Optional[str] = Form(None, description="Optional prompt for expression/style"),
mask_image: Optional[UploadFile] = File(None, description="Optional mask image (InfiniteTalk only)"),
seed: Optional[int] = Form(None, description="Optional random seed"),
model: str = Form("infinitetalk", description="Model to use: 'infinitetalk' or 'hunyuan-avatar'"),
current_user: Dict[str, Any] = Depends(get_current_user),
) -> Dict[str, Any]:
"""
Create talking avatar asynchronously with polling support.
Upload a photo and audio to create a talking avatar with perfect lip-sync.
Supports resolutions of 480p and 720p.
- InfiniteTalk: up to 10 minutes long
- Hunyuan Avatar: up to 2 minutes (120 seconds) long
Returns task_id for polling. Frontend can poll /api/video-studio/task/{task_id}/status
to get progress updates and final result.
"""
try:
user_id = require_authenticated_user(current_user)
# Validate resolution
if resolution not in ["480p", "720p"]:
raise HTTPException(
status_code=400,
detail="Resolution must be '480p' or '720p'"
)
# Read image data
image_data = await image.read()
if len(image_data) == 0:
raise HTTPException(status_code=400, detail="Image file is empty")
# Read audio data
audio_data = await audio.read()
if len(audio_data) == 0:
raise HTTPException(status_code=400, detail="Audio file is empty")
# Convert to base64
image_base64 = base64.b64encode(image_data).decode('utf-8')
# Add data URI prefix
image_mime = image.content_type or "image/png"
image_base64 = f"data:{image_mime};base64,{image_base64}"
audio_base64 = base64.b64encode(audio_data).decode('utf-8')
audio_mime = audio.content_type or "audio/mpeg"
audio_base64 = f"data:{audio_mime};base64,{audio_base64}"
# Handle optional mask image
mask_image_base64 = None
if mask_image:
mask_data = await mask_image.read()
if len(mask_data) > 0:
mask_base64 = base64.b64encode(mask_data).decode('utf-8')
mask_mime = mask_image.content_type or "image/png"
mask_image_base64 = f"data:{mask_mime};base64,{mask_base64}"
# Create task
task_id = task_manager.create_task("avatar_generation")
# Validate model
if model not in ["infinitetalk", "hunyuan-avatar"]:
raise HTTPException(
status_code=400,
detail="Model must be 'infinitetalk' or 'hunyuan-avatar'"
)
# Start background task
background_tasks.add_task(
execute_avatar_generation_task,
task_id=task_id,
user_id=user_id,
image_base64=image_base64,
audio_base64=audio_base64,
resolution=resolution,
prompt=prompt,
mask_image_base64=mask_image_base64,
seed=seed,
model=model,
)
logger.info(f"[AvatarStudio] Started async avatar generation: task_id={task_id}, user={user_id}")
return {
"task_id": task_id,
"status": "pending",
"message": f"Avatar generation started. This may take several minutes. Poll /api/video-studio/task/{task_id}/status for updates."
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[AvatarStudio] Failed to start async avatar generation: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to start avatar generation: {str(e)}")
@router.post("/avatar/estimate-cost")
async def estimate_avatar_cost(
resolution: str = Form("720p", description="Video resolution (480p or 720p)"),
estimated_duration: float = Form(10.0, description="Estimated video duration in seconds", ge=5.0, le=600.0),
model: str = Form("infinitetalk", description="Model to use: 'infinitetalk' or 'hunyuan-avatar'"),
current_user: Dict[str, Any] = Depends(get_current_user),
) -> Dict[str, Any]:
"""
Estimate cost for talking avatar generation.
Returns estimated cost based on resolution, duration, and model.
"""
try:
require_authenticated_user(current_user)
# Validate resolution
if resolution not in ["480p", "720p"]:
raise HTTPException(
status_code=400,
detail="Resolution must be '480p' or '720p'"
)
# Validate model
if model not in ["infinitetalk", "hunyuan-avatar"]:
raise HTTPException(
status_code=400,
detail="Model must be 'infinitetalk' or 'hunyuan-avatar'"
)
# Validate duration for Hunyuan Avatar (max 120 seconds)
if model == "hunyuan-avatar" and estimated_duration > 120:
raise HTTPException(
status_code=400,
detail="Hunyuan Avatar supports maximum 120 seconds (2 minutes)"
)
avatar_service = AvatarStudioService()
estimated_cost = avatar_service.calculate_cost_estimate(resolution, estimated_duration, model)
# Return pricing info based on model
if model == "hunyuan-avatar":
cost_per_5_seconds = 0.15 if resolution == "480p" else 0.30
return {
"estimated_cost": estimated_cost,
"resolution": resolution,
"estimated_duration": estimated_duration,
"model": model,
"cost_per_5_seconds": cost_per_5_seconds,
"pricing_model": "per_5_seconds",
"max_duration": 120,
}
else:
cost_per_second = 0.03 if resolution == "480p" else 0.06
return {
"estimated_cost": estimated_cost,
"resolution": resolution,
"estimated_duration": estimated_duration,
"model": model,
"cost_per_second": cost_per_second,
"pricing_model": "per_second",
"max_duration": 600,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[AvatarStudio] Failed to estimate cost: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to estimate cost: {str(e)}")

View File

@@ -0,0 +1,304 @@
"""
Create video endpoints: text-to-video and image-to-video generation.
"""
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, BackgroundTasks
from sqlalchemy.orm import Session
from typing import Optional, Dict, Any
import uuid
from ...database import get_db
from ...models.content_asset_models import AssetSource, AssetType
from ...services.video_studio import VideoStudioService
from ...services.asset_service import ContentAssetService
from ...utils.auth import get_current_user, require_authenticated_user
from ...utils.logger_utils import get_service_logger
from api.story_writer.task_manager import task_manager
from ..tasks.video_generation import execute_video_generation_task
logger = get_service_logger("video_studio.endpoints.create")
router = APIRouter()
@router.post("/generate")
async def generate_video(
background_tasks: BackgroundTasks,
prompt: str = Form(..., description="Text description for video generation"),
negative_prompt: Optional[str] = Form(None, description="What to avoid in the video"),
duration: int = Form(5, description="Video duration in seconds", ge=1, le=10),
resolution: str = Form("720p", description="Video resolution"),
aspect_ratio: str = Form("16:9", description="Video aspect ratio"),
motion_preset: str = Form("medium", description="Motion intensity"),
provider: str = Form("wavespeed", description="AI provider to use"),
model: str = Form("hunyuan-video-1.5", description="Specific AI model to use"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Generate video from text description using AI models.
Supports multiple providers and models for optimal quality and cost.
"""
try:
user_id = require_authenticated_user(current_user)
# Initialize services
video_service = VideoStudioService()
asset_service = ContentAssetService(db)
logger.info(f"[VideoStudio] Text-to-video request: user={user_id}, model={model}, duration={duration}s")
# Generate video
result = await video_service.generate_text_to_video(
prompt=prompt,
negative_prompt=negative_prompt,
duration=duration,
resolution=resolution,
aspect_ratio=aspect_ratio,
motion_preset=motion_preset,
provider=provider,
model=model,
user_id=user_id,
)
if not result.get("success"):
raise HTTPException(
status_code=500,
detail=f"Video generation failed: {result.get('error', 'Unknown error')}"
)
# Store in asset library if successful
video_url = result.get("video_url")
if video_url:
asset_metadata = {
"prompt": prompt,
"negative_prompt": negative_prompt,
"duration": duration,
"resolution": resolution,
"aspect_ratio": aspect_ratio,
"motion_preset": motion_preset,
"provider": provider,
"model": model,
"generation_type": "text-to-video",
}
asset_service.create_asset(
user_id=user_id,
filename=f"video_{uuid.uuid4().hex[:8]}.mp4",
file_url=video_url,
asset_type=AssetType.VIDEO,
source_module=AssetSource.VIDEO_STUDIO,
asset_metadata=asset_metadata,
cost=result.get("cost", 0),
tags=["video_studio", "text-to-video", "ai-generated"]
)
logger.info(f"[VideoStudio] Video generated successfully: user={user_id}, url={video_url}")
return {
"success": True,
"video_url": video_url,
"cost": result.get("cost", 0),
"estimated_duration": result.get("estimated_duration", duration),
"model_used": model,
"provider": provider,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[VideoStudio] Text-to-video error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Video generation failed: {str(e)}")
@router.post("/transform")
async def transform_to_video(
background_tasks: BackgroundTasks,
file: UploadFile = File(..., description="Image file to transform"),
prompt: Optional[str] = Form(None, description="Optional text prompt to guide transformation"),
duration: int = Form(5, description="Video duration in seconds", ge=1, le=10),
resolution: str = Form("720p", description="Video resolution"),
aspect_ratio: str = Form("16:9", description="Video aspect ratio"),
motion_preset: str = Form("medium", description="Motion intensity"),
provider: str = Form("wavespeed", description="AI provider to use"),
model: str = Form("alibaba/wan-2.5", description="Specific AI model to use"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Transform image to video using AI models.
Supports various motion presets and durations for dynamic video creation.
"""
try:
user_id = require_authenticated_user(current_user)
# Validate file type
if not file.content_type.startswith('image/'):
raise HTTPException(status_code=400, detail="File must be an image")
# Initialize services
video_service = VideoStudioService()
asset_service = ContentAssetService(db)
logger.info(f"[VideoStudio] Image-to-video request: user={user_id}, model={model}, duration={duration}s")
# Read image file
image_data = await file.read()
# Generate video
result = await video_service.generate_image_to_video(
image_data=image_data,
prompt=prompt,
duration=duration,
resolution=resolution,
aspect_ratio=aspect_ratio,
motion_preset=motion_preset,
provider=provider,
model=model,
user_id=user_id,
)
if not result.get("success"):
raise HTTPException(
status_code=500,
detail=f"Video transformation failed: {result.get('error', 'Unknown error')}"
)
# Store in asset library if successful
video_url = result.get("video_url")
if video_url:
asset_metadata = {
"original_image": file.filename,
"prompt": prompt,
"duration": duration,
"resolution": resolution,
"aspect_ratio": aspect_ratio,
"motion_preset": motion_preset,
"provider": provider,
"model": model,
"generation_type": "image-to-video",
}
asset_service.create_asset(
user_id=user_id,
filename=f"video_{uuid.uuid4().hex[:8]}.mp4",
file_url=video_url,
asset_type=AssetType.VIDEO,
source_module=AssetSource.VIDEO_STUDIO,
asset_metadata=asset_metadata,
cost=result.get("cost", 0),
tags=["video_studio", "image-to-video", "ai-generated"]
)
logger.info(f"[VideoStudio] Video transformation successful: user={user_id}, url={video_url}")
return {
"success": True,
"video_url": video_url,
"cost": result.get("cost", 0),
"estimated_duration": result.get("estimated_duration", duration),
"model_used": model,
"provider": provider,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[VideoStudio] Image-to-video error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Video transformation failed: {str(e)}")
@router.post("/generate-async")
async def generate_video_async(
background_tasks: BackgroundTasks,
prompt: Optional[str] = Form(None, description="Text description for video generation"),
image: Optional[UploadFile] = File(None, description="Image file for image-to-video"),
operation_type: str = Form("text-to-video", description="Operation type: text-to-video or image-to-video"),
negative_prompt: Optional[str] = Form(None, description="What to avoid in the video"),
duration: int = Form(5, description="Video duration in seconds", ge=1, le=10),
resolution: str = Form("720p", description="Video resolution"),
aspect_ratio: str = Form("16:9", description="Video aspect ratio"),
motion_preset: str = Form("medium", description="Motion intensity"),
provider: str = Form("wavespeed", description="AI provider to use"),
model: str = Form("alibaba/wan-2.5", description="Specific AI model to use"),
current_user: Dict[str, Any] = Depends(get_current_user),
) -> Dict[str, Any]:
"""
Generate video asynchronously with polling support.
Returns task_id for polling. Frontend can poll /api/video-studio/task/{task_id}/status
to get progress updates and final result.
"""
try:
user_id = require_authenticated_user(current_user)
# Validate operation type
if operation_type not in ["text-to-video", "image-to-video"]:
raise HTTPException(
status_code=400,
detail=f"Invalid operation_type: {operation_type}. Must be 'text-to-video' or 'image-to-video'"
)
# Validate inputs based on operation type
if operation_type == "text-to-video" and not prompt:
raise HTTPException(
status_code=400,
detail="prompt is required for text-to-video generation"
)
if operation_type == "image-to-video" and not image:
raise HTTPException(
status_code=400,
detail="image file is required for image-to-video generation"
)
# Read image data if provided
image_data = None
if image:
image_data = await image.read()
if len(image_data) == 0:
raise HTTPException(status_code=400, detail="Image file is empty")
# Create task
task_id = task_manager.create_task("video_generation")
# Prepare kwargs
kwargs = {
"duration": duration,
"resolution": resolution,
"model": model,
}
if negative_prompt:
kwargs["negative_prompt"] = negative_prompt
if aspect_ratio:
kwargs["aspect_ratio"] = aspect_ratio
if motion_preset:
kwargs["motion_preset"] = motion_preset
# Start background task
background_tasks.add_task(
execute_video_generation_task,
task_id=task_id,
operation_type=operation_type,
user_id=user_id,
prompt=prompt,
image_data=image_data,
provider=provider,
**kwargs
)
logger.info(f"[VideoStudio] Started async video generation: task_id={task_id}, operation={operation_type}, user={user_id}")
return {
"task_id": task_id,
"status": "pending",
"message": f"Video generation started. This may take several minutes. Poll /api/video-studio/task/{task_id}/status for updates."
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[VideoStudio] Failed to start async video generation: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to start video generation: {str(e)}")

View File

@@ -0,0 +1,157 @@
"""
Video enhancement endpoints.
"""
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, BackgroundTasks
from sqlalchemy.orm import Session
from typing import Optional, Dict, Any
import uuid
from ...database import get_db
from ...models.content_asset_models import AssetSource, AssetType
from ...services.video_studio import VideoStudioService
from ...services.asset_service import ContentAssetService
from ...utils.auth import get_current_user, require_authenticated_user
from ...utils.logger_utils import get_service_logger
logger = get_service_logger("video_studio.endpoints.enhance")
router = APIRouter()
@router.post("/enhance")
async def enhance_video(
background_tasks: BackgroundTasks,
file: UploadFile = File(..., description="Video file to enhance"),
enhancement_type: str = Form(..., description="Type of enhancement: upscale, stabilize, colorize, etc"),
target_resolution: Optional[str] = Form(None, description="Target resolution for upscale"),
provider: str = Form("wavespeed", description="AI provider to use"),
model: str = Form("wavespeed/flashvsr", description="Specific AI model to use"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Enhance existing video using AI models.
Supports upscaling, stabilization, colorization, and other enhancements.
"""
try:
user_id = require_authenticated_user(current_user)
if not file.content_type.startswith('video/'):
raise HTTPException(status_code=400, detail="File must be a video")
# Initialize services
video_service = VideoStudioService()
asset_service = ContentAssetService(db)
logger.info(f"[VideoStudio] Video enhancement request: user={user_id}, type={enhancement_type}, model={model}")
# Read video file
video_data = await file.read()
# Enhance video
result = await video_service.enhance_video(
video_data=video_data,
enhancement_type=enhancement_type,
target_resolution=target_resolution,
provider=provider,
model=model,
user_id=user_id,
)
if not result.get("success"):
raise HTTPException(
status_code=500,
detail=f"Video enhancement failed: {result.get('error', 'Unknown error')}"
)
# Store enhanced version in asset library
video_url = result.get("video_url")
if video_url:
asset_metadata = {
"original_file": file.filename,
"enhancement_type": enhancement_type,
"target_resolution": target_resolution,
"provider": provider,
"model": model,
"generation_type": "enhancement",
}
asset_service.create_asset(
user_id=user_id,
filename=f"enhanced_{uuid.uuid4().hex[:8]}.mp4",
file_url=video_url,
asset_type=AssetType.VIDEO,
source_module=AssetSource.VIDEO_STUDIO,
asset_metadata=asset_metadata,
cost=result.get("cost", 0),
tags=["video_studio", "enhancement", "ai-enhanced"]
)
logger.info(f"[VideoStudio] Video enhancement successful: user={user_id}, url={video_url}")
return {
"success": True,
"video_url": video_url,
"cost": result.get("cost", 0),
"enhancement_type": enhancement_type,
"model_used": model,
"provider": provider,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[VideoStudio] Video enhancement error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Video enhancement failed: {str(e)}")
@router.post("/enhance/estimate-cost")
async def estimate_enhance_cost(
target_resolution: str = Form("1080p", description="Target resolution (720p, 1080p, 2k, 4k)"),
estimated_duration: float = Form(10.0, description="Estimated video duration in seconds", ge=5.0),
current_user: Dict[str, Any] = Depends(get_current_user),
) -> Dict[str, Any]:
"""
Estimate cost for video enhancement operation.
Returns estimated cost based on target resolution and duration.
"""
try:
require_authenticated_user(current_user)
# Validate resolution
if target_resolution not in ("720p", "1080p", "2k", "4k"):
raise HTTPException(
status_code=400,
detail="Target resolution must be '720p', '1080p', '2k', or '4k'"
)
# FlashVSR pricing: $0.06-$0.16 per 5 seconds based on resolution
pricing = {
"720p": 0.06 / 5, # $0.012 per second
"1080p": 0.09 / 5, # $0.018 per second
"2k": 0.12 / 5, # $0.024 per second
"4k": 0.16 / 5, # $0.032 per second
}
cost_per_second = pricing.get(target_resolution.lower(), pricing["1080p"])
estimated_cost = max(5.0, estimated_duration) * cost_per_second # Minimum 5 seconds
return {
"estimated_cost": estimated_cost,
"target_resolution": target_resolution,
"estimated_duration": estimated_duration,
"cost_per_second": cost_per_second,
"pricing_model": "per_second",
"min_duration": 5.0,
"max_duration": 600.0, # 10 minutes max
"min_charge": cost_per_second * 5.0,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[VideoStudio] Failed to estimate cost: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to estimate cost: {str(e)}")

View File

@@ -0,0 +1,158 @@
"""
Video extension endpoints.
"""
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, BackgroundTasks
from sqlalchemy.orm import Session
from typing import Optional, Dict, Any
import uuid
from ...database import get_db
from ...models.content_asset_models import AssetSource, AssetType
from ...services.video_studio import VideoStudioService
from ...services.asset_service import ContentAssetService
from ...utils.auth import get_current_user, require_authenticated_user
from ...utils.logger_utils import get_service_logger
logger = get_service_logger("video_studio.endpoints.extend")
router = APIRouter()
@router.post("/extend")
async def extend_video(
background_tasks: BackgroundTasks,
file: UploadFile = File(..., description="Video file to extend"),
prompt: str = Form(..., description="Text prompt describing how to extend the video"),
model: str = Form("wan-2.5", description="Model to use: 'wan-2.5', 'wan-2.2-spicy', or 'seedance-1.5-pro'"),
audio: Optional[UploadFile] = File(None, description="Optional audio file to guide generation (WAN 2.5 only)"),
negative_prompt: Optional[str] = Form(None, description="Negative prompt (WAN 2.5 only)"),
resolution: str = Form("720p", description="Output resolution: 480p, 720p, or 1080p (1080p WAN 2.5 only)"),
duration: int = Form(5, description="Duration of extended video in seconds (varies by model)"),
enable_prompt_expansion: bool = Form(False, description="Enable prompt optimizer (WAN 2.5 only)"),
generate_audio: bool = Form(True, description="Generate audio for extended video (Seedance 1.5 Pro only)"),
camera_fixed: bool = Form(False, description="Fix camera position (Seedance 1.5 Pro only)"),
seed: Optional[int] = Form(None, description="Random seed for reproducibility"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Extend video duration using WAN 2.5, WAN 2.2 Spicy, or Seedance 1.5 Pro video-extend.
Takes a short video clip and extends it with motion/audio continuity.
"""
try:
user_id = require_authenticated_user(current_user)
if not file.content_type.startswith('video/'):
raise HTTPException(status_code=400, detail="File must be a video")
# Validate model-specific constraints
if model in ("wan-2.2-spicy", "wavespeed-ai/wan-2.2-spicy/video-extend"):
if duration not in [5, 8]:
raise HTTPException(status_code=400, detail="WAN 2.2 Spicy only supports 5 or 8 second durations")
if resolution not in ["480p", "720p"]:
raise HTTPException(status_code=400, detail="WAN 2.2 Spicy only supports 480p or 720p resolution")
if audio:
raise HTTPException(status_code=400, detail="Audio is not supported for WAN 2.2 Spicy")
elif model in ("seedance-1.5-pro", "bytedance/seedance-v1.5-pro/video-extend"):
if duration < 4 or duration > 12:
raise HTTPException(status_code=400, detail="Seedance 1.5 Pro only supports 4-12 second durations")
if resolution not in ["480p", "720p"]:
raise HTTPException(status_code=400, detail="Seedance 1.5 Pro only supports 480p or 720p resolution")
if audio:
raise HTTPException(status_code=400, detail="Audio upload is not supported for Seedance 1.5 Pro (use generate_audio instead)")
else:
# WAN 2.5 validation
if duration < 3 or duration > 10:
raise HTTPException(status_code=400, detail="WAN 2.5 duration must be between 3 and 10 seconds")
if resolution not in ["480p", "720p", "1080p"]:
raise HTTPException(status_code=400, detail="WAN 2.5 resolution must be 480p, 720p, or 1080p")
# Initialize services
video_service = VideoStudioService()
asset_service = ContentAssetService(db)
logger.info(f"[VideoStudio] Video extension request: user={user_id}, model={model}, duration={duration}s, resolution={resolution}")
# Read video file
video_data = await file.read()
# Read audio file if provided (WAN 2.5 only)
audio_data = None
if audio:
if model in ("wan-2.2-spicy", "wavespeed-ai/wan-2.2-spicy/video-extend", "seedance-1.5-pro", "bytedance/seedance-v1.5-pro/video-extend"):
raise HTTPException(status_code=400, detail=f"Audio upload is not supported for {model} model")
if not audio.content_type.startswith('audio/'):
raise HTTPException(status_code=400, detail="Audio file must be an audio file")
# Validate audio file size (max 15MB per documentation)
audio_data = await audio.read()
if len(audio_data) > 15 * 1024 * 1024:
raise HTTPException(status_code=400, detail="Audio file must be less than 15MB")
# Note: Audio duration validation (3-30s) would require parsing the audio file
# This is handled by the API, but we could add it here if needed
# Extend video
result = await video_service.extend_video(
video_data=video_data,
prompt=prompt,
model=model,
audio_data=audio_data,
negative_prompt=negative_prompt,
resolution=resolution,
duration=duration,
enable_prompt_expansion=enable_prompt_expansion,
generate_audio=generate_audio,
camera_fixed=camera_fixed,
seed=seed,
user_id=user_id,
)
if not result.get("success"):
raise HTTPException(
status_code=500,
detail=f"Video extension failed: {result.get('error', 'Unknown error')}"
)
# Store extended version in asset library
video_url = result.get("video_url")
if video_url:
asset_metadata = {
"original_file": file.filename,
"prompt": prompt,
"duration": duration,
"resolution": resolution,
"generation_type": "extend",
"model": result.get("model_used", "alibaba/wan-2.5/video-extend"),
}
asset_service.create_asset(
user_id=user_id,
filename=f"extended_{uuid.uuid4().hex[:8]}.mp4",
file_url=video_url,
asset_type=AssetType.VIDEO,
source_module=AssetSource.VIDEO_STUDIO,
asset_metadata=asset_metadata,
cost=result.get("cost", 0),
tags=["video_studio", "extend", "ai-extended"]
)
logger.info(f"[VideoStudio] Video extension successful: user={user_id}, url={video_url}")
return {
"success": True,
"video_url": video_url,
"cost": result.get("cost", 0),
"duration": duration,
"resolution": resolution,
"model_used": result.get("model_used", "alibaba/wan-2.5/video-extend"),
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[VideoStudio] Video extension error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Video extension failed: {str(e)}")

View File

@@ -0,0 +1,237 @@
"""
Face Swap endpoints.
"""
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, BackgroundTasks
from sqlalchemy.orm import Session
from typing import Optional, Dict, Any
import uuid
from ...database import get_db
from ...models.content_asset_models import AssetSource, AssetType
from ...services.video_studio import VideoStudioService
from ...services.video_studio.face_swap_service import FaceSwapService
from ...services.asset_service import ContentAssetService
from ...utils.auth import get_current_user, require_authenticated_user
from ...utils.logger_utils import get_service_logger
logger = get_service_logger("video_studio.endpoints.face_swap")
router = APIRouter()
@router.post("/face-swap")
async def swap_face(
background_tasks: BackgroundTasks,
image_file: UploadFile = File(..., description="Reference image for character swap"),
video_file: UploadFile = File(..., description="Source video for face swap"),
model: str = Form("mocha", description="AI model to use: 'mocha' or 'video-face-swap'"),
prompt: Optional[str] = Form(None, description="Optional prompt to guide the swap (MoCha only)"),
resolution: str = Form("480p", description="Output resolution for MoCha (480p or 720p)"),
seed: Optional[int] = Form(None, description="Random seed for reproducibility (MoCha only, -1 for random)"),
target_gender: str = Form("all", description="Filter which faces to swap (video-face-swap only: all, female, male)"),
target_index: int = Form(0, description="Select which face to swap (video-face-swap only: 0 = largest)"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Perform face/character swap using MoCha or Video Face Swap.
Supports two models:
1. MoCha (wavespeed-ai/wan-2.1/mocha) - Character replacement with motion preservation
- Resolution: 480p ($0.04/s) or 720p ($0.08/s)
- Max length: 120 seconds
- Features: Prompt guidance, seed control
2. Video Face Swap (wavespeed-ai/video-face-swap) - Simple face swap with multi-face support
- Pricing: $0.01/s
- Max length: 10 minutes (600 seconds)
- Features: Gender filter, face index selection
Requirements:
- Image: Clear reference image (JPG/PNG, avoid WEBP)
- Video: Source video (max 120s for MoCha, max 600s for video-face-swap)
- Minimum charge: 5 seconds for both models
"""
try:
user_id = require_authenticated_user(current_user)
# Validate file types
if not image_file.content_type.startswith('image/'):
raise HTTPException(status_code=400, detail="Image file must be an image")
if not video_file.content_type.startswith('video/'):
raise HTTPException(status_code=400, detail="Video file must be a video")
# Validate resolution
if resolution not in ("480p", "720p"):
raise HTTPException(
status_code=400,
detail="Resolution must be '480p' or '720p'"
)
# Initialize services
face_swap_service = FaceSwapService()
asset_service = ContentAssetService(db)
logger.info(
f"[FaceSwap] Face swap request: user={user_id}, "
f"resolution={resolution}"
)
# Read files
image_data = await image_file.read()
video_data = await video_file.read()
# Validate file sizes
if len(image_data) > 10 * 1024 * 1024: # 10MB
raise HTTPException(status_code=400, detail="Image file must be less than 10MB")
if len(video_data) > 500 * 1024 * 1024: # 500MB
raise HTTPException(status_code=400, detail="Video file must be less than 500MB")
# Perform face swap
result = await face_swap_service.swap_face(
image_data=image_data,
video_data=video_data,
model=model,
prompt=prompt,
resolution=resolution,
seed=seed,
target_gender=target_gender,
target_index=target_index,
user_id=user_id,
)
if not result.get("success"):
raise HTTPException(
status_code=500,
detail=f"Face swap failed: {result.get('error', 'Unknown error')}"
)
# Store in asset library
video_url = result.get("video_url")
if video_url:
model_name = "wavespeed-ai/wan-2.1/mocha" if model == "mocha" else "wavespeed-ai/video-face-swap"
asset_metadata = {
"image_file": image_file.filename,
"video_file": video_file.filename,
"model": model,
"operation_type": "face_swap",
}
if model == "mocha":
asset_metadata.update({
"prompt": prompt,
"resolution": resolution,
"seed": seed,
})
else: # video-face-swap
asset_metadata.update({
"target_gender": target_gender,
"target_index": target_index,
})
asset_service.create_asset(
user_id=user_id,
filename=f"face_swap_{uuid.uuid4().hex[:8]}.mp4",
file_url=video_url,
asset_type=AssetType.VIDEO,
source_module=AssetSource.VIDEO_STUDIO,
asset_metadata=asset_metadata,
cost=result.get("cost", 0),
tags=["video_studio", "face_swap", "ai-generated"],
)
logger.info(f"[FaceSwap] Face swap successful: user={user_id}, url={video_url}")
return {
"success": True,
"video_url": video_url,
"cost": result.get("cost", 0),
"model": model,
"resolution": result.get("resolution"),
"metadata": result.get("metadata", {}),
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[FaceSwap] Face swap error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Face swap failed: {str(e)}")
@router.post("/face-swap/estimate-cost")
async def estimate_face_swap_cost(
model: str = Form("mocha", description="AI model to use: 'mocha' or 'video-face-swap'"),
resolution: str = Form("480p", description="Output resolution for MoCha (480p or 720p)"),
estimated_duration: float = Form(10.0, description="Estimated video duration in seconds", ge=5.0),
current_user: Dict[str, Any] = Depends(get_current_user),
) -> Dict[str, Any]:
"""
Estimate cost for face swap operation.
Returns estimated cost based on model, resolution (for MoCha), and duration.
"""
try:
require_authenticated_user(current_user)
# Validate model
if model not in ("mocha", "video-face-swap"):
raise HTTPException(
status_code=400,
detail="Model must be 'mocha' or 'video-face-swap'"
)
# Validate resolution (only for MoCha)
if model == "mocha":
if resolution not in ("480p", "720p"):
raise HTTPException(
status_code=400,
detail="Resolution must be '480p' or '720p' for MoCha"
)
max_duration = 120.0
else:
max_duration = 600.0 # 10 minutes for video-face-swap
if estimated_duration > max_duration:
raise HTTPException(
status_code=400,
detail=f"Estimated duration must be <= {max_duration} seconds for {model}"
)
face_swap_service = FaceSwapService()
estimated_cost = face_swap_service.calculate_cost(model, resolution if model == "mocha" else None, estimated_duration)
# Pricing info
if model == "mocha":
cost_per_second = 0.04 if resolution == "480p" else 0.08
return {
"estimated_cost": estimated_cost,
"model": model,
"resolution": resolution,
"estimated_duration": estimated_duration,
"cost_per_second": cost_per_second,
"pricing_model": "per_second",
"min_duration": 5.0,
"max_duration": 120.0,
"min_charge": cost_per_second * 5.0,
}
else: # video-face-swap
return {
"estimated_cost": estimated_cost,
"model": model,
"estimated_duration": estimated_duration,
"cost_per_second": 0.01,
"pricing_model": "per_second",
"min_duration": 5.0,
"max_duration": 600.0,
"min_charge": 0.05, # $0.01 * 5 seconds
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[FaceSwap] Failed to estimate cost: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to estimate cost: {str(e)}")

View File

@@ -0,0 +1,82 @@
"""
Model listing and cost estimation endpoints.
"""
from fastapi import APIRouter, Depends, HTTPException
from typing import Optional, Dict, Any
from ...services.video_studio import VideoStudioService
from ...utils.auth import get_current_user, require_authenticated_user
from ...utils.logger_utils import get_service_logger
logger = get_service_logger("video_studio.endpoints.models")
router = APIRouter()
@router.get("/models")
async def list_available_models(
operation_type: Optional[str] = None,
current_user: Dict[str, Any] = Depends(get_current_user),
) -> Dict[str, Any]:
"""
List available AI models for video generation.
Optionally filter by operation type (text-to-video, image-to-video, avatar, enhancement).
"""
try:
user_id = require_authenticated_user(current_user)
video_service = VideoStudioService()
models = video_service.get_available_models(operation_type)
logger.info(f"[VideoStudio] Listed models for user={user_id}, operation={operation_type}")
return {
"success": True,
"models": models,
"operation_type": operation_type,
}
except Exception as e:
logger.error(f"[VideoStudio] Error listing models: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to list models: {str(e)}")
@router.get("/cost-estimate")
async def estimate_cost(
operation_type: str,
duration: Optional[int] = None,
resolution: Optional[str] = None,
model: Optional[str] = None,
current_user: Dict[str, Any] = Depends(get_current_user),
) -> Dict[str, Any]:
"""
Estimate cost for video generation operations.
Provides real-time cost estimates before generation.
"""
try:
user_id = require_authenticated_user(current_user)
video_service = VideoStudioService()
estimate = video_service.estimate_cost(
operation_type=operation_type,
duration=duration,
resolution=resolution,
model=model,
)
logger.info(f"[VideoStudio] Cost estimate for user={user_id}: {estimate}")
return {
"success": True,
"estimate": estimate,
"operation_type": operation_type,
}
except Exception as e:
logger.error(f"[VideoStudio] Error estimating cost: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to estimate cost: {str(e)}")

View File

@@ -0,0 +1,89 @@
"""
Prompt optimization endpoints for Video Studio.
"""
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any
from ...utils.auth import get_current_user, require_authenticated_user
from ...utils.logger_utils import get_service_logger
from services.wavespeed.client import WaveSpeedClient
logger = get_service_logger("video_studio.endpoints.prompt")
router = APIRouter()
class PromptOptimizeRequest(BaseModel):
text: str = Field(..., description="The prompt text to optimize")
mode: Optional[str] = Field(
default="video",
pattern="^(image|video)$",
description="Optimization mode: 'image' or 'video' (default: 'video' for Video Studio)"
)
style: Optional[str] = Field(
default="default",
pattern="^(default|artistic|photographic|technical|anime|realistic)$",
description="Style: 'default', 'artistic', 'photographic', 'technical', 'anime', or 'realistic'"
)
image: Optional[str] = Field(None, description="Base64-encoded image for context (optional)")
class PromptOptimizeResponse(BaseModel):
optimized_prompt: str
success: bool
@router.post("/optimize-prompt")
async def optimize_prompt(
request: PromptOptimizeRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
) -> PromptOptimizeResponse:
"""
Optimize a prompt using WaveSpeed prompt optimizer.
The WaveSpeedAI Prompt Optimizer enhances prompts specifically for image and video
generation workflows. It restructures and enriches your input prompt to improve:
- Visual clarity and composition
- Cinematic framing and lighting
- Camera movement and style consistency
- Motion dynamics for video generation
Produces significantly better outputs across video generation models like FLUX, Wan,
Kling, Veo, Seedance, and more.
"""
try:
user_id = require_authenticated_user(current_user)
if not request.text or not request.text.strip():
raise HTTPException(status_code=400, detail="Prompt text is required")
# Default to "video" mode for Video Studio
mode = request.mode or "video"
style = request.style or "default"
logger.info(f"[VideoStudio] Optimizing prompt for user {user_id} (mode={mode}, style={style})")
client = WaveSpeedClient()
optimized_prompt = client.optimize_prompt(
text=request.text.strip(),
mode=mode,
style=style,
image=request.image, # Optional base64 image
enable_sync_mode=True,
timeout=30
)
logger.info(f"[VideoStudio] Prompt optimized successfully for user {user_id}")
return PromptOptimizeResponse(
optimized_prompt=optimized_prompt,
success=True
)
except HTTPException:
raise
except Exception as exc:
logger.error(f"[VideoStudio] Failed to optimize prompt: {exc}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to optimize prompt: {str(exc)}")

View File

@@ -0,0 +1,74 @@
"""
Video serving endpoints.
"""
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse
from typing import Dict, Any
from pathlib import Path
from ...utils.auth import get_current_user, require_authenticated_user
from ...utils.logger_utils import get_service_logger
logger = get_service_logger("video_studio.endpoints.serve")
router = APIRouter()
@router.get("/videos/{user_id}/{video_filename:path}", summary="Serve Video Studio Video")
async def serve_video_studio_video(
user_id: str,
video_filename: str,
current_user: Dict[str, Any] = Depends(get_current_user),
) -> FileResponse:
"""
Serve a generated Video Studio video file.
Security: Only the video owner can access their videos.
"""
try:
# Verify the requesting user matches the video owner
authenticated_user_id = require_authenticated_user(current_user)
if authenticated_user_id != user_id:
raise HTTPException(
status_code=403,
detail="You can only access your own videos"
)
# Get base directory
base_dir = Path(__file__).parent.parent.parent.parent
video_studio_videos_dir = base_dir / "video_studio_videos"
video_path = video_studio_videos_dir / user_id / video_filename
# Security: Ensure path is within video_studio_videos directory
try:
resolved_path = video_path.resolve()
resolved_base = video_studio_videos_dir.resolve()
if not str(resolved_path).startswith(str(resolved_base)):
raise HTTPException(
status_code=403,
detail="Invalid video path"
)
except (OSError, ValueError) as e:
logger.error(f"[VideoStudio] Path resolution error: {e}")
raise HTTPException(status_code=403, detail="Invalid video path")
# Check if file exists
if not video_path.exists() or not video_path.is_file():
raise HTTPException(
status_code=404,
detail=f"Video not found: {video_filename}"
)
logger.info(f"[VideoStudio] Serving video: {video_path}")
return FileResponse(
path=str(video_path),
media_type="video/mp4",
filename=video_filename,
)
except HTTPException:
raise
except Exception as e:
logger.error(f"[VideoStudio] Failed to serve video: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to serve video: {str(e)}")

View File

@@ -0,0 +1,195 @@
"""
Social Optimizer endpoints.
"""
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, BackgroundTasks
from sqlalchemy.orm import Session
from typing import Optional, Dict, Any, List
import json
from ...database import get_db
from ...models.content_asset_models import AssetSource, AssetType
from ...services.video_studio import VideoStudioService
from ...services.video_studio.social_optimizer_service import (
SocialOptimizerService,
OptimizationOptions,
)
from ...services.asset_service import ContentAssetService
from ...utils.auth import get_current_user, require_authenticated_user
from ...utils.logger_utils import get_service_logger
logger = get_service_logger("video_studio.endpoints.social")
router = APIRouter()
@router.post("/social/optimize")
async def optimize_for_social(
background_tasks: BackgroundTasks,
file: UploadFile = File(..., description="Source video file"),
platforms: str = Form(..., description="Comma-separated list of platforms (instagram,tiktok,youtube,linkedin,facebook,twitter)"),
auto_crop: bool = Form(True, description="Auto-crop to platform aspect ratio"),
generate_thumbnails: bool = Form(True, description="Generate thumbnails"),
compress: bool = Form(True, description="Compress for file size limits"),
trim_mode: str = Form("beginning", description="Trim mode if video exceeds duration (beginning, middle, end)"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Optimize video for multiple social media platforms.
Creates platform-optimized versions with:
- Aspect ratio conversion
- Duration trimming
- File size compression
- Thumbnail generation
Returns optimized videos for each selected platform.
"""
try:
user_id = require_authenticated_user(current_user)
if not file.content_type.startswith('video/'):
raise HTTPException(status_code=400, detail="File must be a video")
# Parse platforms
platform_list = [p.strip().lower() for p in platforms.split(",") if p.strip()]
if not platform_list:
raise HTTPException(status_code=400, detail="At least one platform must be specified")
# Validate platforms
valid_platforms = ["instagram", "tiktok", "youtube", "linkedin", "facebook", "twitter"]
invalid_platforms = [p for p in platform_list if p not in valid_platforms]
if invalid_platforms:
raise HTTPException(
status_code=400,
detail=f"Invalid platforms: {', '.join(invalid_platforms)}. Valid platforms: {', '.join(valid_platforms)}"
)
# Validate trim_mode
valid_trim_modes = ["beginning", "middle", "end"]
if trim_mode not in valid_trim_modes:
raise HTTPException(
status_code=400,
detail=f"Invalid trim_mode. Must be one of: {', '.join(valid_trim_modes)}"
)
# Initialize services
video_service = VideoStudioService()
social_optimizer = SocialOptimizerService()
asset_service = ContentAssetService(db)
logger.info(
f"[SocialOptimizer] Optimization request: "
f"user={user_id}, platforms={platform_list}"
)
# Read video file
video_data = await file.read()
# Create optimization options
options = OptimizationOptions(
auto_crop=auto_crop,
generate_thumbnails=generate_thumbnails,
compress=compress,
trim_mode=trim_mode,
)
# Optimize for platforms
result = await social_optimizer.optimize_for_platforms(
video_bytes=video_data,
platforms=platform_list,
options=options,
user_id=user_id,
video_studio_service=video_service,
)
if not result.get("success"):
raise HTTPException(
status_code=500,
detail=f"Optimization failed: {result.get('errors', 'Unknown error')}"
)
# Store results in asset library
for platform_result in result.get("results", []):
asset_metadata = {
"platform": platform_result["platform"],
"name": platform_result["name"],
"aspect_ratio": platform_result["aspect_ratio"],
"duration": platform_result["duration"],
"file_size": platform_result["file_size"],
"width": platform_result["width"],
"height": platform_result["height"],
"optimization_type": "social_optimizer",
}
asset_service.create_asset(
user_id=user_id,
filename=f"social_{platform_result['platform']}_{platform_result['name'].replace(' ', '_').lower()}.mp4",
file_url=platform_result["video_url"],
asset_type=AssetType.VIDEO,
source_module=AssetSource.VIDEO_STUDIO,
asset_metadata=asset_metadata,
cost=0.0, # Free (FFmpeg processing)
tags=["video_studio", "social_optimizer", platform_result["platform"]],
)
logger.info(
f"[SocialOptimizer] Optimization successful: "
f"user={user_id}, platforms={len(result.get('results', []))}"
)
return {
"success": True,
"results": result.get("results", []),
"errors": result.get("errors", []),
"cost": result.get("cost", 0.0),
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[SocialOptimizer] Optimization error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Optimization failed: {str(e)}")
@router.get("/social/platforms")
async def get_platforms(
current_user: Dict[str, Any] = Depends(get_current_user),
) -> Dict[str, Any]:
"""
Get list of available platforms and their specifications.
"""
try:
require_authenticated_user(current_user)
from ...services.video_studio.platform_specs import (
PLATFORM_SPECS,
Platform,
)
platforms_data = {}
for platform in Platform:
specs = [spec for spec in PLATFORM_SPECS if spec.platform == platform]
platforms_data[platform.value] = [
{
"name": spec.name,
"aspect_ratio": spec.aspect_ratio,
"width": spec.width,
"height": spec.height,
"max_duration": spec.max_duration,
"max_file_size_mb": spec.max_file_size_mb,
"formats": spec.formats,
"description": spec.description,
}
for spec in specs
]
return {
"success": True,
"platforms": platforms_data,
}
except Exception as e:
logger.error(f"[SocialOptimizer] Failed to get platforms: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to get platforms: {str(e)}")

View File

@@ -0,0 +1,40 @@
"""
Async task status endpoints.
"""
from fastapi import APIRouter, Depends, HTTPException
from typing import Dict, Any
from ...utils.auth import get_current_user, require_authenticated_user
from ...utils.logger_utils import get_service_logger
from api.story_writer.task_manager import task_manager
logger = get_service_logger("video_studio.endpoints.tasks")
router = APIRouter()
@router.get("/task/{task_id}/status")
async def get_task_status(
task_id: str,
current_user: Dict[str, Any] = Depends(get_current_user),
) -> Dict[str, Any]:
"""
Poll for video generation task status.
Returns task status, progress, and result when complete.
"""
try:
require_authenticated_user(current_user)
status = task_manager.get_task_status(task_id)
if not status:
raise HTTPException(status_code=404, detail="Task not found or expired")
return status
except HTTPException:
raise
except Exception as e:
logger.error(f"[VideoStudio] Failed to get task status: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to get task status: {str(e)}")

View File

@@ -0,0 +1,144 @@
"""
Video transformation endpoints.
"""
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, BackgroundTasks
from sqlalchemy.orm import Session
from typing import Optional, Dict, Any
import uuid
from ...database import get_db
from ...models.content_asset_models import AssetSource, AssetType
from ...services.video_studio import VideoStudioService
from ...services.asset_service import ContentAssetService
from ...utils.auth import get_current_user, require_authenticated_user
from ...utils.logger_utils import get_service_logger
logger = get_service_logger("video_studio.endpoints.transform")
router = APIRouter()
@router.post("/transform")
async def transform_video(
background_tasks: BackgroundTasks,
file: UploadFile = File(..., description="Video file to transform"),
transform_type: str = Form(..., description="Type of transformation: format, aspect, speed, resolution, compress"),
# Format conversion parameters
output_format: Optional[str] = Form(None, description="Output format for format conversion (mp4, mov, webm, gif)"),
codec: Optional[str] = Form(None, description="Video codec (libx264, libvpx-vp9, etc.)"),
quality: Optional[str] = Form(None, description="Quality preset (high, medium, low)"),
audio_codec: Optional[str] = Form(None, description="Audio codec (aac, mp3, opus, etc.)"),
# Aspect ratio parameters
target_aspect: Optional[str] = Form(None, description="Target aspect ratio (16:9, 9:16, 1:1, 4:5, 21:9)"),
crop_mode: Optional[str] = Form("center", description="Crop mode for aspect conversion (center, letterbox)"),
# Speed parameters
speed_factor: Optional[float] = Form(None, description="Speed multiplier (0.25, 0.5, 1.0, 1.5, 2.0, 4.0)"),
# Resolution parameters
target_resolution: Optional[str] = Form(None, description="Target resolution (480p, 720p, 1080p, 1440p, 4k)"),
maintain_aspect: bool = Form(True, description="Whether to maintain aspect ratio when scaling"),
# Compression parameters
target_size_mb: Optional[float] = Form(None, description="Target file size in MB for compression"),
compress_quality: Optional[str] = Form(None, description="Quality preset for compression (high, medium, low)"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Transform video using FFmpeg/MoviePy (format, aspect, speed, resolution, compression).
Supports:
- Format conversion (MP4, MOV, WebM, GIF)
- Aspect ratio conversion (16:9, 9:16, 1:1, 4:5, 21:9)
- Speed adjustment (0.25x - 4x)
- Resolution scaling (480p - 4K)
- Compression (file size optimization)
"""
try:
user_id = require_authenticated_user(current_user)
if not file.content_type.startswith('video/'):
raise HTTPException(status_code=400, detail="File must be a video")
# Initialize services
video_service = VideoStudioService()
asset_service = ContentAssetService(db)
logger.info(
f"[VideoStudio] Video transformation request: "
f"user={user_id}, type={transform_type}"
)
# Read video file
video_data = await file.read()
# Validate transform type
valid_transform_types = ["format", "aspect", "speed", "resolution", "compress"]
if transform_type not in valid_transform_types:
raise HTTPException(
status_code=400,
detail=f"Invalid transform_type. Must be one of: {', '.join(valid_transform_types)}"
)
# Transform video
result = await video_service.transform_video(
video_data=video_data,
transform_type=transform_type,
user_id=user_id,
output_format=output_format,
codec=codec,
quality=quality,
audio_codec=audio_codec,
target_aspect=target_aspect,
crop_mode=crop_mode,
speed_factor=speed_factor,
target_resolution=target_resolution,
maintain_aspect=maintain_aspect,
target_size_mb=target_size_mb,
compress_quality=compress_quality,
)
if not result.get("success"):
raise HTTPException(
status_code=500,
detail=f"Video transformation failed: {result.get('error', 'Unknown error')}"
)
# Store transformed version in asset library
video_url = result.get("video_url")
if video_url:
asset_metadata = {
"original_file": file.filename,
"transform_type": transform_type,
"output_format": output_format,
"target_aspect": target_aspect,
"speed_factor": speed_factor,
"target_resolution": target_resolution,
"generation_type": "transformation",
}
asset_service.create_asset(
user_id=user_id,
filename=f"transformed_{uuid.uuid4().hex[:8]}.mp4",
file_url=video_url,
asset_type=AssetType.VIDEO,
source_module=AssetSource.VIDEO_STUDIO,
asset_metadata=asset_metadata,
cost=result.get("cost", 0),
tags=["video_studio", "transform", transform_type]
)
logger.info(f"[VideoStudio] Video transformation successful: user={user_id}, url={video_url}")
return {
"success": True,
"video_url": video_url,
"cost": result.get("cost", 0),
"transform_type": transform_type,
"metadata": result.get("metadata", {}),
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[VideoStudio] Video transformation error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Video transformation failed: {str(e)}")

View File

@@ -0,0 +1,146 @@
"""
Video Background Remover endpoints.
"""
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, BackgroundTasks
from sqlalchemy.orm import Session
from typing import Optional, Dict, Any
import uuid
from ...database import get_db
from ...models.content_asset_models import AssetSource, AssetType
from ...services.video_studio.video_background_remover_service import VideoBackgroundRemoverService
from ...services.asset_service import ContentAssetService
from ...utils.auth import get_current_user, require_authenticated_user
from ...utils.logger_utils import get_service_logger
logger = get_service_logger("video_studio.endpoints.video_background_remover")
router = APIRouter()
@router.post("/video-background-remover")
async def remove_background(
background_tasks: BackgroundTasks,
video_file: UploadFile = File(..., description="Source video for background removal"),
background_image_file: Optional[UploadFile] = File(None, description="Optional background image for replacement"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Remove or replace video background using WaveSpeed Video Background Remover.
Features:
- Clean matting and edge-aware blending
- Natural compositing for realistic results
- Optional background image replacement
- Supports videos up to 10 minutes
Args:
video_file: Source video file
background_image_file: Optional replacement background image
"""
try:
user_id = require_authenticated_user(current_user)
if not video_file.content_type.startswith('video/'):
raise HTTPException(status_code=400, detail="File must be a video")
# Initialize services
background_remover_service = VideoBackgroundRemoverService()
asset_service = ContentAssetService(db)
logger.info(f"[VideoBackgroundRemover] Background removal request: user={user_id}, has_background={background_image_file is not None}")
# Read video file
video_data = await video_file.read()
# Read background image if provided
background_image_data = None
if background_image_file:
if not background_image_file.content_type.startswith('image/'):
raise HTTPException(status_code=400, detail="Background file must be an image")
background_image_data = await background_image_file.read()
# Remove/replace background
result = await background_remover_service.remove_background(
video_data=video_data,
background_image_data=background_image_data,
user_id=user_id,
)
if not result.get("success"):
raise HTTPException(
status_code=500,
detail=f"Background removal failed: {result.get('error', 'Unknown error')}"
)
# Store processed video in asset library
video_url = result.get("video_url")
if video_url:
asset_metadata = {
"original_file": video_file.filename,
"has_background_replacement": result.get("has_background_replacement", False),
"background_file": background_image_file.filename if background_image_file else None,
"generation_type": "background_removal",
}
asset_service.create_asset(
user_id=user_id,
filename=f"bg_removed_{uuid.uuid4().hex[:8]}.mp4",
file_url=video_url,
asset_type=AssetType.VIDEO,
source_module=AssetSource.VIDEO_STUDIO,
asset_metadata=asset_metadata,
cost=result.get("cost", 0),
tags=["video_studio", "background_removal", "ai-processed"]
)
logger.info(f"[VideoBackgroundRemover] Background removal successful: user={user_id}, url={video_url}")
return {
"success": True,
"video_url": video_url,
"cost": result.get("cost", 0),
"has_background_replacement": result.get("has_background_replacement", False),
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[VideoBackgroundRemover] Background removal error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Background removal failed: {str(e)}")
@router.post("/video-background-remover/estimate-cost")
async def estimate_background_removal_cost(
estimated_duration: float = Form(10.0, description="Estimated video duration in seconds", ge=5.0),
current_user: Dict[str, Any] = Depends(get_current_user),
) -> Dict[str, Any]:
"""
Estimate cost for video background removal operation.
Returns estimated cost based on duration.
"""
try:
require_authenticated_user(current_user)
background_remover_service = VideoBackgroundRemoverService()
estimated_cost = background_remover_service.calculate_cost(estimated_duration)
return {
"estimated_cost": estimated_cost,
"estimated_duration": estimated_duration,
"cost_per_second": 0.01,
"pricing_model": "per_second",
"min_duration": 0.0,
"max_duration": 600.0, # 10 minutes max
"min_charge": 0.05, # Minimum $0.05 for ≤5 seconds
"max_charge": 6.00, # Maximum $6.00 for 600 seconds
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[VideoBackgroundRemover] Failed to estimate cost: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to estimate cost: {str(e)}")

View File

@@ -0,0 +1,260 @@
"""
Video Translate endpoints.
"""
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, BackgroundTasks
from sqlalchemy.orm import Session
from typing import Optional, Dict, Any
import uuid
from ...database import get_db
from ...models.content_asset_models import AssetSource, AssetType
from ...services.video_studio import VideoStudioService
from ...services.video_studio.video_translate_service import VideoTranslateService
from ...services.asset_service import ContentAssetService
from ...utils.auth import get_current_user, require_authenticated_user
from ...utils.logger_utils import get_service_logger
logger = get_service_logger("video_studio.endpoints.video_translate")
router = APIRouter()
@router.post("/video-translate")
async def translate_video(
background_tasks: BackgroundTasks,
video_file: UploadFile = File(..., description="Source video to translate"),
output_language: str = Form("English", description="Target language for translation"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Translate video to target language using HeyGen Video Translate.
Supports 70+ languages and 175+ dialects. Translates both audio and video
with lip-sync preservation.
Requirements:
- Video: Source video file (MP4, WebM, etc.)
- Output Language: Target language (default: "English")
- Pricing: $0.0375/second
Supported languages include:
- English, Spanish, French, Hindi, Italian, German, Polish, Portuguese
- Chinese, Japanese, Korean, Arabic, Russian, and many more
- Regional variants (e.g., "English (United States)", "Spanish (Mexico)")
"""
try:
user_id = require_authenticated_user(current_user)
# Validate file type
if not video_file.content_type.startswith('video/'):
raise HTTPException(status_code=400, detail="File must be a video")
# Initialize services
video_translate_service = VideoTranslateService()
asset_service = ContentAssetService(db)
logger.info(
f"[VideoTranslate] Video translate request: user={user_id}, "
f"output_language={output_language}"
)
# Read file
video_data = await video_file.read()
# Validate file size (reasonable limit)
if len(video_data) > 500 * 1024 * 1024: # 500MB
raise HTTPException(status_code=400, detail="Video file must be less than 500MB")
# Perform video translation
result = await video_translate_service.translate_video(
video_data=video_data,
output_language=output_language,
user_id=user_id,
)
if not result.get("success"):
raise HTTPException(
status_code=500,
detail=f"Video translation failed: {result.get('error', 'Unknown error')}"
)
# Store in asset library
video_url = result.get("video_url")
if video_url:
asset_metadata = {
"video_file": video_file.filename,
"output_language": output_language,
"operation_type": "video_translate",
"model": "heygen/video-translate",
}
asset_service.create_asset(
user_id=user_id,
filename=f"video_translate_{uuid.uuid4().hex[:8]}.mp4",
file_url=video_url,
asset_type=AssetType.VIDEO,
source_module=AssetSource.VIDEO_STUDIO,
asset_metadata=asset_metadata,
cost=result.get("cost", 0),
tags=["video_studio", "video_translate", "ai-generated"],
)
logger.info(f"[VideoTranslate] Video translate successful: user={user_id}, url={video_url}")
return {
"success": True,
"video_url": video_url,
"cost": result.get("cost", 0),
"output_language": output_language,
"metadata": result.get("metadata", {}),
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[VideoTranslate] Video translate error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Video translation failed: {str(e)}")
@router.post("/video-translate/estimate-cost")
async def estimate_video_translate_cost(
estimated_duration: float = Form(10.0, description="Estimated video duration in seconds", ge=1.0),
current_user: Dict[str, Any] = Depends(get_current_user),
) -> Dict[str, Any]:
"""
Estimate cost for video translation operation.
Returns estimated cost based on duration.
"""
try:
require_authenticated_user(current_user)
video_translate_service = VideoTranslateService()
estimated_cost = video_translate_service.calculate_cost(estimated_duration)
return {
"estimated_cost": estimated_cost,
"estimated_duration": estimated_duration,
"cost_per_second": 0.0375,
"pricing_model": "per_second",
"min_duration": 1.0,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[VideoTranslate] Failed to estimate cost: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to estimate cost: {str(e)}")
@router.get("/video-translate/languages")
async def get_supported_languages(
current_user: Dict[str, Any] = Depends(get_current_user),
) -> Dict[str, Any]:
"""
Get list of supported languages for video translation.
Returns a categorized list of 70+ languages and 175+ dialects.
"""
try:
require_authenticated_user(current_user)
# Common languages (simplified list - full list has 175+ dialects)
languages = [
"English",
"English (United States)",
"English (UK)",
"English (Australia)",
"English (Canada)",
"Spanish",
"Spanish (Spain)",
"Spanish (Mexico)",
"Spanish (Argentina)",
"French",
"French (France)",
"French (Canada)",
"German",
"German (Germany)",
"Italian",
"Italian (Italy)",
"Portuguese",
"Portuguese (Brazil)",
"Portuguese (Portugal)",
"Chinese",
"Chinese (Mandarin, Simplified)",
"Chinese (Cantonese, Traditional)",
"Japanese",
"Japanese (Japan)",
"Korean",
"Korean (Korea)",
"Hindi",
"Hindi (India)",
"Arabic",
"Arabic (Saudi Arabia)",
"Arabic (Egypt)",
"Russian",
"Russian (Russia)",
"Polish",
"Polish (Poland)",
"Dutch",
"Dutch (Netherlands)",
"Turkish",
"Turkish (Türkiye)",
"Thai",
"Thai (Thailand)",
"Vietnamese",
"Vietnamese (Vietnam)",
"Indonesian",
"Indonesian (Indonesia)",
"Malay",
"Malay (Malaysia)",
"Filipino",
"Filipino (Philippines)",
"Bengali (India)",
"Tamil (India)",
"Telugu (India)",
"Marathi (India)",
"Gujarati (India)",
"Kannada (India)",
"Malayalam (India)",
"Urdu (India)",
"Urdu (Pakistan)",
"Swedish",
"Swedish (Sweden)",
"Norwegian Bokmål (Norway)",
"Danish",
"Danish (Denmark)",
"Finnish",
"Finnish (Finland)",
"Greek",
"Greek (Greece)",
"Hebrew (Israel)",
"Czech",
"Czech (Czechia)",
"Romanian",
"Romanian (Romania)",
"Hungarian",
"Hungarian (Hungary)",
"Bulgarian",
"Bulgarian (Bulgaria)",
"Croatian",
"Croatian (Croatia)",
"Ukrainian",
"Ukrainian (Ukraine)",
"English - Your Accent",
"English - American Accent",
]
return {
"languages": sorted(languages),
"total_count": len(languages),
"note": "This is a simplified list. Full API supports 70+ languages and 175+ dialects. See documentation for complete list.",
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[VideoTranslate] Failed to get languages: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to get languages: {str(e)}")