Save local changes (GSC/Bing integrations) before merging PR #354

This commit is contained in:
ajaysi
2026-02-13 13:11:27 +05:30
parent 43e66835ac
commit 08a1f4a1d8
144 changed files with 8310 additions and 2748 deletions

View File

@@ -0,0 +1 @@
# Video Studio API Module

View 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)

View 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)

View 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()