支持fastapi服务

This commit is contained in:
puke
2025-10-28 01:33:36 +08:00
committed by puke
parent c387137446
commit c200761b97
28 changed files with 1854 additions and 4 deletions

24
api/routers/__init__.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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))