Files
ALwrity/backend/routers/video_studio/endpoints/edit.py

419 lines
14 KiB
Python

"""
Edit Studio API endpoints.
Phase 1: Basic FFmpeg operations (Trim/Cut, Speed Control, Stabilization)
"""
from typing import Dict, Any, Optional
from fastapi import APIRouter, File, UploadFile, Form, Depends, HTTPException
from sqlalchemy.orm import Session
from backend.middleware.auth import get_current_user, require_authenticated_user
from backend.database.database import get_db
from backend.services.video_studio.edit_service import EditService
router = APIRouter()
@router.post("/edit/trim")
async def trim_video(
file: UploadFile = File(..., description="Video file to trim"),
start_time: float = Form(0.0, description="Start time in seconds"),
end_time: Optional[float] = Form(None, description="End time in seconds (optional)"),
max_duration: Optional[float] = Form(None, description="Maximum duration in seconds (trims if video is longer)"),
trim_mode: str = Form("beginning", description="How to trim if max_duration is set: beginning, middle, end"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Trim video to specified duration or time range.
Supports:
- Trim by start/end time
- Trim to maximum duration
- Trim modes: beginning, middle, end
"""
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 trim_mode
valid_modes = ["beginning", "middle", "end"]
if trim_mode not in valid_modes:
raise HTTPException(
status_code=400,
detail=f"Invalid trim_mode. Must be one of: {', '.join(valid_modes)}"
)
# Initialize service
edit_service = EditService()
# Read video file
video_data = await file.read()
# Trim video
result = await edit_service.trim_video(
video_data=video_data,
start_time=start_time,
end_time=end_time,
max_duration=max_duration,
trim_mode=trim_mode,
user_id=user_id,
)
return result
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Video trimming failed: {str(e)}"
)
@router.post("/edit/speed")
async def adjust_video_speed(
file: UploadFile = File(..., description="Video file to adjust speed"),
speed_factor: float = Form(..., description="Speed multiplier (0.25, 0.5, 1.0, 1.5, 2.0, 4.0)"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Adjust video playback speed.
Supports:
- Slow motion: 0.25x, 0.5x
- Normal: 1.0x
- Fast forward: 1.5x, 2.0x, 4.0x
"""
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 speed factor
if speed_factor <= 0 or speed_factor > 4.0:
raise HTTPException(
status_code=400,
detail="Speed factor must be between 0.25 and 4.0"
)
# Initialize service
edit_service = EditService()
# Read video file
video_data = await file.read()
# Adjust speed
result = await edit_service.adjust_speed(
video_data=video_data,
speed_factor=speed_factor,
user_id=user_id,
)
return result
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Video speed adjustment failed: {str(e)}"
)
@router.post("/edit/stabilize")
async def stabilize_video(
file: UploadFile = File(..., description="Video file to stabilize"),
smoothing: int = Form(10, description="Smoothing window size (1-100, default: 10)"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Stabilize shaky video using FFmpeg's vidstab filters.
Uses two-pass stabilization:
1. Detect camera shake (vidstabdetect)
2. Apply stabilization (vidstabtransform)
Note: Requires FFmpeg with vidstab filters enabled.
"""
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 smoothing
if smoothing < 1 or smoothing > 100:
raise HTTPException(
status_code=400,
detail="Smoothing must be between 1 and 100"
)
# Initialize service
edit_service = EditService()
# Read video file
video_data = await file.read()
# Stabilize video
result = await edit_service.stabilize_video(
video_data=video_data,
smoothing=smoothing,
user_id=user_id,
)
return result
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Video stabilization failed: {str(e)}"
)
@router.post("/edit/estimate-cost")
async def estimate_edit_cost(
edit_type: str = Form(..., description="Type of edit: trim, speed, stabilize, text, volume, normalize, denoise"),
duration: float = Form(10.0, description="Estimated video duration in seconds"),
current_user: Dict[str, Any] = Depends(get_current_user),
) -> Dict[str, Any]:
"""
Estimate cost for video editing operation.
Note: FFmpeg-based operations are free.
AI-based operations will have costs (Phase 3).
"""
try:
require_authenticated_user(current_user)
edit_service = EditService()
estimated_cost = edit_service.calculate_cost(edit_type, duration)
return {
"estimated_cost": estimated_cost,
"edit_type": edit_type,
"estimated_duration": duration,
"pricing_model": "free", # FFmpeg operations are free
"note": "FFmpeg-based editing operations are free. AI-based operations may have costs.",
}
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Cost estimation failed: {str(e)}"
)
# ==================== Phase 2: Text & Audio Endpoints ====================
@router.post("/edit/text")
async def add_text_overlay(
file: UploadFile = File(..., description="Video file to add text overlay"),
text: str = Form(..., description="Text to overlay on video"),
position: str = Form("center", description="Text position: top, center, bottom, top-left, top-right, bottom-left, bottom-right"),
font_size: int = Form(48, description="Font size in pixels"),
font_color: str = Form("white", description="Font color (e.g., white, #FFFFFF)"),
background_color: str = Form("black@0.5", description="Background color with opacity (e.g., black@0.5)"),
start_time: float = Form(0.0, description="When to start showing text (seconds)"),
end_time: Optional[float] = Form(None, description="When to stop showing text (None = end of video)"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Add text overlay to video using FFmpeg drawtext filter.
Supports:
- Multiple positions (center, top, bottom, corners)
- Custom font size and colors
- Background box with opacity
- Time-limited display (show text only during specific time range)
"""
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")
valid_positions = ["top", "center", "bottom", "top-left", "top-right", "bottom-left", "bottom-right"]
if position not in valid_positions:
raise HTTPException(
status_code=400,
detail=f"Invalid position. Must be one of: {', '.join(valid_positions)}"
)
edit_service = EditService()
video_data = await file.read()
result = await edit_service.add_text_overlay(
video_data=video_data,
text=text,
position=position,
font_size=font_size,
font_color=font_color,
background_color=background_color,
start_time=start_time,
end_time=end_time,
user_id=user_id,
)
return result
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Text overlay failed: {str(e)}"
)
@router.post("/edit/volume")
async def adjust_volume(
file: UploadFile = File(..., description="Video file to adjust volume"),
volume_factor: float = Form(..., description="Volume multiplier (0.0 = mute, 1.0 = original, 2.0 = double)"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Adjust video audio volume.
Supports:
- Mute (0.0)
- Reduce volume (0.0 - 1.0)
- Original (1.0)
- Increase volume (1.0 - 3.0+)
"""
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")
if volume_factor < 0:
raise HTTPException(status_code=400, detail="Volume factor must be non-negative")
if volume_factor > 5.0:
raise HTTPException(status_code=400, detail="Volume factor cannot exceed 5.0 to prevent distortion")
edit_service = EditService()
video_data = await file.read()
result = await edit_service.adjust_volume(
video_data=video_data,
volume_factor=volume_factor,
user_id=user_id,
)
return result
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Volume adjustment failed: {str(e)}"
)
@router.post("/edit/normalize")
async def normalize_audio(
file: UploadFile = File(..., description="Video file to normalize audio"),
target_level: float = Form(-14.0, description="Target integrated loudness in LUFS (default: -14 for streaming)"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Normalize audio levels using EBU R128 standard (loudnorm filter).
Common target levels:
- -14 LUFS: YouTube, Spotify, general streaming
- -16 LUFS: Podcast standard
- -23 LUFS: Broadcast TV (EBU R128)
- -24 LUFS: US Broadcast (ATSC A/85)
"""
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")
if target_level > 0 or target_level < -50:
raise HTTPException(
status_code=400,
detail="Target level must be between -50 and 0 LUFS"
)
edit_service = EditService()
video_data = await file.read()
result = await edit_service.normalize_audio(
video_data=video_data,
target_level=target_level,
user_id=user_id,
)
return result
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Audio normalization failed: {str(e)}"
)
@router.post("/edit/denoise")
async def reduce_noise(
file: UploadFile = File(..., description="Video file to reduce audio noise"),
strength: float = Form(0.5, description="Noise reduction strength (0.0 - 1.0)"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""
Reduce audio noise using FFmpeg's noise reduction filters.
Supports:
- Light noise reduction (0.0 - 0.3): Subtle cleanup
- Moderate reduction (0.3 - 0.6): Good for background noise
- Strong reduction (0.6 - 1.0): Heavy noise, may affect audio quality
"""
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")
if strength < 0 or strength > 1:
raise HTTPException(
status_code=400,
detail="Strength must be between 0.0 and 1.0"
)
edit_service = EditService()
video_data = await file.read()
result = await edit_service.reduce_noise(
video_data=video_data,
noise_reduction_strength=strength,
user_id=user_id,
)
return result
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Noise reduction failed: {str(e)}"
)