完善fastapi接口

This commit is contained in:
puke
2025-11-05 19:46:47 +08:00
parent eee604d8e9
commit 15899afb6f
11 changed files with 595 additions and 56 deletions

View File

@@ -10,6 +10,8 @@ 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
from api.routers.resources import router as resources_router
from api.routers.frame import router as frame_router
__all__ = [
"health_router",
@@ -20,5 +22,7 @@ __all__ = [
"video_router",
"tasks_router",
"files_router",
"resources_router",
"frame_router",
]

View File

@@ -1,7 +1,7 @@
"""
File service endpoints
Provides access to generated files (videos, images, audio).
Provides access to generated files (videos, images, audio) and resource files.
"""
from pathlib import Path
@@ -17,15 +17,49 @@ async def get_file(file_path: str):
"""
Get file by path
Serves files from the output directory only.
Serves files from allowed directories:
- output/ - Generated files (videos, images, audio)
- workflows/ - ComfyUI workflow files
- templates/ - HTML templates
- bgm/ - Background music
- data/bgm/ - Custom background music
- data/templates/ - Custom templates
- resources/ - Other resources (images, fonts, etc.)
- **file_path**: File name or path (e.g., "abc123.mp4" or "subfolder/abc123.mp4")
- **file_path**: File path relative to allowed directories
Examples:
- "abc123.mp4" → output/abc123.mp4
- "workflows/runninghub/image_flux.json" → workflows/runninghub/image_flux.json
- "templates/1080x1920/default.html" → templates/1080x1920/default.html
- "bgm/default.mp3" → bgm/default.mp3
- "resources/example.png" → resources/example.png
Returns file for download or preview.
"""
try:
# Automatically prepend "output/" to the path
full_path = f"output/{file_path}"
# Define allowed directories (in priority order)
allowed_prefixes = [
"output/",
"workflows/",
"templates/",
"bgm/",
"data/bgm/",
"data/templates/",
"resources/",
]
# Check if path starts with allowed prefix, otherwise try output/
full_path = None
for prefix in allowed_prefixes:
if file_path.startswith(prefix):
full_path = file_path
break
# If no prefix matched, assume it's in output/ (backward compatibility)
if full_path is None:
full_path = f"output/{file_path}"
abs_path = Path.cwd() / full_path
if not abs_path.exists():
@@ -34,11 +68,19 @@ async def get_file(file_path: str):
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
# Security: only allow access to specified directories
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")
rel_path_str = str(rel_path)
# Check if path starts with any allowed prefix
is_allowed = any(rel_path_str.startswith(prefix.rstrip('/')) for prefix in allowed_prefixes)
if not is_allowed:
raise HTTPException(
status_code=403,
detail=f"Access denied: only {', '.join(p.rstrip('/') for p in allowed_prefixes)} directories are accessible"
)
except ValueError:
raise HTTPException(status_code=403, detail="Access denied")
@@ -52,6 +94,8 @@ async def get_file(file_path: str):
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.html': 'text/html',
'.json': 'application/json',
}
media_type = media_types.get(suffix, 'application/octet-stream')

73
api/routers/frame.py Normal file
View File

@@ -0,0 +1,73 @@
"""
Frame/Template rendering endpoints
"""
from fastapi import APIRouter, HTTPException
from loguru import logger
from api.dependencies import PixelleVideoDep
from api.schemas.frame import FrameRenderRequest, FrameRenderResponse
from pixelle_video.services.frame_html import HTMLFrameGenerator
from pixelle_video.utils.template_util import parse_template_size, resolve_template_path
router = APIRouter(prefix="/frame", tags=["Frame Rendering"])
@router.post("/render", response_model=FrameRenderResponse)
async def render_frame(
request: FrameRenderRequest,
pixelle_video: PixelleVideoDep
):
"""
Render a single frame using HTML template
Generates a frame image by combining template, title, text, and image.
This is useful for previewing templates or generating custom frames.
- **template**: Template key (e.g., '1080x1920/default.html')
- **title**: Optional title text
- **text**: Frame text content
- **image**: Image path (can be local path or URL)
Returns path to generated frame image.
Example:
```json
{
"template": "1080x1920/modern.html",
"title": "Welcome",
"text": "This is a beautiful frame with custom styling",
"image": "resources/example.png"
}
```
"""
try:
logger.info(f"Frame render request: template={request.template}")
# Resolve template path (handles both "default.html" and "1080x1920/default.html")
template_path = resolve_template_path(request.template)
full_template_path = f"templates/{template_path}"
# Parse template size
width, height = parse_template_size(full_template_path)
# Create HTML frame generator
generator = HTMLFrameGenerator(full_template_path)
# Generate frame
frame_path = await generator.generate_frame(
title=request.title,
text=request.text,
image=request.image
)
return FrameRenderResponse(
frame_path=frame_path,
width=width,
height=height
)
except Exception as e:
logger.error(f"Frame render error: {e}")
raise HTTPException(status_code=500, detail=str(e))

229
api/routers/resources.py Normal file
View File

@@ -0,0 +1,229 @@
"""
Resource discovery endpoints
Provides endpoints to discover available workflows, templates, and BGM.
"""
from pathlib import Path
from fastapi import APIRouter, HTTPException
from loguru import logger
from api.dependencies import PixelleVideoDep
from api.schemas.resources import (
WorkflowInfo,
WorkflowListResponse,
TemplateInfo,
TemplateListResponse,
BGMInfo,
BGMListResponse,
)
from pixelle_video.utils.os_util import list_resource_files, get_root_path, get_data_path
from pixelle_video.utils.template_util import get_all_templates_with_info
router = APIRouter(prefix="/resources", tags=["Resources"])
@router.get("/workflows/tts", response_model=WorkflowListResponse)
async def list_tts_workflows(pixelle_video: PixelleVideoDep):
"""
List available TTS workflows
Returns list of TTS workflows from both RunningHub and self-hosted sources.
Example response:
```json
{
"workflows": [
{
"name": "tts_edge.json",
"display_name": "tts_edge.json - Runninghub",
"source": "runninghub",
"path": "workflows/runninghub/tts_edge.json",
"key": "runninghub/tts_edge.json",
"workflow_id": "123456"
}
]
}
```
"""
try:
# Get all workflows from TTS service
all_workflows = pixelle_video.tts.list_workflows()
# Filter to TTS workflows only (filename starts with "tts_")
tts_workflows = [
WorkflowInfo(**wf)
for wf in all_workflows
if wf["name"].startswith("tts_")
]
return WorkflowListResponse(workflows=tts_workflows)
except Exception as e:
logger.error(f"List TTS workflows error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/workflows/image", response_model=WorkflowListResponse)
async def list_image_workflows(pixelle_video: PixelleVideoDep):
"""
List available image generation workflows
Returns list of image workflows from both RunningHub and self-hosted sources.
Example response:
```json
{
"workflows": [
{
"name": "image_flux.json",
"display_name": "image_flux.json - Runninghub",
"source": "runninghub",
"path": "workflows/runninghub/image_flux.json",
"key": "runninghub/image_flux.json",
"workflow_id": "123456"
}
]
}
```
"""
try:
# Get all workflows from image service
all_workflows = pixelle_video.image.list_workflows()
# Filter to image workflows only (filename starts with "image_")
image_workflows = [
WorkflowInfo(**wf)
for wf in all_workflows
if wf["name"].startswith("image_")
]
return WorkflowListResponse(workflows=image_workflows)
except Exception as e:
logger.error(f"List image workflows error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/templates", response_model=TemplateListResponse)
async def list_templates():
"""
List available video templates
Returns list of HTML templates grouped by size (portrait, landscape, square).
Templates are merged from both default (templates/) and custom (data/templates/) directories.
Example response:
```json
{
"templates": [
{
"name": "default.html",
"display_name": "default.html",
"size": "1080x1920",
"width": 1080,
"height": 1920,
"orientation": "portrait",
"path": "templates/1080x1920/default.html",
"key": "1080x1920/default.html"
}
]
}
```
"""
try:
# Get all templates with info
all_templates = get_all_templates_with_info()
# Convert to API response format
templates = []
for t in all_templates:
templates.append(TemplateInfo(
name=t.display_info.name,
display_name=t.display_info.name,
size=t.display_info.size,
width=t.display_info.width,
height=t.display_info.height,
orientation=t.display_info.orientation,
path=t.template_path,
key=t.template_path
))
return TemplateListResponse(templates=templates)
except Exception as e:
logger.error(f"List templates error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/bgm", response_model=BGMListResponse)
async def list_bgm():
"""
List available background music files
Returns list of BGM files merged from both default (bgm/) and custom (data/bgm/) directories.
Custom files take precedence over default files with the same name.
Supported formats: mp3, wav, flac, m4a, aac, ogg
Example response:
```json
{
"bgm_files": [
{
"name": "default.mp3",
"path": "bgm/default.mp3",
"source": "default"
},
{
"name": "happy.mp3",
"path": "data/bgm/happy.mp3",
"source": "custom"
}
]
}
```
"""
try:
# Supported audio extensions
audio_extensions = ('.mp3', '.wav', '.flac', '.m4a', '.aac', '.ogg')
# Collect BGM files from both locations
bgm_files_dict = {} # {filename: {"path": str, "source": str}}
# Scan default bgm/ directory
default_bgm_dir = Path(get_root_path("bgm"))
if default_bgm_dir.exists() and default_bgm_dir.is_dir():
for item in default_bgm_dir.iterdir():
if item.is_file() and item.suffix.lower() in audio_extensions:
bgm_files_dict[item.name] = {
"path": f"bgm/{item.name}",
"source": "default"
}
# Scan custom data/bgm/ directory (overrides default)
custom_bgm_dir = Path(get_data_path("bgm"))
if custom_bgm_dir.exists() and custom_bgm_dir.is_dir():
for item in custom_bgm_dir.iterdir():
if item.is_file() and item.suffix.lower() in audio_extensions:
bgm_files_dict[item.name] = {
"path": f"data/bgm/{item.name}",
"source": "custom"
}
# Convert to response format
bgm_files = [
BGMInfo(
name=name,
path=info["path"],
source=info["source"]
)
for name, info in sorted(bgm_files_dict.items())
]
return BGMListResponse(bgm_files=bgm_files)
except Exception as e:
logger.error(f"List BGM error: {e}")
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -20,21 +20,53 @@ async def tts_synthesize(
"""
Text-to-Speech synthesis endpoint
Convert text to speech audio.
Convert text to speech audio using ComfyUI workflows.
- **text**: Text to synthesize
- **voice_id**: Voice ID (e.g., '[Chinese] zh-CN Yunjian', '[English] en-US Aria')
- **workflow**: TTS workflow key (optional, uses default if not specified)
- **ref_audio**: Reference audio for voice cloning (optional)
- **voice_id**: (Deprecated) Voice ID for legacy compatibility
Returns path to generated audio file and duration.
Examples:
```json
{
"text": "Hello, welcome to Pixelle-Video!",
"workflow": "runninghub/tts_edge.json"
}
```
With voice cloning:
```json
{
"text": "Hello, this is a cloned voice",
"workflow": "runninghub/tts_index2.json",
"ref_audio": "path/to/reference.wav"
}
```
"""
try:
logger.info(f"TTS synthesis request: {request.text[:50]}...")
# Build TTS parameters
tts_params = {"text": request.text}
# Add workflow if specified
if request.workflow:
tts_params["workflow"] = request.workflow
# Add ref_audio if specified
if request.ref_audio:
tts_params["ref_audio"] = request.ref_audio
# Legacy voice_id support (deprecated)
if request.voice_id and not request.workflow:
logger.warning("voice_id parameter is deprecated, please use workflow instead")
tts_params["voice"] = request.voice_id
# Call TTS service
audio_path = await pixelle_video.tts(
text=request.text,
voice=request.voice_id
)
audio_path = await pixelle_video.tts(**tts_params)
# Get audio duration
duration = get_audio_duration(audio_path)

View File

@@ -51,26 +51,41 @@ async def generate_video_sync(
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,
"image_width": request_body.image_width,
"image_height": request_body.image_height,
"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(
text=request_body.text,
mode=request_body.mode,
title=request_body.title,
n_scenes=request_body.n_scenes,
voice_id=request_body.voice_id,
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_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,
)
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
@@ -124,27 +139,42 @@ async def generate_video_async(
# Define async execution function
async def execute_video_generation():
"""Execute video generation in background"""
result = await pixelle_video.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,
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_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,
# 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,
"image_width": request_body.image_width,
"image_height": request_body.image_height,
"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(...)
)
# "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