# Copyright (C) 2025 AIDC-AI # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Video generation endpoints Supports both synchronous and asynchronous video generation. """ import os from fastapi import APIRouter, HTTPException, Request from loguru import logger from api.dependencies import PixelleVideoDep from api.schemas.video import ( VideoGenerateRequest, VideoGenerateResponse, VideoGenerateAsyncResponse, ) from api.tasks import task_manager, TaskType router = APIRouter(prefix="/video", tags=["Video Generation"]) def path_to_url(request: Request, file_path: str) -> str: """Convert file path to accessible URL""" # file_path is like "output/abc123.mp4" # Remove "output/" prefix for cleaner URL if file_path.startswith("output/"): file_path = file_path[7:] # Remove "output/" base_url = str(request.base_url).rstrip('/') return f"{base_url}/api/files/{file_path}" @router.post("/generate/sync", response_model=VideoGenerateResponse) async def generate_video_sync( request_body: VideoGenerateRequest, pixelle_video: PixelleVideoDep, request: Request ): """ Generate video synchronously This endpoint blocks until video generation is complete. Suitable for small videos (< 30 seconds). **Note**: May timeout for large videos. Use `/generate/async` instead. Request body includes all video generation parameters. See VideoGenerateRequest schema for details. Returns path to generated video, duration, and file size. """ try: logger.info(f"Sync video generation: {request_body.text[:50]}...") # Build video generation parameters video_params = { "text": request_body.text, "mode": request_body.mode, "title": request_body.title, "n_scenes": request_body.n_scenes, "min_narration_words": request_body.min_narration_words, "max_narration_words": request_body.max_narration_words, "min_image_prompt_words": request_body.min_image_prompt_words, "max_image_prompt_words": request_body.max_image_prompt_words, # Note: image_width and image_height are now auto-determined from template "image_workflow": request_body.image_workflow, "video_fps": request_body.video_fps, "frame_template": request_body.frame_template, "prompt_prefix": request_body.prompt_prefix, "bgm_path": request_body.bgm_path, "bgm_volume": request_body.bgm_volume, } # Add TTS workflow if specified if request_body.tts_workflow: video_params["tts_workflow"] = request_body.tts_workflow # Add ref_audio if specified if request_body.ref_audio: video_params["ref_audio"] = request_body.ref_audio # Legacy voice_id support (deprecated) if request_body.voice_id: logger.warning("voice_id parameter is deprecated, please use tts_workflow instead") video_params["voice_id"] = request_body.voice_id # Call video generator service result = await pixelle_video.generate_video(**video_params) # Get file size file_size = os.path.getsize(result.video_path) if os.path.exists(result.video_path) else 0 # Convert path to URL video_url = path_to_url(request, result.video_path) return VideoGenerateResponse( video_url=video_url, duration=result.duration, file_size=file_size ) except Exception as e: logger.error(f"Sync video generation error: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/generate/async", response_model=VideoGenerateAsyncResponse) async def generate_video_async( request_body: VideoGenerateRequest, pixelle_video: PixelleVideoDep, request: Request ): """ Generate video asynchronously Creates a background task for video generation. Returns immediately with a task_id for tracking progress. **Workflow:** 1. Submit video generation request 2. Receive task_id in response 3. Poll `/api/tasks/{task_id}` to check status 4. When status is "completed", retrieve video from result Request body includes all video generation parameters. See VideoGenerateRequest schema for details. Returns task_id for tracking progress. """ try: logger.info(f"Async video generation: {request_body.text[:50]}...") # Create task task = task_manager.create_task( task_type=TaskType.VIDEO_GENERATION, request_params=request_body.model_dump() ) # Define async execution function async def execute_video_generation(): """Execute video generation in background""" # Build video generation parameters video_params = { "text": request_body.text, "mode": request_body.mode, "title": request_body.title, "n_scenes": request_body.n_scenes, "min_narration_words": request_body.min_narration_words, "max_narration_words": request_body.max_narration_words, "min_image_prompt_words": request_body.min_image_prompt_words, "max_image_prompt_words": request_body.max_image_prompt_words, # Note: image_width and image_height are now auto-determined from template "image_workflow": request_body.image_workflow, "video_fps": request_body.video_fps, "frame_template": request_body.frame_template, "prompt_prefix": request_body.prompt_prefix, "bgm_path": request_body.bgm_path, "bgm_volume": request_body.bgm_volume, # Progress callback can be added here if needed # "progress_callback": lambda event: task_manager.update_progress(...) } # Add TTS workflow if specified if request_body.tts_workflow: video_params["tts_workflow"] = request_body.tts_workflow # Add ref_audio if specified if request_body.ref_audio: video_params["ref_audio"] = request_body.ref_audio # Legacy voice_id support (deprecated) if request_body.voice_id: logger.warning("voice_id parameter is deprecated, please use tts_workflow instead") video_params["voice_id"] = request_body.voice_id result = await pixelle_video.generate_video(**video_params) # Get file size file_size = os.path.getsize(result.video_path) if os.path.exists(result.video_path) else 0 # Convert path to URL video_url = path_to_url(request, result.video_path) return { "video_url": video_url, "duration": result.duration, "file_size": file_size } # Start execution await task_manager.execute_task( task_id=task.task_id, coro_func=execute_video_generation ) return VideoGenerateAsyncResponse( task_id=task.task_id ) except Exception as e: logger.error(f"Async video generation error: {e}") raise HTTPException(status_code=500, detail=str(e))