Base code
This commit is contained in:
1
backend/routers/video_studio/endpoints/__init__.py
Normal file
1
backend/routers/video_studio/endpoints/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Video Studio endpoint modules."""
|
||||
159
backend/routers/video_studio/endpoints/add_audio_to_video.py
Normal file
159
backend/routers/video_studio/endpoints/add_audio_to_video.py
Normal 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)}")
|
||||
293
backend/routers/video_studio/endpoints/avatar.py
Normal file
293
backend/routers/video_studio/endpoints/avatar.py
Normal 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)}")
|
||||
304
backend/routers/video_studio/endpoints/create.py
Normal file
304
backend/routers/video_studio/endpoints/create.py
Normal 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)}")
|
||||
157
backend/routers/video_studio/endpoints/enhance.py
Normal file
157
backend/routers/video_studio/endpoints/enhance.py
Normal 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)}")
|
||||
158
backend/routers/video_studio/endpoints/extend.py
Normal file
158
backend/routers/video_studio/endpoints/extend.py
Normal 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)}")
|
||||
237
backend/routers/video_studio/endpoints/face_swap.py
Normal file
237
backend/routers/video_studio/endpoints/face_swap.py
Normal 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)}")
|
||||
82
backend/routers/video_studio/endpoints/models.py
Normal file
82
backend/routers/video_studio/endpoints/models.py
Normal 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)}")
|
||||
89
backend/routers/video_studio/endpoints/prompt.py
Normal file
89
backend/routers/video_studio/endpoints/prompt.py
Normal 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)}")
|
||||
74
backend/routers/video_studio/endpoints/serve.py
Normal file
74
backend/routers/video_studio/endpoints/serve.py
Normal 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)}")
|
||||
195
backend/routers/video_studio/endpoints/social.py
Normal file
195
backend/routers/video_studio/endpoints/social.py
Normal 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)}")
|
||||
40
backend/routers/video_studio/endpoints/tasks.py
Normal file
40
backend/routers/video_studio/endpoints/tasks.py
Normal 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)}")
|
||||
144
backend/routers/video_studio/endpoints/transform.py
Normal file
144
backend/routers/video_studio/endpoints/transform.py
Normal 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)}")
|
||||
@@ -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)}")
|
||||
260
backend/routers/video_studio/endpoints/video_translate.py
Normal file
260
backend/routers/video_studio/endpoints/video_translate.py
Normal 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)}")
|
||||
Reference in New Issue
Block a user