305 lines
12 KiB
Python
305 lines
12 KiB
Python
"""
|
|
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)}")
|