From 6c6b19e03e21e357e7e5e38059f6ca3acba93c29 Mon Sep 17 00:00:00 2001 From: wall-E <18810335268@163.com> Date: Fri, 5 Sep 2025 00:43:20 +0800 Subject: [PATCH] function script --- function/API_DOCUMENTATION.md | 528 ++++++++++++++++++++++++++++++++ function/DOWNLOAD_API_README.md | 234 ++++++++++++++ function/async_video_api.py | 368 ++++++++++++++++++++++ function/digital_human_api.py | 211 +++++++++++++ function/miniprogram_api.py | 523 +++++++++++++++++++++++++++++++ 5 files changed, 1864 insertions(+) create mode 100644 function/API_DOCUMENTATION.md create mode 100644 function/DOWNLOAD_API_README.md create mode 100644 function/async_video_api.py create mode 100644 function/digital_human_api.py create mode 100644 function/miniprogram_api.py diff --git a/function/API_DOCUMENTATION.md b/function/API_DOCUMENTATION.md new file mode 100644 index 0000000..b1cf099 --- /dev/null +++ b/function/API_DOCUMENTATION.md @@ -0,0 +1,528 @@ +# 数字人小程序API文档 + +## 概述 + +这套API系统专为数字人小程序设计,提供完整的文件上传、异步任务处理、数字人生成、模板管理等功能。 + +## 基础URL + +``` +http://aigc.yanhan.cn:5000/api +``` + +## 认证 + +目前系统支持可选的用户ID识别,后续可扩展为完整的认证系统。 + +--- + +## 📁 文件上传API + +### 1. 上传视频文件 + +**POST** `/upload/video` + +**请求格式:** `multipart/form-data` + +**参数:** +- `file` (file, 必需): 视频文件 (支持mp4, avi, mov等格式,最大500MB) +- `custom_name` (string, 可选): 自定义文件名 + +**响应示例:** +```json +{ + "success": true, + "message": "视频上传成功", + "file_info": { + "filename": "abc123.mp4", + "file_path": "/mnt/docker/resource/uploads/video/abc123.mp4", + "file_type": "video", + "file_size": 15728640, + "mime_type": "video/mp4", + "relative_path": "uploads/video/abc123.mp4", + "download_url": "/download/upload/video/abc123.mp4" + } +} +``` + +### 2. 上传音频文件 + +**POST** `/upload/audio` + +**请求格式:** `multipart/form-data` + +**参数:** +- `file` (file, 必需): 音频文件 (支持wav, mp3, aac等格式,最大50MB) +- `custom_name` (string, 可选): 自定义文件名 + +### 3. 上传图片文件 + +**POST** `/upload/image` + +**请求格式:** `multipart/form-data` + +**参数:** +- `file` (file, 必需): 图片文件 (支持jpg, png, gif等格式,最大10MB) +- `custom_name` (string, 可选): 自定义文件名 + +### 4. 列出已上传文件 + +**GET** `/files/list?type={file_type}` + +**参数:** +- `type` (string, 可选): 文件类型过滤 (`video`, `audio`, `image`) + +**响应示例:** +```json +{ + "success": true, + "files": [ + { + "filename": "sample.mp4", + "file_type": "video", + "file_size": 15728640, + "download_url": "/download/upload/video/sample.mp4" + } + ], + "total": 1 +} +``` + +--- + +## 🎯 异步任务API + +### 1. 创建语音生成任务 + +**POST** `/tasks/voice/generate` + +**请求体:** +```json +{ + "text": "要转换的文本内容", + "reference_audio": "参考音频文件名(可选)", + "reference_text": "参考音频对应文本(可选)", + "uuid": "自定义UUID(可选)", + "user_id": "用户ID(可选)" +} +``` + +**响应示例:** +```json +{ + "success": true, + "task_id": "550e8400-e29b-41d4-a716-446655440000", + "message": "语音生成任务已创建", + "status_url": "/api/tasks/550e8400-e29b-41d4-a716-446655440000/status" +} +``` + +### 2. 创建数字人生成任务 + +**POST** `/tasks/digital-human/create` + +**请求体:** +```json +{ + "speech_text": "数字人要说的文本", + "sample_video": "样本视频文件名", + "sample_voice": "样本音频文件名", + "uuid": "自定义UUID(可选)", + "user_id": "用户ID(可选)" +} +``` + +### 3. 创建数字人模板任务 + +**POST** `/tasks/template/create` + +**请求体:** +```json +{ + "person_image": "人物图片文件名", + "background_image": "背景图片文件名", + "title_text": "标题文字(可选)", + "title_position": [50, 50], + "title_font_size": 48, + "video_length": 10.0, + "user_id": "用户ID(可选)" +} +``` + +### 4. 创建视频合成任务 + +**POST** `/tasks/video/compose` + +**请求体:** +```json +{ + "template_id": "模板ID", + "audio_file": "音频文件名", + "text_content": "文本内容(可选)", + "uuid": "自定义UUID(可选)", + "user_id": "用户ID(可选)" +} +``` + +### 5. 创建音频提取任务 + +**POST** `/tasks/audio/extract` + +**请求体:** +```json +{ + "video_file": "视频文件名", + "user_id": "用户ID(可选)" +} +``` + +### 6. 查询任务状态 + +**GET** `/tasks/{task_id}/status` + +**响应示例:** +```json +{ + "success": true, + "task_id": "550e8400-e29b-41d4-a716-446655440000", + "status": "completed", + "progress": 100, + "result": { + "audio_path": "/path/to/generated/audio.wav", + "audio_url": "/download/generated/audio/abc123", + "uuid": "abc123" + }, + "error_message": null, + "created_at": "2025-08-04T10:30:00", + "updated_at": "2025-08-04T10:35:00" +} +``` + +**任务状态说明:** +- `pending`: 等待处理 +- `processing`: 正在处理 +- `completed`: 已完成 +- `failed`: 处理失败 +- `cancelled`: 已取消 + +### 7. 取消任务 + +**POST** `/tasks/{task_id}/cancel` + +--- + +## 📥 文件下载API + +### 1. 下载上传的文件 + +**GET** `/download/upload/{file_type}/{filename}` + +### 2. 下载生成的视频 + +**GET** `/download/generated/video/{uuid}` + +### 3. 下载生成的音频 + +**GET** `/download/generated/audio/{uuid}` + +### 4. 下载模板文件 + +**GET** `/download/template/{template_id}` + +--- + +## 🎨 模板管理API + +### 1. 列出所有模板 + +**GET** `/templates/list` + +**响应示例:** +```json +{ + "success": true, + "templates": [ + { + "template_id": "template123", + "title_text": "示例标题", + "person_image": "person.jpg", + "background_image": "bg.jpg", + "created_at": "2025-08-04T10:00:00", + "download_url": "/api/download/template/template123" + } + ], + "total": 1 +} +``` + +### 2. 获取模板信息 + +**GET** `/templates/{template_id}` + +--- + +## 📊 系统状态API + +### 获取系统状态 + +**GET** `/system/status` + +**响应示例:** +```json +{ + "success": true, + "system_status": "running", + "task_stats": { + "pending": 2, + "processing": 1, + "completed": 15, + "failed": 1 + }, + "file_stats": { + "video": 10, + "audio": 8, + "image": 12 + }, + "total_tasks": 19, + "worker_running": true +} +``` + +--- + +## 🔄 典型使用流程 + +### 流程1:基础语音生成 + +1. **上传音频文件**(作为声音模板) + ```bash + POST /api/upload/audio + ``` + +2. **创建语音生成任务** + ```bash + POST /api/tasks/voice/generate + { + "text": "你好,欢迎使用数字人系统", + "reference_audio": "uploaded_audio.wav" + } + ``` + +3. **查询任务状态** + ```bash + GET /api/tasks/{task_id}/status + ``` + +4. **下载生成的音频** + ```bash + GET /api/download/generated/audio/{uuid} + ``` + +### 流程2:数字人视频生成 + +1. **上传素材文件** + ```bash + POST /api/upload/video # 上传样本视频 + POST /api/upload/audio # 上传样本音频 + ``` + +2. **创建数字人生成任务** + ```bash + POST /api/tasks/digital-human/create + { + "speech_text": "大家好,我是数字人小助手", + "sample_video": "sample_video.mp4", + "sample_voice": "sample_audio.wav" + } + ``` + + + + +进行任务 +POST /api/task/process +{ + "task_id": 任务ID +} + + + + +3. **监控任务进度** + ```bash + GET /api/tasks/{task_id}/status + ``` + +4. **下载生成的数字人视频** + ```bash + GET /api/download/generated/video/{uuid} + ``` + +### 流程3:模板化视频制作 + +1. **上传图片素材** + ```bash + POST /api/upload/image # 上传人物图片 + POST /api/upload/image # 上传背景图片 + ``` + +2. **创建数字人模板** + ```bash + POST /api/tasks/template/create + { + "person_image": "person.jpg", + "background_image": "background.jpg", + "title_text": "AI助手", + "title_position": [100, 50], + "video_length": 15.0 + } + ``` + +3. **使用模板合成视频** + ```bash + POST /api/tasks/video/compose + { + "template_id": "template123", + "audio_file": "voice_content.wav" + } + ``` + +--- + +## ⚠️ 错误处理 + +所有API都遵循统一的错误响应格式: + +```json +{ + "error": "错误描述信息" +} +``` + +**常见HTTP状态码:** +- `200`: 成功 +- `400`: 请求参数错误 +- `404`: 资源不存在 +- `413`: 文件过大 +- `500`: 服务器内部错误 + +--- + +## 📱 小程序集成建议 + +### 1. 文件上传组件 + +```javascript +// 小程序文件上传示例 +wx.chooseMedia({ + count: 1, + mediaType: ['video'], + success(res) { + const tempFilePath = res.tempFiles[0].tempFilePath; + + wx.uploadFile({ + url: 'https://your-api.com/api/upload/video', + filePath: tempFilePath, + name: 'file', + success(uploadRes) { + const data = JSON.parse(uploadRes.data); + console.log('上传成功', data.file_info); + } + }); + } +}); +``` + +### 2. 任务状态轮询 + +```javascript +// 任务状态查询 +function checkTaskStatus(taskId) { + return new Promise((resolve, reject) => { + const timer = setInterval(() => { + wx.request({ + url: `https://your-api.com/api/tasks/${taskId}/status`, + success(res) { + const { status, progress, result } = res.data; + + if (status === 'completed') { + clearInterval(timer); + resolve(result); + } else if (status === 'failed') { + clearInterval(timer); + reject(new Error('任务失败')); + } + + // 更新进度条 + console.log(`任务进度: ${progress}%`); + } + }); + }, 2000); // 每2秒查询一次 + }); +} +``` + +### 3. 完整的数字人生成流程 + +```javascript +// 完整流程示例 +async function createDigitalHuman(speechText, videoFile, audioFile) { + try { + // 1. 上传文件 + const videoInfo = await uploadFile(videoFile, 'video'); + const audioInfo = await uploadFile(audioFile, 'audio'); + + // 2. 创建任务 + const taskResponse = await wx.request({ + url: 'https://your-api.com/api/tasks/digital-human/create', + method: 'POST', + data: { + speech_text: speechText, + sample_video: videoInfo.filename, + sample_voice: audioInfo.filename + } + }); + + // 3. 等待完成 + const result = await checkTaskStatus(taskResponse.data.task_id); + + // 4. 下载结果 + const downloadUrl = result.video_url; + console.log('数字人视频生成完成:', downloadUrl); + + return result; + } catch (error) { + console.error('生成失败:', error); + throw error; + } +} +``` + +--- + +## 🚀 部署说明 + +### 启动API服务 + +```bash +cd /mnt/docker/code +python miniprogram_api.py +``` + +服务将在 `http://aigc.yanhan.cn:5000` 启动 + +### 环境要求 + +- Python 3.8+ +- Flask +- 相关依赖包(详见requirements.txt) +- 足够的磁盘空间存储上传文件和生成结果 + +### 配置文件 + +可在各模块中修改配置: +- 文件上传限制:`file_upload.py` 中的 `MAX_FILE_SIZES` +- 存储路径:各模块中的路径常量 +- 服务端口:API配置中的端口设置 + +这套API系统提供了完整的数字人生成能力,支持异步处理,适合小程序等前端应用集成使用。 diff --git a/function/DOWNLOAD_API_README.md b/function/DOWNLOAD_API_README.md new file mode 100644 index 0000000..de67949 --- /dev/null +++ b/function/DOWNLOAD_API_README.md @@ -0,0 +1,234 @@ +# Download API Documentation + +This document describes the download APIs added to your Flask application for downloading video and audio files from the system. + +## Available Endpoints + +### 1. Download Video File +**Endpoint:** `GET /download/video/` + +**Description:** Download a video file from the system + +**Parameters:** +- `filename` (path parameter): The name of the video file to download +- `directory` (query parameter, optional): Custom directory path (default: `/mnt/docker/resource`) + +**Example:** +```bash +curl -O "http://localhost:5000/download/video/scene_one.mp4" +curl -O "http://localhost:5000/download/video/scene_one.mp4?directory=/custom/path" +``` + +**Response:** +- Success: File download with appropriate MIME type +- Error: JSON with error message and HTTP status code + +--- + +### 2. Download Audio File +**Endpoint:** `GET /download/audio/` + +**Description:** Download an audio file from the system + +**Parameters:** +- `filename` (path parameter): The name of the audio file to download +- `directory` (query parameter, optional): Custom directory path (default: `/mnt/docker/resource`) + +**Example:** +```bash +curl -O "http://localhost:5000/download/audio/output.wav" +curl -O "http://localhost:5000/download/audio/output.wav?directory=/custom/path" +``` + +--- + +### 3. Download Generated Video +**Endpoint:** `GET /download/generated/video/` + +**Description:** Download a generated video file by UUID + +**Parameters:** +- `uuid` (path parameter): The unique identifier for the generated video +- `task_id` (query parameter, optional): Alternative task identifier + +**Example:** +```bash +curl -O "http://localhost:5000/download/generated/video/5" +curl -O "http://localhost:5000/download/generated/video/5?task_id=123" +``` + +--- + +### 4. Download Generated Audio +**Endpoint:** `GET /download/generated/audio/` + +**Description:** Download a generated audio file by UUID + +**Parameters:** +- `uuid` (path parameter): The unique identifier for the generated audio + +**Example:** +```bash +curl -O "http://localhost:5000/download/generated/audio/5" +``` + +--- + +### 5. List Available Files +**Endpoint:** `GET /list/files` + +**Description:** List available files in the system for download + +**Parameters:** +- `directory` (query parameter, optional): Directory to list files from (default: `/mnt/docker/resource`) +- `type` (query parameter, optional): Filter by file type - `video`, `audio`, or `all` (default: `all`) + +**Example:** +```bash +curl "http://localhost:5000/list/files" +curl "http://localhost:5000/list/files?type=video" +curl "http://localhost:5000/list/files?type=audio&directory=/custom/path" +``` + +**Response:** +```json +{ + "directory": "/mnt/docker/resource", + "total_files": 3, + "files": [ + { + "filename": "scene_one.mp4", + "size": 1048576, + "type": "video", + "modified": 1691234567, + "download_url_video": "/download/video/scene_one.mp4", + "download_url_audio": null + }, + { + "filename": "output.wav", + "size": 524288, + "type": "audio", + "modified": 1691234568, + "download_url_video": null, + "download_url_audio": "/download/audio/output.wav" + } + ] +} +``` + +--- + +### 6. List Generated Files by UUID +**Endpoint:** `GET /list/generated/` + +**Description:** List generated files for a specific UUID + +**Parameters:** +- `uuid` (path parameter): The unique identifier to search for generated files + +**Example:** +```bash +curl "http://localhost:5000/list/generated/5" +``` + +**Response:** +```json +{ + "uuid": "5", + "total_files": 2, + "generated_files": [ + { + "type": "audio", + "filename": "5output.wav", + "path": "/mnt/docker/resource/5output.wav", + "download_url": "/download/generated/audio/5" + }, + { + "type": "video", + "filename": "5output.mp4", + "path": "/mnt/docker/resource/5output.mp4", + "download_url": "/download/generated/video/5" + } + ] +} +``` + +## Error Responses + +All endpoints return JSON error responses with appropriate HTTP status codes: + +```json +{ + "error": "Error message describing what went wrong" +} +``` + +Common HTTP status codes: +- `200`: Success +- `400`: Bad request (invalid file type, etc.) +- `404`: File not found +- `500`: Internal server error + +## File Type Support + +**Video formats:** `.mp4`, `.avi`, `.mov`, `.mkv`, `.wmv`, `.flv`, `.webm` + +**Audio formats:** `.wav`, `.mp3`, `.aac`, `.flac`, `.ogg`, `.m4a`, `.wma` + +## Security Notes + +- File paths are validated to prevent directory traversal attacks +- Only files with recognized video/audio extensions are downloadable +- Files must exist in the specified directory to be downloaded + +## Usage Examples + +### Python Example +```python +import requests + +# List all files +response = requests.get("http://localhost:5000/list/files") +files = response.json() + +# Download a specific video +response = requests.get("http://localhost:5000/download/video/scene_one.mp4") +if response.status_code == 200: + with open("downloaded_video.mp4", "wb") as f: + f.write(response.content) + +# Download generated audio by UUID +response = requests.get("http://localhost:5000/download/generated/audio/5") +if response.status_code == 200: + with open("generated_audio.wav", "wb") as f: + f.write(response.content) +``` + +### JavaScript Example +```javascript +// List files +fetch('/list/files') + .then(response => response.json()) + .then(data => console.log(data)); + +// Download file +fetch('/download/video/scene_one.mp4') + .then(response => response.blob()) + .then(blob => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'scene_one.mp4'; + a.click(); + }); +``` + +## Testing + +Use the provided test script to verify the APIs: + +```bash +python test_download_api.py +``` + +Make sure your Flask server is running on port 5000 before testing. diff --git a/function/async_video_api.py b/function/async_video_api.py new file mode 100644 index 0000000..7576568 --- /dev/null +++ b/function/async_video_api.py @@ -0,0 +1,368 @@ +""" +异步视频处理和数字人合成API模块 +支持文件上传、异步任务处理、模板管理等功能 +""" + +import asyncio +import uuid as uuid_lib +from typing import Optional, Dict, Any, List +import os +import json +import time +from enum import Enum +from dataclasses import dataclass +import threading +import queue +from datetime import datetime +import logging + +logger = logging.getLogger(__name__) + +def resolve_file_path(filename: str, file_type: str) -> str: + """ + 解析文件路径,支持Docker容器挂载目录 + + Args: + filename: 文件名(可能包含路径) + file_type: 文件类型 ('audio', 'video', 'image') + + Returns: + 可访问的文件路径 + """ + # 如果是完整路径,直接返回 + if os.path.isabs(filename) and os.path.exists(filename): + return filename + + # 仅文件名的情况,尝试多个可能的路径 + base_filename = os.path.basename(filename) + + # 定义搜索路径优先级 + search_paths = [] + + if file_type == 'audio': + search_paths = [ + f"/mnt/docker/resource/uploads/audio/{base_filename}", + f"{os.path.expanduser('~/heygem_data/voice/data')}/{base_filename}", + f"/mnt/docker/resource/{base_filename}", + f"/mnt/docker/code/data/temp/{base_filename}" + ] + elif file_type == 'video': + search_paths = [ + f"/mnt/docker/resource/uploads/video/{base_filename}", + f"{os.path.expanduser('~/heygem_data/face2face/temp')}/{base_filename}", + f"/mnt/docker/resource/{base_filename}", + f"/mnt/docker/video_resource/{base_filename}" + ] + elif file_type == 'image': + search_paths = [ + f"/mnt/docker/resource/uploads/image/{base_filename}", + f"/mnt/docker/resource/{base_filename}" + ] + + # 查找第一个存在的文件 + for path in search_paths: + if os.path.exists(path): + logger.info(f"Resolved {file_type} file: {filename} -> {path}") + return path + + # 如果都没找到,返回原始文件名(让调用者处理错误) + logger.warning(f"Could not resolve {file_type} file path: {filename}") + return filename + +class TaskStatus(Enum): + """任务状态枚举""" + PENDING = "pending" # 等待中 + PROCESSING = "processing" # 处理中 + COMPLETED = "completed" # 已完成 + FAILED = "failed" # 失败 + CANCELLED = "cancelled" # 已取消 + +class TaskType(Enum): + """任务类型枚举""" + VOICE_GENERATION = "voice_generation" + DIGITAL_HUMAN_CREATION = "digital_human_creation" + VIDEO_COMPOSITION = "video_composition" + TEMPLATE_CREATION = "template_creation" + AUDIO_EXTRACTION = "audio_extraction" + +@dataclass +class Task: + """任务数据类""" + task_id: str + task_type: TaskType + status: TaskStatus + progress: int = 0 + result: Optional[Dict[str, Any]] = None + error_message: Optional[str] = None + created_at: datetime = None + updated_at: datetime = None + user_id: Optional[str] = None + input_data: Optional[Dict[str, Any]] = None + + def __post_init__(self): + if self.created_at is None: + self.created_at = datetime.now() + self.updated_at = self.created_at + +class TaskManager: + """任务管理器""" + + def __init__(self): + self.tasks: Dict[str, Task] = {} + self.task_queue = queue.Queue() + self.worker_thread = None + self.is_running = False + + def start_worker(self): + """启动工作线程""" + if not self.is_running: + self.is_running = True + self.worker_thread = threading.Thread(target=self._worker_loop, daemon=True) + self.worker_thread.start() + logger.info("Task worker started") + + def stop_worker(self): + """停止工作线程""" + self.is_running = False + if self.worker_thread: + self.worker_thread.join() + logger.info("Task worker stopped") + + def create_task(self, task_type: TaskType, input_data: Dict[str, Any], user_id: str = None) -> str: + """创建新任务""" + task_id = str(uuid_lib.uuid4()) + task = Task( + task_id=task_id, + task_type=task_type, + status=TaskStatus.PENDING, + input_data=input_data, + user_id=user_id + ) + self.tasks[task_id] = task + self.task_queue.put(task_id) + logger.info(f"Created task {task_id} of type {task_type.value}") + return task_id + + def get_task(self, task_id: str) -> Optional[Task]: + """获取任务信息""" + return self.tasks.get(task_id) + + def update_task_status(self, task_id: str, status: TaskStatus, progress: int = None, result: Dict[str, Any] = None, error_message: str = None): + """更新任务状态""" + if task_id in self.tasks: + task = self.tasks[task_id] + task.status = status + if progress is not None: + task.progress = progress + if result is not None: + task.result = result + if error_message is not None: + task.error_message = error_message + task.updated_at = datetime.now() + logger.info(f"Updated task {task_id}: status={status.value}, progress={progress}") + + def _worker_loop(self): + """工作线程主循环""" + while self.is_running: + try: + task_id = self.task_queue.get(timeout=1) + self._process_task(task_id) + self.task_queue.task_done() + except queue.Empty: + continue + except Exception as e: + logger.error(f"Error in worker loop: {str(e)}") + + def _process_task(self, task_id: str): + """处理具体任务""" + task = self.get_task(task_id) + if not task: + logger.error(f"Task {task_id} not found") + return + + try: + self.update_task_status(task_id, TaskStatus.PROCESSING, 10) + + if task.task_type == TaskType.VOICE_GENERATION: + result = self._process_voice_generation(task) + elif task.task_type == TaskType.DIGITAL_HUMAN_CREATION: + result = self._process_digital_human_creation(task) + elif task.task_type == TaskType.VIDEO_COMPOSITION: + result = self._process_video_composition(task) + elif task.task_type == TaskType.TEMPLATE_CREATION: + result = self._process_template_creation(task) + elif task.task_type == TaskType.AUDIO_EXTRACTION: + result = self._process_audio_extraction(task) + else: + raise ValueError(f"Unknown task type: {task.task_type}") + + self.update_task_status(task_id, TaskStatus.COMPLETED, 100, result) + + except Exception as e: + logger.error(f"Task {task_id} failed: {str(e)}") + self.update_task_status(task_id, TaskStatus.FAILED, error_message=str(e)) + + def _process_voice_generation(self, task: Task) -> Dict[str, Any]: + """处理语音生成任务""" + import digital_human_api + + input_data = task.input_data + text = input_data['text'] + reference_audio = input_data.get('reference_audio') + reference_text = input_data.get('reference_text', '') + uuid = input_data.get('uuid', task.task_id[:8]) + + self.update_task_status(task.task_id, TaskStatus.PROCESSING, 30) + + # 如果需要先训练声音模型 + if reference_audio and not reference_text: + voice_result = digital_human_api.train_voice_v2(reference_audio) + reference_audio = voice_result.get('asr_format_audio_url') + reference_text = voice_result.get('reference_audio_text', '') + self.update_task_status(task.task_id, TaskStatus.PROCESSING, 60) + + # 生成语音 + audio_path = digital_human_api.generate_voice_v2(text, reference_audio, reference_text, uuid) + + return { + "audio_path": audio_path, + "audio_url": f"/download/generated/audio/{uuid}", + "uuid": uuid, + "text": text + } + + def _process_digital_human_creation(self, task: Task) -> Dict[str, Any]: + """处理数字人创建任务""" + import digital_human_api + + input_data = task.input_data + speech_text = input_data['speech_text'] + sample_video = input_data['sample_video'] + sample_voice = input_data['sample_voice'] + uuid = input_data.get('uuid', task.task_id[:8]) + + self.update_task_status(task.task_id, TaskStatus.PROCESSING, 20) + + # 生成数字人(直接传递文件名) + result = digital_human_api.generate_digital_human_v2( + speech_text, + sample_video, + sample_voice, + uuid + ) + + self.update_task_status(task.task_id, TaskStatus.PROCESSING, 80) + + return { + "digital_human_result": result, + "video_url": f"/download/generated/video/{uuid}", + "audio_url": f"/download/generated/audio/{uuid}", + "uuid": uuid, + "speech_text": speech_text + } + + def _process_video_composition(self, task: Task) -> Dict[str, Any]: + """处理视频合成任务""" + import api + + input_data = task.input_data + template_id = input_data['template_id'] + audio_file = input_data['audio_file'] + text_content = input_data.get('text_content', '') + uuid = input_data.get('uuid', task.task_id[:8]) + + self.update_task_status(task.task_id, TaskStatus.PROCESSING, 40) + + # 获取模板信息 + template_info = self._get_template_info(template_id) + if not template_info: + raise ValueError(f"Template {template_id} not found") + + # 使用模板合成视频 + result = api.generate_video(template_info['video_path'], audio_file, uuid) + + self.update_task_status(task.task_id, TaskStatus.PROCESSING, 90) + + return { + "composition_result": result, + "video_url": f"/download/generated/video/{uuid}", + "template_id": template_id, + "uuid": uuid + } + + def _process_template_creation(self, task: Task) -> Dict[str, Any]: + """处理模板创建任务""" + import api + + input_data = task.input_data + person_image = input_data['person_image'] + title_text = input_data.get('title_text', '') + title_position = tuple(input_data.get('title_position', [50, 50])) + title_font_size = input_data.get('title_font_size', 48) + background_image = input_data['background_image'] + video_length = input_data.get('video_length', 10.0) + + self.update_task_status(task.task_id, TaskStatus.PROCESSING, 50) + + # 生成带标题的模板视频 + template_path = api.generate_video_with_title( + person_image, title_text, title_position, + title_font_size, background_image, video_length + ) + + # 保存模板信息 + template_id = self._save_template_info({ + "template_path": template_path, + "title_text": title_text, + "person_image": person_image, + "background_image": background_image, + "created_at": datetime.now().isoformat() + }) + + return { + "template_id": template_id, + "template_path": template_path, + "template_url": f"/download/template/{template_id}", + "title_text": title_text + } + + def _process_audio_extraction(self, task: Task) -> Dict[str, Any]: + """处理音频提取任务""" + import api + + input_data = task.input_data + video_file = input_data['video_file'] + + self.update_task_status(task.task_id, TaskStatus.PROCESSING, 50) + + # 提取文本 + extracted_text = api.speech_to_text(video_file) + + return { + "extracted_text": extracted_text, + "video_file": video_file + } + + def _get_template_info(self, template_id: str) -> Optional[Dict[str, Any]]: + """获取模板信息""" + template_file = f"/mnt/docker/resource/templates/{template_id}.json" + if os.path.exists(template_file): + with open(template_file, 'r', encoding='utf-8') as f: + return json.load(f) + return None + + def _save_template_info(self, template_data: Dict[str, Any]) -> str: + """保存模板信息""" + template_id = str(uuid_lib.uuid4()) + template_dir = "/mnt/docker/resource/templates" + os.makedirs(template_dir, exist_ok=True) + + template_file = os.path.join(template_dir, f"{template_id}.json") + with open(template_file, 'w', encoding='utf-8') as f: + json.dump(template_data, f, ensure_ascii=False, indent=2) + + return template_id + +# 全局任务管理器实例 +task_manager = TaskManager() +task_manager.start_worker() diff --git a/function/digital_human_api.py b/function/digital_human_api.py new file mode 100644 index 0000000..5b32cf6 --- /dev/null +++ b/function/digital_human_api.py @@ -0,0 +1,211 @@ +""" +优化后的数字人生成API +按照新的文件管理架构设计 +""" + +import requests +import os +import time +import logging +from typing import Dict, Any +from api import Config, APIException, _make_request, _copy_file + +logger = logging.getLogger(__name__) + +def generate_digital_human_v2(speech_text: str, sample_video: str, sample_voice: str, uuid: str) -> Dict[str, Any]: + """ + 生成数字人 (优化版本) + + Args: + speech_text: 语音文本 + sample_video: 样本视频文件名(在上传目录中) + sample_voice: 样本语音文件名(在上传目录中) + uuid: 唯一标识符 + + Returns: + 生成结果 + + Raises: + APIException: 生成失败时抛出异常 + """ + logger.info(f"Generating digital human for UUID: {uuid}") + + try: + from file_upload import file_manager + + # 步骤1: 将音频文件复制到TTS服务目录进行预处理 + tts_audio_filename = file_manager.copy_audio_for_tts(sample_voice, f"dh_{uuid}") + + # 步骤2: 预处理和训练语音模型 + url = f"{Config.VOICE_SERVICE_URL}/v1/preprocess_and_tran" + request_body_one = { + "format": "wav", + "reference_audio": tts_audio_filename, # 使用TTS目录中的文件名 + "lang": "zh" + } + + response_one = _make_request(url, request_body_one) + + # 检查响应中是否包含必要字段 + if 'asr_format_audio_url' not in response_one: + logger.error(f"Voice preprocessing response missing asr_format_audio_url: {response_one}") + raise APIException(f"Voice preprocessing failed: missing asr_format_audio_url in response", 500) + + asr_format_audio_url = response_one['asr_format_audio_url'] + reference_audio_text = response_one.get('reference_audio_text', '') + + # 步骤3: 生成语音 + url = f"{Config.VOICE_SERVICE_URL}/v1/invoke" + request_body_two = { + "speaker": uuid, + "text": speech_text, + "reference_audio": asr_format_audio_url, + "reference_text": reference_audio_text, + **Config.DEFAULT_VOICE_PARAMS + } + + response = requests.post(url, json=request_body_two, timeout=60) + logger.info(f"Voice generation response status: {response.status_code}") + + if response.status_code != 200: + logger.error(f"Voice generation failed: {response.text}") + raise APIException(f"Voice generation failed: {response.text}", response.status_code) + + # 保存生成的音频文件到resource目录 + generated_audio_filename = f"{uuid}output.wav" + generated_audio_path = os.path.join(Config.RESOURCE_DIR, generated_audio_filename) + with open(generated_audio_path, "wb") as f: + f.write(response.content) + logger.info(f"Generated audio saved: {generated_audio_path}") + + # 步骤4: 将视频和生成的音频复制到Face2Face服务目录 + # 首先将生成的音频从resource目录复制到uploads目录(临时) + upload_audio_dir = "/mnt/docker/resource/uploads/audio" + os.makedirs(upload_audio_dir, exist_ok=True) + upload_audio_path = os.path.join(upload_audio_dir, generated_audio_filename) + _copy_file(generated_audio_path, upload_audio_path) + + face2face_video, face2face_audio = file_manager.copy_files_for_face2face( + sample_video, generated_audio_filename, uuid + ) + + # 步骤5: 提交视频生成任务 + url = f"{Config.VIDEO_SERVICE_URL}/easy/submit" + request_body = { + "audio_url": face2face_audio, + "video_url": face2face_video, + "code": str(int(time.time())), # 使用时间戳作为唯一任务ID + "chaofen": 0, + "watermark_switch": 0, + "pn": 1 + } + + result = _make_request(url, request_body) + logger.info(f"Digital human generation submitted successfully: {result}") + + # 清理临时文件 + try: + os.remove(upload_audio_path) + except: + pass + + return result + + except Exception as e: + logger.error(f"Failed to generate digital human: {str(e)}") + if isinstance(e, APIException): + raise + raise APIException(f"Failed to generate digital human: {str(e)}", 500) + + +def generate_voice_v2(text: str, reference_audio: str, reference_text: str, uuid: str) -> str: + """ + 生成语音 (优化版本) + + Args: + text: 要转换的文本 + reference_audio: 参考音频文件名(在上传目录中) + reference_text: 参考文本 + uuid: 唯一标识符 + + Returns: + 生成的音频文件路径(在resource目录中) + + Raises: + APIException: 生成失败时抛出异常 + """ + logger.info(f"Generating voice for UUID: {uuid}") + + try: + from file_upload import file_manager + + # 先将参考音频复制到TTS服务目录 + tts_audio_filename = file_manager.copy_audio_for_tts(reference_audio, uuid) + + url = f"{Config.VOICE_SERVICE_URL}/v1/invoke" + request_body = { + "speaker": uuid, + "text": text, + "reference_audio": tts_audio_filename, # 使用TTS目录中的文件名 + "reference_text": reference_text, + **Config.DEFAULT_VOICE_PARAMS + } + + response = requests.post(url, json=request_body, timeout=60) + logger.info(f"Voice generation response status: {response.status_code}") + + if response.status_code == 200: + # 保存生成的音频到resource目录 + output_filename = f"{uuid}output.wav" + output_path = os.path.join(Config.RESOURCE_DIR, output_filename) + with open(output_path, "wb") as f: + f.write(response.content) + logger.info(f"Generated voice saved to: {output_path}") + return output_path + else: + logger.error(f"Voice generation failed: {response.text}") + raise APIException(f"Voice generation failed: {response.text}", response.status_code) + + except Exception as e: + if isinstance(e, APIException): + raise + logger.error(f"Network error during voice generation: {str(e)}") + raise APIException(f"Voice generation error: {str(e)}", 500) + + +def train_voice_v2(voice_file_name: str) -> Dict[str, Any]: + """ + 训练语音模型 (优化版本) + + Args: + voice_file_name: 语音文件名(在上传目录中) + + Returns: + 训练结果 + + Raises: + APIException: 训练失败时抛出异常 + """ + logger.info(f"Training voice model with file: {voice_file_name}") + + try: + from file_upload import file_manager + + # 将音频文件复制到TTS服务目录 + temp_uuid = "train_" + str(int(time.time())) + tts_audio_filename = file_manager.copy_audio_for_tts(voice_file_name, temp_uuid) + + url = f"{Config.VOICE_SERVICE_URL}/v1/preprocess_and_tran" + request_body = { + "format": "wav", + "reference_audio": tts_audio_filename, # 使用TTS目录中的文件名 + "lang": "zh" + } + + return _make_request(url, request_body) + + except Exception as e: + if isinstance(e, APIException): + raise + logger.error(f"Voice training error: {str(e)}") + raise APIException(f"Voice training error: {str(e)}", 500) diff --git a/function/miniprogram_api.py b/function/miniprogram_api.py new file mode 100644 index 0000000..0fdf042 --- /dev/null +++ b/function/miniprogram_api.py @@ -0,0 +1,523 @@ +""" +数字人小程序API路由系统 +提供完整的文件上传、异步任务处理、数字人生成等功能 +""" + +from flask import Flask, request, jsonify, send_file +import os +import logging +from typing import Dict, Any +import json +from datetime import datetime + +# 导入我们的模块 +import api +from async_video_api import task_manager, TaskType, TaskStatus +from file_upload import file_manager, save_uploaded_file, get_uploaded_file_info, list_uploaded_files + +app = Flask(__name__) +app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500MB 最大上传大小 + +logger = logging.getLogger(__name__) + +# ================================ +# 文件上传相关API +# ================================ + +@app.route('/api/upload/video', methods=['POST']) +def upload_video(): + """上传视频文件""" + try: + if 'file' not in request.files: + return jsonify({"error": "没有文件"}), 400 + + file = request.files['file'] + custom_name = request.form.get('custom_name') + + file_info = save_uploaded_file(file, 'video', custom_name) + + return jsonify({ + "success": True, + "message": "视频上传成功", + "file_info": file_info + }) + + except Exception as e: + logger.error(f"Video upload failed: {str(e)}") + return jsonify({"error": f"上传失败: {str(e)}"}), 500 + +@app.route('/api/upload/audio', methods=['POST']) +def upload_audio(): + """上传音频文件""" + try: + if 'file' not in request.files: + return jsonify({"error": "没有文件"}), 400 + + file = request.files['file'] + custom_name = request.form.get('custom_name') + + file_info = save_uploaded_file(file, 'audio', custom_name) + + return jsonify({ + "success": True, + "message": "音频上传成功", + "file_info": file_info + }) + + except Exception as e: + logger.error(f"Audio upload failed: {str(e)}") + return jsonify({"error": f"上传失败: {str(e)}"}), 500 + +@app.route('/api/upload/image', methods=['POST']) +def upload_image(): + """上传图片文件""" + try: + if 'file' not in request.files: + return jsonify({"error": "没有文件"}), 400 + + file = request.files['file'] + custom_name = request.form.get('custom_name') + + file_info = save_uploaded_file(file, 'image', custom_name) + + return jsonify({ + "success": True, + "message": "图片上传成功", + "file_info": file_info + }) + + except Exception as e: + logger.error(f"Image upload failed: {str(e)}") + return jsonify({"error": f"上传失败: {str(e)}"}), 500 + +@app.route('/api/files/list', methods=['GET']) +def list_files(): + """列出已上传的文件""" + try: + file_type = request.args.get('type') # video, audio, image 或 None (全部) + files = list_uploaded_files(file_type) + + return jsonify({ + "success": True, + "files": files, + "total": len(files) + }) + + except Exception as e: + logger.error(f"List files failed: {str(e)}") + return jsonify({"error": f"获取文件列表失败: {str(e)}"}), 500 + +# ================================ +# 异步任务相关API +# ================================ + +@app.route('/api/tasks/voice/generate', methods=['POST']) +def create_voice_generation_task(): + """创建语音生成任务""" + try: + data = request.get_json() + + required_fields = ['text'] + if not all(field in data for field in required_fields): + return jsonify({"error": "缺少必要字段"}), 400 + + # 创建任务 + task_id = task_manager.create_task( + TaskType.VOICE_GENERATION, + { + 'text': data['text'], + 'reference_audio': data.get('reference_audio'), + 'reference_text': data.get('reference_text', ''), + 'uuid': data.get('uuid') + }, + user_id=data.get('user_id') + ) + + return jsonify({ + "success": True, + "task_id": task_id, + "message": "语音生成任务已创建", + "status_url": f"/api/tasks/{task_id}/status" + }) + + except Exception as e: + logger.error(f"Create voice generation task failed: {str(e)}") + return jsonify({"error": f"创建任务失败: {str(e)}"}), 500 + +@app.route('/api/tasks/digital-human/create', methods=['POST']) +def create_digital_human_task(): + """创建数字人生成任务""" + try: + data = request.get_json() + + required_fields = ['speech_text', 'sample_video', 'sample_voice'] + if not all(field in data for field in required_fields): + return jsonify({"error": "缺少必要字段"}), 400 + + # 创建任务 + task_id = task_manager.create_task( + TaskType.DIGITAL_HUMAN_CREATION, + { + 'speech_text': data['speech_text'], + 'sample_video': data['sample_video'], + 'sample_voice': data['sample_voice'], + 'uuid': data.get('uuid') + }, + user_id=data.get('user_id') + ) + + return jsonify({ + "success": True, + "task_id": task_id, + "message": "数字人生成任务已创建", + "status_url": f"/api/tasks/{task_id}/status" + }) + + except Exception as e: + logger.error(f"Create digital human task failed: {str(e)}") + return jsonify({"error": f"创建任务失败: {str(e)}"}), 500 + +@app.route('/api/tasks/template/create', methods=['POST']) +def create_template_task(): + """创建数字人模板任务""" + try: + data = request.get_json() + + required_fields = ['person_image', 'background_image'] + if not all(field in data for field in required_fields): + return jsonify({"error": "缺少必要字段"}), 400 + + # 创建任务 + task_id = task_manager.create_task( + TaskType.TEMPLATE_CREATION, + { + 'person_image': data['person_image'], + 'background_image': data['background_image'], + 'title_text': data.get('title_text', ''), + 'title_position': data.get('title_position', [50, 50]), + 'title_font_size': data.get('title_font_size', 48), + 'video_length': data.get('video_length', 10.0) + }, + user_id=data.get('user_id') + ) + + return jsonify({ + "success": True, + "task_id": task_id, + "message": "数字人模板创建任务已创建", + "status_url": f"/api/tasks/{task_id}/status" + }) + + except Exception as e: + logger.error(f"Create template task failed: {str(e)}") + return jsonify({"error": f"创建任务失败: {str(e)}"}), 500 + +@app.route('/api/tasks/video/compose', methods=['POST']) +def create_video_composition_task(): + """创建视频合成任务""" + try: + data = request.get_json() + + required_fields = ['template_id', 'audio_file'] + if not all(field in data for field in required_fields): + return jsonify({"error": "缺少必要字段"}), 400 + + # 创建任务 + task_id = task_manager.create_task( + TaskType.VIDEO_COMPOSITION, + { + 'template_id': data['template_id'], + 'audio_file': data['audio_file'], + 'text_content': data.get('text_content', ''), + 'uuid': data.get('uuid') + }, + user_id=data.get('user_id') + ) + + return jsonify({ + "success": True, + "task_id": task_id, + "message": "视频合成任务已创建", + "status_url": f"/api/tasks/{task_id}/status" + }) + + except Exception as e: + logger.error(f"Create video composition task failed: {str(e)}") + return jsonify({"error": f"创建任务失败: {str(e)}"}), 500 + + + +@app.route('/api/task/process', methods=['POST']) +def process_task(): + """处理指定任务""" + try: + # 支持 form-data 和 application/json 两种方式 + if request.is_json: + task_id = request.json.get('task_id') + else: + task_id = request.form.get('task_id') + + if not task_id: + return jsonify({"error": "缺少任务ID"}), 400 + + # 调用任务处理函数 + task_manager._process_task(task_id) + + return jsonify({ + "success": True, + "message": f"任务 {task_id} 正在处理或已完成" + }) + + except Exception as e: + logger.error(f"Task processing failed: {str(e)}") + return jsonify({"error": f"任务处理失败: {str(e)}"}), 500 + + + + +@app.route('/api/tasks/audio/extract', methods=['POST']) +def create_audio_extraction_task(): + """创建音频提取任务""" + try: + data = request.get_json() + + required_fields = ['video_file'] + if not all(field in data for field in required_fields): + return jsonify({"error": "缺少必要字段"}), 400 + + # 创建任务 + task_id = task_manager.create_task( + TaskType.AUDIO_EXTRACTION, + { + 'video_file': data['video_file'] + }, + user_id=data.get('user_id') + ) + + return jsonify({ + "success": True, + "task_id": task_id, + "message": "音频提取任务已创建", + "status_url": f"/api/tasks/{task_id}/status" + }) + + except Exception as e: + logger.error(f"Create audio extraction task failed: {str(e)}") + return jsonify({"error": f"创建任务失败: {str(e)}"}), 500 + +@app.route('/api/tasks//status', methods=['GET']) +def get_task_status(task_id): + """获取任务状态""" + try: + task = task_manager.get_task(task_id) + if not task: + return jsonify({"error": "任务不存在"}), 404 + + return jsonify({ + "success": True, + "task_id": task_id, + "status": task.status.value, + "progress": task.progress, + "result": task.result, + "error_message": task.error_message, + "created_at": task.created_at.isoformat() if task.created_at else None, + "updated_at": task.updated_at.isoformat() if task.updated_at else None + }) + + except Exception as e: + logger.error(f"Get task status failed: {str(e)}") + return jsonify({"error": f"获取任务状态失败: {str(e)}"}), 500 + +@app.route('/api/tasks//cancel', methods=['POST']) +def cancel_task(task_id): + """取消任务""" + try: + task = task_manager.get_task(task_id) + if not task: + return jsonify({"error": "任务不存在"}), 404 + + if task.status in [TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED]: + return jsonify({"error": "任务已完成或已取消"}), 400 + + task_manager.update_task_status(task_id, TaskStatus.CANCELLED) + + return jsonify({ + "success": True, + "message": "任务已取消" + }) + + except Exception as e: + logger.error(f"Cancel task failed: {str(e)}") + return jsonify({"error": f"取消任务失败: {str(e)}"}), 500 + +# ================================ +# 文件下载相关API +# ================================ + +@app.route('/api/download/upload//', methods=['GET']) +def download_uploaded_file(file_type, filename): + """下载上传的文件""" + try: + file_info = get_uploaded_file_info(file_type, filename) + if not file_info: + return jsonify({"error": "文件不存在"}), 404 + + return send_file( + file_info['file_path'], + as_attachment=True, + download_name=filename, + mimetype=file_info.get('mime_type') + ) + + except Exception as e: + logger.error(f"Download uploaded file failed: {str(e)}") + return jsonify({"error": f"下载文件失败: {str(e)}"}), 500 + +@app.route('/api/download/generated/video/', methods=['GET']) +def download_generated_video(uuid): + """下载生成的视频""" + try: + return api.download_generated_video(uuid) + except Exception as e: + logger.error(f"Download generated video failed: {str(e)}") + return jsonify({"error": f"下载生成视频失败: {str(e)}"}), 500 + +@app.route('/api/download/generated/audio/', methods=['GET']) +def download_generated_audio(uuid): + """下载生成的音频""" + try: + return api.download_generated_audio(uuid) + except Exception as e: + logger.error(f"Download generated audio failed: {str(e)}") + return jsonify({"error": f"下载生成音频失败: {str(e)}"}), 500 + +@app.route('/api/download/template/', methods=['GET']) +def download_template(template_id): + """下载模板文件""" + try: + template_info = task_manager._get_template_info(template_id) + if not template_info: + return jsonify({"error": "模板不存在"}), 404 + + template_path = template_info.get('template_path') + if not template_path or not os.path.exists(template_path): + return jsonify({"error": "模板文件不存在"}), 404 + + return send_file( + template_path, + as_attachment=True, + download_name=f"template_{template_id}.mp4", + mimetype='video/mp4' + ) + + except Exception as e: + logger.error(f"Download template failed: {str(e)}") + return jsonify({"error": f"下载模板失败: {str(e)}"}), 500 + +# ================================ +# 模板管理相关API +# ================================ + +@app.route('/api/templates/list', methods=['GET']) +def list_templates(): + """列出所有模板""" + try: + templates_dir = "/mnt/docker/resource/templates" + if not os.path.exists(templates_dir): + return jsonify({"success": True, "templates": [], "total": 0}) + + templates = [] + for filename in os.listdir(templates_dir): + if filename.endswith('.json'): + template_id = filename[:-5] # 移除.json扩展名 + template_info = task_manager._get_template_info(template_id) + if template_info: + template_info['template_id'] = template_id + template_info['download_url'] = f"/api/download/template/{template_id}" + templates.append(template_info) + + return jsonify({ + "success": True, + "templates": templates, + "total": len(templates) + }) + + except Exception as e: + logger.error(f"List templates failed: {str(e)}") + return jsonify({"error": f"获取模板列表失败: {str(e)}"}), 500 + +@app.route('/api/templates/', methods=['GET']) +def get_template_info(template_id): + """获取模板信息""" + try: + template_info = task_manager._get_template_info(template_id) + if not template_info: + return jsonify({"error": "模板不存在"}), 404 + + template_info['template_id'] = template_id + template_info['download_url'] = f"/api/download/template/{template_id}" + + return jsonify({ + "success": True, + "template": template_info + }) + + except Exception as e: + logger.error(f"Get template info failed: {str(e)}") + return jsonify({"error": f"获取模板信息失败: {str(e)}"}), 500 + +# ================================ +# 系统状态相关API +# ================================ + +@app.route('/api/system/status', methods=['GET']) +def get_system_status(): + """获取系统状态""" + try: + # 统计任务状态 + task_stats = {} + for task in task_manager.tasks.values(): + status = task.status.value + task_stats[status] = task_stats.get(status, 0) + 1 + + # 统计文件数量 + file_stats = {} + for file_type in ['video', 'audio', 'image']: + files = list_uploaded_files(file_type) + file_stats[file_type] = len(files) + + return jsonify({ + "success": True, + "system_status": "running", + "task_stats": task_stats, + "file_stats": file_stats, + "total_tasks": len(task_manager.tasks), + "worker_running": task_manager.is_running + }) + + except Exception as e: + logger.error(f"Get system status failed: {str(e)}") + return jsonify({"error": f"获取系统状态失败: {str(e)}"}), 500 + +# ================================ +# 错误处理 +# ================================ + +@app.errorhandler(413) +def too_large(e): + return jsonify({"error": "文件过大"}), 413 + +@app.errorhandler(404) +def not_found(e): + return jsonify({"error": "接口不存在"}), 404 + +@app.errorhandler(500) +def internal_error(e): + return jsonify({"error": "服务器内部错误"}), 500 + +if __name__ == '__main__': + # 启动时清理旧的临时文件 + file_manager.cleanup_temp_files() + + # 启动Flask应用 + app.run(host='0.0.0.0', port=5000, debug=True)