Initial commit: Pixel AI comic/video creation platform

- FastAPI backend with SQLModel, Alembic migrations, AgentScope agents
- Next.js 15 frontend with React 19, Tailwind, Zustand, React Flow
- Multi-provider AI system (DashScope, Kling, MiniMax, Volcengine, OpenAI, etc.)
- All HTTP clients migrated from sync requests to async httpx
- Admin-managed API keys via environment variables
- SSRF vulnerability fixed in ensure_url()
This commit is contained in:
张鹏
2026-04-29 01:20:12 +08:00
commit f9f4560459
808 changed files with 151724 additions and 0 deletions

View File

@@ -0,0 +1,159 @@
"""
Audio & Export Endpoints - 音频生成和导出模块
包含:
- POST /audio/generate - 音频生成
- POST /generations/audio - 统一音频生成
- POST /export/project/{project_id} - 项目导出
"""
import logging
from fastapi import APIRouter, Depends
from src.models.schemas import (
ResponseModel,
AudioGenerationRequest,
GenerateAudioRequest,
ExportProjectRequest
)
from src.services.provider.registry import ModelRegistry, ModelType
from src.services.task_manager import task_manager
from src.services.export_service import VideoExportService
from src.auth.dependencies import get_current_user, UserAuth
from .helpers import resolve_service, check_user_api_key
from src.utils.errors import ErrorCode, AppException
router = APIRouter()
logger = logging.getLogger(__name__)
# 初始化 export service
export_service = VideoExportService()
@router.post("/audio/generate", response_model=ResponseModel)
async def generate_audio(
request: GenerateAudioRequest,
current_user: UserAuth = Depends(get_current_user)
):
"""生成 audio from text兼容旧接口内部走统一任务系统"""
logger.info("Audio generation request (legacy): model=%s, text=%s...", request.model, request.text[:50])
# 兼容旧接口model 可为空,回退到默认音频模型
model_name = request.model or ModelRegistry.get_default_id(ModelType.AUDIO)
if not model_name:
raise AppException(
message="Audio service not configured",
code=ErrorCode.INTERNAL_ERROR,
status_code=500
)
# 验证模型可解析并获取服务
audio_service = resolve_service(model_name, ModelType.AUDIO)
# 检查用户是否配置了 API Key
# 从 audio_service 获取 provider_id
provider = getattr(audio_service, 'provider_id', None)
if provider:
check_user_api_key(current_user.id, provider)
try:
task_params = {
"text": request.text,
"voice": request.voice,
"model": model_name,
"project_id": request.project_id,
"storyboard_id": request.storyboard_id,
"extra_params": request.extra_params or {},
}
task = await task_manager.create_task(
task_type="audio",
model=model_name,
params=task_params,
user_id=current_user.id,
project_id=request.project_id,
)
# 兼容前端:保留 audio_url 字段但异步任务初始为空
return ResponseModel(data={
"audio_url": None,
"task_id": task.id,
"status": task.status
})
except Exception as e:
raise AppException(
message=str(e),
code=ErrorCode.INTERNAL_ERROR,
status_code=500
)
@router.post("/generations/audio", response_model=ResponseModel)
async def generate_audio_unified(
request: AudioGenerationRequest,
current_user: UserAuth = Depends(get_current_user)
):
"""统一音频生成端点(推荐)"""
logger.info("Audio generation request: model=%s, text=%s...", request.model, request.text[:50])
audio_service = resolve_service(request.model, ModelType.AUDIO)
# 检查用户是否配置了 API Key
# 从 audio_service 获取 provider_id
provider = getattr(audio_service, 'provider_id', None)
if provider:
check_user_api_key(current_user.id, provider)
if not audio_service:
raise AppException(
message="Audio service not configured",
code=ErrorCode.INTERNAL_ERROR,
status_code=500
)
try:
task_params = request.model_dump()
# Get user_id from authenticated user
user_id = current_user.id
# Handle project_id from multiple sources
# Priority 1: request.project_id (from request body, alias="projectId")
# Priority 2: request.source_id if source=='project' (from extra_params, alias="sourceId")
project_id = request.project_id
if not project_id and request.source == "project":
project_id = request.source_id
task = await task_manager.create_task(
task_type="audio",
model=request.model,
params=task_params,
user_id=user_id,
project_id=project_id
)
except Exception as e:
logger.error("Failed to create audio task: %s", e)
raise AppException(
message=f"Failed to create task: {e}",
code=ErrorCode.INTERNAL_ERROR,
status_code=500
)
return ResponseModel(data={
"task_id": task.id,
"status": task.status
})
@router.post("/export/project/{project_id}", response_model=ResponseModel)
async def export_project(project_id: str, request: ExportProjectRequest):
""" 导出 project to video"""
try:
result = await export_service.export_project(project_id, format=request.format)
return ResponseModel(data=result)
except Exception as e:
raise AppException(
message=str(e),
code=ErrorCode.INTERNAL_ERROR,
status_code=500
)