Save local changes (GSC/Bing integrations) before merging PR #354
This commit is contained in:
1
backend/api/video_studio/__init__.py
Normal file
1
backend/api/video_studio/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Video Studio API Module
|
||||
173
backend/api/video_studio/handlers/avatar.py
Normal file
173
backend/api/video_studio/handlers/avatar.py
Normal file
@@ -0,0 +1,173 @@
|
||||
from fastapi import APIRouter, UploadFile, File, Form, BackgroundTasks, HTTPException, Depends
|
||||
from fastapi.responses import FileResponse
|
||||
from typing import Optional, Dict, Any
|
||||
import shutil
|
||||
import os
|
||||
from pathlib import Path
|
||||
from services.wavespeed.infinitetalk import animate_scene_with_voiceover
|
||||
from ..task_manager import task_manager
|
||||
from middleware.auth_middleware import get_current_user
|
||||
from loguru import logger
|
||||
from services.database import get_engine_for_user
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from utils.asset_tracker import save_asset_to_library
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Define storage directory
|
||||
UPLOAD_DIR = Path("backend/data/video_studio/uploads")
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _process_avatar_generation(task_id: str, image_path: Path, audio_path: Path, user_id: str, resolution: str, model: str):
|
||||
"""
|
||||
Background task to process avatar generation using shared InfiniteTalk service.
|
||||
"""
|
||||
try:
|
||||
task_manager.update_task(task_id, "processing", user_id=user_id)
|
||||
|
||||
# Read file bytes
|
||||
with open(image_path, "rb") as f:
|
||||
image_bytes = f.read()
|
||||
with open(audio_path, "rb") as f:
|
||||
audio_bytes = f.read()
|
||||
|
||||
# Dummy scene data required by the service (used for prompt generation)
|
||||
scene_data = {
|
||||
"title": "Test Persona",
|
||||
"description": "A talking avatar video generated via Video Studio."
|
||||
}
|
||||
story_context = {}
|
||||
|
||||
# Call the common interface function
|
||||
logger.info(f"[VideoStudio] Starting InfiniteTalk generation for task {task_id}")
|
||||
result = animate_scene_with_voiceover(
|
||||
image_bytes=image_bytes,
|
||||
audio_bytes=audio_bytes,
|
||||
scene_data=scene_data,
|
||||
story_context=story_context,
|
||||
user_id=user_id,
|
||||
resolution=resolution
|
||||
)
|
||||
|
||||
# Save the resulting video bytes to a file
|
||||
video_filename = f"video_{task_id}.mp4"
|
||||
video_path = UPLOAD_DIR / video_filename
|
||||
with open(video_path, "wb") as f:
|
||||
f.write(result["video_bytes"])
|
||||
|
||||
# Prepare result for frontend (remove raw bytes)
|
||||
result.pop("video_bytes", None)
|
||||
|
||||
# Add local download URL
|
||||
video_url = f"/api/video-studio/download/{video_filename}"
|
||||
result["video_url"] = video_url
|
||||
|
||||
# Save asset to library
|
||||
try:
|
||||
engine = get_engine_for_user(user_id)
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
db = SessionLocal()
|
||||
try:
|
||||
save_asset_to_library(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
asset_type="video",
|
||||
source_module="video_studio",
|
||||
filename=video_filename,
|
||||
file_url=video_url,
|
||||
file_path=str(video_path),
|
||||
file_size=video_path.stat().st_size,
|
||||
mime_type="video/mp4",
|
||||
title=f"Avatar Video {task_id}",
|
||||
description=f"Generated avatar video using {model}",
|
||||
model=model,
|
||||
cost=result.get("cost", 0.0),
|
||||
generation_time=result.get("generation_time", 0.0)
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
logger.error(f"[VideoStudio] Failed to save asset to library: {e}")
|
||||
|
||||
logger.info(f"[VideoStudio] Task {task_id} completed successfully")
|
||||
task_manager.update_task(task_id, "completed", result=result, user_id=user_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[VideoStudio] Avatar generation failed for task {task_id}: {e}", exc_info=True)
|
||||
task_manager.update_task(task_id, "failed", error=str(e), user_id=user_id)
|
||||
finally:
|
||||
# Cleanup temp upload files
|
||||
try:
|
||||
if image_path.exists(): image_path.unlink()
|
||||
if audio_path.exists(): audio_path.unlink()
|
||||
except Exception as e:
|
||||
logger.warning(f"[VideoStudio] Failed to cleanup temp files: {e}")
|
||||
|
||||
@router.post("/avatar/create-async")
|
||||
async def create_avatar_video(
|
||||
background_tasks: BackgroundTasks,
|
||||
image: UploadFile = File(...),
|
||||
audio: UploadFile = File(...),
|
||||
resolution: str = Form("720p"),
|
||||
model: str = Form("infinitetalk"),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Create a talking avatar video using InfiniteTalk (WaveSpeed).
|
||||
Directly uses the common backend service without Podcast Maker dependencies.
|
||||
"""
|
||||
user_id = current_user.get("id", "anonymous")
|
||||
|
||||
# Validate file types roughly
|
||||
if not image.content_type.startswith("image/"):
|
||||
raise HTTPException(status_code=400, detail="Invalid image file type")
|
||||
|
||||
task_id = task_manager.create_task("avatar_generation", user_id=user_id)
|
||||
|
||||
# Generate temp paths
|
||||
image_ext = Path(image.filename).suffix or ".png"
|
||||
audio_ext = Path(audio.filename).suffix or ".mp3"
|
||||
image_path = UPLOAD_DIR / f"img_{task_id}{image_ext}"
|
||||
audio_path = UPLOAD_DIR / f"aud_{task_id}{audio_ext}"
|
||||
|
||||
try:
|
||||
# Save uploaded files
|
||||
with open(image_path, "wb") as f:
|
||||
shutil.copyfileobj(image.file, f)
|
||||
with open(audio_path, "wb") as f:
|
||||
shutil.copyfileobj(audio.file, f)
|
||||
|
||||
# Start background task
|
||||
background_tasks.add_task(
|
||||
_process_avatar_generation,
|
||||
task_id,
|
||||
image_path,
|
||||
audio_path,
|
||||
user_id,
|
||||
resolution,
|
||||
model
|
||||
)
|
||||
|
||||
return {"task_id": task_id, "status": "pending", "message": "Video generation started successfully."}
|
||||
|
||||
except Exception as e:
|
||||
# Cleanup if immediate failure
|
||||
if image_path.exists(): image_path.unlink()
|
||||
if audio_path.exists(): audio_path.unlink()
|
||||
logger.error(f"[VideoStudio] Failed to start generation: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to start generation: {str(e)}")
|
||||
|
||||
@router.get("/task/{task_id}")
|
||||
async def get_task_status(task_id: str, current_user: dict = Depends(get_current_user)):
|
||||
user_id = current_user.get("id", "anonymous")
|
||||
task = task_manager.get_task(task_id, user_id=user_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
return task
|
||||
|
||||
@router.get("/download/{filename}")
|
||||
async def download_video(filename: str):
|
||||
file_path = UPLOAD_DIR / filename
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
return FileResponse(file_path)
|
||||
6
backend/api/video_studio/router.py
Normal file
6
backend/api/video_studio/router.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
from .handlers import avatar
|
||||
|
||||
router = APIRouter(prefix="/api/video-studio", tags=["Video Studio"])
|
||||
|
||||
router.include_router(avatar.router)
|
||||
126
backend/api/video_studio/task_manager.py
Normal file
126
backend/api/video_studio/task_manager.py
Normal file
@@ -0,0 +1,126 @@
|
||||
from typing import Dict, Any, Optional
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from services.database import get_engine_for_user
|
||||
from models.video_models import VideoGenerationTask, VideoTaskStatus, Base
|
||||
|
||||
class TaskManager:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def create_task(self, task_type: str, user_id: str, request_data: Optional[Dict] = None) -> str:
|
||||
"""Create a new persistent task."""
|
||||
task_id = str(uuid.uuid4())
|
||||
|
||||
try:
|
||||
engine = get_engine_for_user(user_id)
|
||||
# Ensure table exists
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
task = VideoGenerationTask(
|
||||
task_id=task_id,
|
||||
user_id=user_id,
|
||||
status=VideoTaskStatus.PENDING,
|
||||
request_data=request_data
|
||||
)
|
||||
db.add(task)
|
||||
db.commit()
|
||||
logger.info(f"[VideoStudio] Created persistent task {task_id} for user {user_id}")
|
||||
return task_id
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
logger.error(f"[VideoStudio] Failed to create task: {e}")
|
||||
raise
|
||||
|
||||
def update_task(self, task_id: str, status: str, result: Optional[Dict] = None, error: Optional[str] = None, user_id: str = None, progress: float = None, message: str = None):
|
||||
"""Update an existing task."""
|
||||
if not user_id:
|
||||
logger.error(f"[VideoStudio] Cannot update task {task_id} without user_id")
|
||||
return
|
||||
|
||||
try:
|
||||
engine = get_engine_for_user(user_id)
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
task = db.query(VideoGenerationTask).filter(VideoGenerationTask.task_id == task_id).first()
|
||||
if not task:
|
||||
logger.warning(f"[VideoStudio] Task {task_id} not found in DB for update")
|
||||
return
|
||||
|
||||
# Map string status to Enum
|
||||
try:
|
||||
# Handle case-insensitive status mapping
|
||||
status_upper = status.upper()
|
||||
if status_upper == "RUNNING":
|
||||
status_upper = "PROCESSING"
|
||||
enum_status = VideoTaskStatus[status_upper]
|
||||
except KeyError:
|
||||
logger.warning(f"[VideoStudio] Invalid status {status}, defaulting to PROCESSING")
|
||||
enum_status = VideoTaskStatus.PROCESSING
|
||||
|
||||
task.status = enum_status
|
||||
task.updated_at = datetime.utcnow()
|
||||
|
||||
if result:
|
||||
task.result = result
|
||||
if error:
|
||||
task.error = error
|
||||
if progress is not None:
|
||||
task.progress = progress
|
||||
if message:
|
||||
task.message = message
|
||||
|
||||
db.commit()
|
||||
logger.debug(f"[VideoStudio] Updated task {task_id} to {status}")
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
logger.error(f"[VideoStudio] Failed to update task {task_id}: {e}")
|
||||
|
||||
def get_task(self, task_id: str, user_id: str = None) -> Optional[Dict[str, Any]]:
|
||||
"""Retrieve task status."""
|
||||
if not user_id:
|
||||
logger.error(f"[VideoStudio] Cannot get task {task_id} without user_id")
|
||||
return None
|
||||
|
||||
try:
|
||||
engine = get_engine_for_user(user_id)
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
task = db.query(VideoGenerationTask).filter(VideoGenerationTask.task_id == task_id).first()
|
||||
if not task:
|
||||
return None
|
||||
|
||||
# Map internal status to frontend status
|
||||
status_val = task.status.value
|
||||
if status_val == "processing":
|
||||
status_val = "running"
|
||||
|
||||
return {
|
||||
"task_id": task.task_id,
|
||||
"status": status_val,
|
||||
"result": task.result,
|
||||
"error": task.error,
|
||||
"progress": task.progress,
|
||||
"message": task.message,
|
||||
"created_at": task.created_at,
|
||||
"updated_at": task.updated_at
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
logger.error(f"[VideoStudio] Failed to get task {task_id}: {e}")
|
||||
return None
|
||||
|
||||
task_manager = TaskManager()
|
||||
Reference in New Issue
Block a user