支持fastapi服务
This commit is contained in:
24
api/routers/__init__.py
Normal file
24
api/routers/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
API Routers
|
||||
"""
|
||||
|
||||
from api.routers.health import router as health_router
|
||||
from api.routers.llm import router as llm_router
|
||||
from api.routers.tts import router as tts_router
|
||||
from api.routers.image import router as image_router
|
||||
from api.routers.content import router as content_router
|
||||
from api.routers.video import router as video_router
|
||||
from api.routers.tasks import router as tasks_router
|
||||
from api.routers.files import router as files_router
|
||||
|
||||
__all__ = [
|
||||
"health_router",
|
||||
"llm_router",
|
||||
"tts_router",
|
||||
"image_router",
|
||||
"content_router",
|
||||
"video_router",
|
||||
"tasks_router",
|
||||
"files_router",
|
||||
]
|
||||
|
||||
125
api/routers/content.py
Normal file
125
api/routers/content.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
Content generation endpoints
|
||||
|
||||
Endpoints for generating narrations, image prompts, and titles.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from api.dependencies import ReelForgeDep
|
||||
from api.schemas.content import (
|
||||
NarrationGenerateRequest,
|
||||
NarrationGenerateResponse,
|
||||
ImagePromptGenerateRequest,
|
||||
ImagePromptGenerateResponse,
|
||||
TitleGenerateRequest,
|
||||
TitleGenerateResponse,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/content", tags=["Content Generation"])
|
||||
|
||||
|
||||
@router.post("/narration", response_model=NarrationGenerateResponse)
|
||||
async def generate_narration(
|
||||
request: NarrationGenerateRequest,
|
||||
reelforge: ReelForgeDep
|
||||
):
|
||||
"""
|
||||
Generate narrations from text
|
||||
|
||||
Uses LLM to break down text into multiple narration segments.
|
||||
|
||||
- **text**: Source text
|
||||
- **n_scenes**: Number of narrations to generate
|
||||
- **min_words**: Minimum words per narration
|
||||
- **max_words**: Maximum words per narration
|
||||
|
||||
Returns list of narration strings.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Generating {request.n_scenes} narrations from text")
|
||||
|
||||
# Call narration generator service
|
||||
narrations = await reelforge.narration_generator(
|
||||
text=request.text,
|
||||
n_scenes=request.n_scenes,
|
||||
min_words=request.min_words,
|
||||
max_words=request.max_words
|
||||
)
|
||||
|
||||
return NarrationGenerateResponse(
|
||||
narrations=narrations
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Narration generation error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/image-prompt", response_model=ImagePromptGenerateResponse)
|
||||
async def generate_image_prompt(
|
||||
request: ImagePromptGenerateRequest,
|
||||
reelforge: ReelForgeDep
|
||||
):
|
||||
"""
|
||||
Generate image prompts from narrations
|
||||
|
||||
Uses LLM to create detailed image generation prompts.
|
||||
|
||||
- **narrations**: List of narration texts
|
||||
- **min_words**: Minimum words per prompt
|
||||
- **max_words**: Maximum words per prompt
|
||||
|
||||
Returns list of image prompts.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Generating image prompts for {len(request.narrations)} narrations")
|
||||
|
||||
# Call image prompt generator service
|
||||
image_prompts = await reelforge.image_prompt_generator(
|
||||
narrations=request.narrations,
|
||||
min_words=request.min_words,
|
||||
max_words=request.max_words
|
||||
)
|
||||
|
||||
return ImagePromptGenerateResponse(
|
||||
image_prompts=image_prompts
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Image prompt generation error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/title", response_model=TitleGenerateResponse)
|
||||
async def generate_title(
|
||||
request: TitleGenerateRequest,
|
||||
reelforge: ReelForgeDep
|
||||
):
|
||||
"""
|
||||
Generate video title from text
|
||||
|
||||
Uses LLM to create an engaging title.
|
||||
|
||||
- **text**: Source text
|
||||
- **style**: Optional title style hint
|
||||
|
||||
Returns generated title.
|
||||
"""
|
||||
try:
|
||||
logger.info("Generating title from text")
|
||||
|
||||
# Call title generator service
|
||||
title = await reelforge.title_generator(
|
||||
text=request.text
|
||||
)
|
||||
|
||||
return TitleGenerateResponse(
|
||||
title=title
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Title generation error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
72
api/routers/files.py
Normal file
72
api/routers/files.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
File service endpoints
|
||||
|
||||
Provides access to generated files (videos, images, audio).
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from loguru import logger
|
||||
|
||||
router = APIRouter(prefix="/files", tags=["Files"])
|
||||
|
||||
|
||||
@router.get("/{file_path:path}")
|
||||
async def get_file(file_path: str):
|
||||
"""
|
||||
Get file by path
|
||||
|
||||
Serves files from the output directory only.
|
||||
|
||||
- **file_path**: File name or path (e.g., "abc123.mp4" or "subfolder/abc123.mp4")
|
||||
|
||||
Returns file for download or preview.
|
||||
"""
|
||||
try:
|
||||
# Automatically prepend "output/" to the path
|
||||
full_path = f"output/{file_path}"
|
||||
abs_path = Path.cwd() / full_path
|
||||
|
||||
if not abs_path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"File not found: {file_path}")
|
||||
|
||||
if not abs_path.is_file():
|
||||
raise HTTPException(status_code=400, detail=f"Path is not a file: {file_path}")
|
||||
|
||||
# Security: only allow access to output directory
|
||||
try:
|
||||
rel_path = abs_path.relative_to(Path.cwd())
|
||||
if not str(rel_path).startswith("output"):
|
||||
raise HTTPException(status_code=403, detail="Access denied: only output directory is accessible")
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
# Determine media type
|
||||
suffix = abs_path.suffix.lower()
|
||||
media_types = {
|
||||
'.mp4': 'video/mp4',
|
||||
'.mp3': 'audio/mpeg',
|
||||
'.wav': 'audio/wav',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
}
|
||||
media_type = media_types.get(suffix, 'application/octet-stream')
|
||||
|
||||
# Use inline disposition for browser preview
|
||||
return FileResponse(
|
||||
path=str(abs_path),
|
||||
media_type=media_type,
|
||||
headers={
|
||||
"Content-Disposition": f'inline; filename="{abs_path.name}"'
|
||||
}
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"File access error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
42
api/routers/health.py
Normal file
42
api/routers/health.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
Health check and system info endpoints
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(tags=["Health"])
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Health check response"""
|
||||
status: str = "healthy"
|
||||
version: str = "0.1.0"
|
||||
service: str = "ReelForge API"
|
||||
|
||||
|
||||
class CapabilitiesResponse(BaseModel):
|
||||
"""Capabilities response"""
|
||||
success: bool = True
|
||||
capabilities: dict
|
||||
|
||||
|
||||
@router.get("/health", response_model=HealthResponse)
|
||||
async def health_check():
|
||||
"""
|
||||
Health check endpoint
|
||||
|
||||
Returns service status and version information.
|
||||
"""
|
||||
return HealthResponse()
|
||||
|
||||
|
||||
@router.get("/version", response_model=HealthResponse)
|
||||
async def get_version():
|
||||
"""
|
||||
Get API version
|
||||
|
||||
Returns version information.
|
||||
"""
|
||||
return HealthResponse()
|
||||
|
||||
49
api/routers/image.py
Normal file
49
api/routers/image.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
Image generation endpoints
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from api.dependencies import ReelForgeDep
|
||||
from api.schemas.image import ImageGenerateRequest, ImageGenerateResponse
|
||||
|
||||
router = APIRouter(prefix="/image", tags=["Image"])
|
||||
|
||||
|
||||
@router.post("/generate", response_model=ImageGenerateResponse)
|
||||
async def image_generate(
|
||||
request: ImageGenerateRequest,
|
||||
reelforge: ReelForgeDep
|
||||
):
|
||||
"""
|
||||
Image generation endpoint
|
||||
|
||||
Generate image from text prompt using ComfyKit.
|
||||
|
||||
- **prompt**: Image description/prompt
|
||||
- **width**: Image width (512-2048)
|
||||
- **height**: Image height (512-2048)
|
||||
- **workflow**: Optional custom workflow filename
|
||||
|
||||
Returns path to generated image.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Image generation request: {request.prompt[:50]}...")
|
||||
|
||||
# Call image service
|
||||
image_path = await reelforge.image(
|
||||
prompt=request.prompt,
|
||||
width=request.width,
|
||||
height=request.height,
|
||||
workflow=request.workflow
|
||||
)
|
||||
|
||||
return ImageGenerateResponse(
|
||||
image_path=image_path
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Image generation error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
48
api/routers/llm.py
Normal file
48
api/routers/llm.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
LLM (Large Language Model) endpoints
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from api.dependencies import ReelForgeDep
|
||||
from api.schemas.llm import LLMChatRequest, LLMChatResponse
|
||||
|
||||
router = APIRouter(prefix="/llm", tags=["LLM"])
|
||||
|
||||
|
||||
@router.post("/chat", response_model=LLMChatResponse)
|
||||
async def llm_chat(
|
||||
request: LLMChatRequest,
|
||||
reelforge: ReelForgeDep
|
||||
):
|
||||
"""
|
||||
LLM chat endpoint
|
||||
|
||||
Generate text response using configured LLM.
|
||||
|
||||
- **prompt**: User prompt/question
|
||||
- **temperature**: Creativity level (0.0-2.0, lower = more deterministic)
|
||||
- **max_tokens**: Maximum response length
|
||||
|
||||
Returns generated text response.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"LLM chat request: {request.prompt[:50]}...")
|
||||
|
||||
# Call LLM service
|
||||
response = await reelforge.llm(
|
||||
prompt=request.prompt,
|
||||
temperature=request.temperature,
|
||||
max_tokens=request.max_tokens
|
||||
)
|
||||
|
||||
return LLMChatResponse(
|
||||
content=response,
|
||||
tokens_used=None # Can add token counting if needed
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LLM chat error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
93
api/routers/tasks.py
Normal file
93
api/routers/tasks.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
Task management endpoints
|
||||
|
||||
Endpoints for managing async tasks (checking status, canceling, etc.)
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from loguru import logger
|
||||
|
||||
from api.tasks import task_manager, Task, TaskStatus
|
||||
|
||||
router = APIRouter(prefix="/tasks", tags=["Tasks"])
|
||||
|
||||
|
||||
@router.get("", response_model=List[Task])
|
||||
async def list_tasks(
|
||||
status: Optional[TaskStatus] = Query(None, description="Filter by status"),
|
||||
limit: int = Query(100, ge=1, le=1000, description="Maximum number of tasks")
|
||||
):
|
||||
"""
|
||||
List tasks
|
||||
|
||||
Retrieve list of tasks with optional filtering.
|
||||
|
||||
- **status**: Optional filter by status (pending/running/completed/failed/cancelled)
|
||||
- **limit**: Maximum number of tasks to return (default 100)
|
||||
|
||||
Returns list of tasks sorted by creation time (newest first).
|
||||
"""
|
||||
try:
|
||||
tasks = task_manager.list_tasks(status=status, limit=limit)
|
||||
return tasks
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"List tasks error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{task_id}", response_model=Task)
|
||||
async def get_task(task_id: str):
|
||||
"""
|
||||
Get task details
|
||||
|
||||
Retrieve detailed information about a specific task.
|
||||
|
||||
- **task_id**: Task ID
|
||||
|
||||
Returns task details including status, progress, and result (if completed).
|
||||
"""
|
||||
try:
|
||||
task = task_manager.get_task(task_id)
|
||||
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
|
||||
|
||||
return task
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Get task error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{task_id}")
|
||||
async def cancel_task(task_id: str):
|
||||
"""
|
||||
Cancel task
|
||||
|
||||
Cancel a running or pending task.
|
||||
|
||||
- **task_id**: Task ID
|
||||
|
||||
Returns success status.
|
||||
"""
|
||||
try:
|
||||
success = task_manager.cancel_task(task_id)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Task {task_id} cancelled successfully"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Cancel task error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
50
api/routers/tts.py
Normal file
50
api/routers/tts.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
TTS (Text-to-Speech) endpoints
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from api.dependencies import ReelForgeDep
|
||||
from api.schemas.tts import TTSSynthesizeRequest, TTSSynthesizeResponse
|
||||
from reelforge.utils.tts_util import get_audio_duration
|
||||
|
||||
router = APIRouter(prefix="/tts", tags=["TTS"])
|
||||
|
||||
|
||||
@router.post("/synthesize", response_model=TTSSynthesizeResponse)
|
||||
async def tts_synthesize(
|
||||
request: TTSSynthesizeRequest,
|
||||
reelforge: ReelForgeDep
|
||||
):
|
||||
"""
|
||||
Text-to-Speech synthesis endpoint
|
||||
|
||||
Convert text to speech audio.
|
||||
|
||||
- **text**: Text to synthesize
|
||||
- **voice_id**: Voice ID (e.g., 'zh-CN-YunjianNeural', 'en-US-AriaNeural')
|
||||
|
||||
Returns path to generated audio file and duration.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"TTS synthesis request: {request.text[:50]}...")
|
||||
|
||||
# Call TTS service
|
||||
audio_path = await reelforge.tts(
|
||||
text=request.text,
|
||||
voice_id=request.voice_id
|
||||
)
|
||||
|
||||
# Get audio duration
|
||||
duration = get_audio_duration(audio_path)
|
||||
|
||||
return TTSSynthesizeResponse(
|
||||
audio_path=audio_path,
|
||||
duration=duration
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"TTS synthesis error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
180
api/routers/video.py
Normal file
180
api/routers/video.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
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 ReelForgeDep
|
||||
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,
|
||||
reelforge: ReelForgeDep,
|
||||
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]}...")
|
||||
|
||||
# Call video generator service
|
||||
result = await reelforge.generate_video(
|
||||
text=request_body.text,
|
||||
mode=request_body.mode,
|
||||
title=request_body.title,
|
||||
n_scenes=request_body.n_scenes,
|
||||
voice_id=request_body.voice_id,
|
||||
use_uuid_filename=True, # API mode: use UUID filename
|
||||
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,
|
||||
image_width=request_body.image_width,
|
||||
image_height=request_body.image_height,
|
||||
image_workflow=request_body.image_workflow,
|
||||
video_width=request_body.video_width,
|
||||
video_height=request_body.video_height,
|
||||
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,
|
||||
)
|
||||
|
||||
# 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,
|
||||
reelforge: ReelForgeDep,
|
||||
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"""
|
||||
result = await reelforge.generate_video(
|
||||
text=request_body.text,
|
||||
mode=request_body.mode,
|
||||
title=request_body.title,
|
||||
n_scenes=request_body.n_scenes,
|
||||
voice_id=request_body.voice_id,
|
||||
use_uuid_filename=True, # API mode: use UUID filename
|
||||
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,
|
||||
image_width=request_body.image_width,
|
||||
image_height=request_body.image_height,
|
||||
image_workflow=request_body.image_workflow,
|
||||
video_width=request_body.video_width,
|
||||
video_height=request_body.video_height,
|
||||
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(...)
|
||||
)
|
||||
|
||||
# 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))
|
||||
|
||||
Reference in New Issue
Block a user