Spaces:
Sleeping
Sleeping
Ray commited on
Commit ·
b17403a
0
Parent(s):
Initial commit: MiniMax Tools for Hugging Face Space
Browse files- CLAUDE.md +0 -0
- Dockerfile +27 -0
- README.md +72 -0
- app/main.py +49 -0
- app/routes/__init__.py +1 -0
- app/routes/image_generation.py +71 -0
- app/routes/music.py +118 -0
- app/routes/vlm.py +117 -0
- app/routes/voice_design.py +118 -0
- app/templates/admin.html +77 -0
- app/templates/base.html +178 -0
- app/templates/image_generation.html +160 -0
- app/templates/index.html +47 -0
- app/templates/music.html +96 -0
- app/templates/vlm.html +111 -0
- app/templates/voice_design.html +106 -0
- app/utils/__init__.py +1 -0
- app/utils/cleanup.py +66 -0
- app/utils/monitor.py +106 -0
- app/utils/rate_limiter.py +53 -0
- requirements.txt +8 -0
CLAUDE.md
ADDED
|
Binary file (7.3 kB). View file
|
|
|
Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# 设置工作目录
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
# 安装系统依赖
|
| 7 |
+
RUN apt-get update && apt-get install -y \
|
| 8 |
+
curl \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
# 复制需求文件
|
| 12 |
+
COPY requirements.txt .
|
| 13 |
+
|
| 14 |
+
# 安装Python依赖
|
| 15 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 16 |
+
|
| 17 |
+
# 复制应用代码
|
| 18 |
+
COPY app/ ./
|
| 19 |
+
|
| 20 |
+
# 创建临时目录(使用标准 /tmp)
|
| 21 |
+
RUN mkdir -p /tmp
|
| 22 |
+
|
| 23 |
+
# 暴露端口(Hugging Face Space 使用 7860)
|
| 24 |
+
EXPOSE 7860
|
| 25 |
+
|
| 26 |
+
# 启动命令
|
| 27 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: MiniMax Tools
|
| 3 |
+
emoji: 🎵
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# MiniMax Tools
|
| 12 |
+
|
| 13 |
+
基于 MiniMax API 的一站式工具集合,包含音乐生成、图片生成、图像分析和语音设计四大功能。
|
| 14 |
+
|
| 15 |
+
## 🌟 功能特性
|
| 16 |
+
|
| 17 |
+
- **🎵 音乐生成**:根据文本提示和歌词生成音乐
|
| 18 |
+
- **🖼️ 图片生成**:基于文本描述生成高质量图片
|
| 19 |
+
- **👁️ 图像分析**:使用视觉语言模型分析图片内容
|
| 20 |
+
- **🔊 语音设计**:根据描述设计个性化语音
|
| 21 |
+
- **📊 统一监控**:实时查看使用统计和性能指标
|
| 22 |
+
- **🔒 数据安全**:用户数据自动清理,无长期存储
|
| 23 |
+
- **⚡ 限流保护**:内置限流机制,防止API过度调用
|
| 24 |
+
|
| 25 |
+
## 🚀 使用说明
|
| 26 |
+
|
| 27 |
+
1. 访问对应功能页面
|
| 28 |
+
2. 输入您的 MiniMax API Key
|
| 29 |
+
3. 填写相关参数
|
| 30 |
+
4. 提交请求即可
|
| 31 |
+
|
| 32 |
+
## 📋 API端点
|
| 33 |
+
|
| 34 |
+
- `GET /` - 主页
|
| 35 |
+
- `GET /music` - 音乐生成页面
|
| 36 |
+
- `POST /music/generate` - 生成音乐
|
| 37 |
+
- `GET /image-generation` - 图片生成页面
|
| 38 |
+
- `POST /image-generation/generate` - 生成图片
|
| 39 |
+
- `GET /vlm` - 图像分析页面
|
| 40 |
+
- `POST /vlm/analyze` - 分析图像
|
| 41 |
+
- `GET /voice-design` - 语音设计页面
|
| 42 |
+
- `POST /voice-design/design` - 设计语音
|
| 43 |
+
- `GET /admin` - 监控面板
|
| 44 |
+
|
| 45 |
+
## 🔧 限流策略
|
| 46 |
+
|
| 47 |
+
- **音乐生成**: 5分钟5次
|
| 48 |
+
- **图片生成**: 5分钟5次
|
| 49 |
+
- **图像分析**: 5分钟10次
|
| 50 |
+
- **语音设计**: 5分钟5次
|
| 51 |
+
|
| 52 |
+
## 🛡️ 数据安全
|
| 53 |
+
|
| 54 |
+
- 用户上传的文件在1小时后自动删除
|
| 55 |
+
- 不存储用户的API Key
|
| 56 |
+
- 仅保留API调用统计数据
|
| 57 |
+
- 所有临时数据定期清理
|
| 58 |
+
|
| 59 |
+
## 🆘 常见问题
|
| 60 |
+
|
| 61 |
+
### Q: 如何获取 MiniMax API Key?
|
| 62 |
+
A: 访问 [MiniMax 官网](https://api.minimaxi.com) 注册账户并获取 API Key。
|
| 63 |
+
|
| 64 |
+
### Q: 文件上传失败怎么办?
|
| 65 |
+
A: 检查文件格式是否正确,确保网络连接稳定。
|
| 66 |
+
|
| 67 |
+
### Q: API 调用被限流怎么办?
|
| 68 |
+
A: 等待限流时间窗口结束,或联系管理员调整限流策略。
|
| 69 |
+
|
| 70 |
+
## 📄 License
|
| 71 |
+
|
| 72 |
+
MIT License
|
app/main.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, Request
|
| 2 |
+
from fastapi.staticfiles import StaticFiles
|
| 3 |
+
from fastapi.templating import Jinja2Templates
|
| 4 |
+
from fastapi.responses import HTMLResponse
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
from routes import music, vlm, voice_design, image_generation
|
| 8 |
+
from utils.monitor import setup_monitoring
|
| 9 |
+
from utils.cleanup import start_cleanup_task
|
| 10 |
+
|
| 11 |
+
app = FastAPI(title="MiniMax Tools", version="1.0.0")
|
| 12 |
+
|
| 13 |
+
# 静态文件和模板
|
| 14 |
+
# 检查 static 目录是否存在
|
| 15 |
+
if os.path.exists("static"):
|
| 16 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 17 |
+
templates = Jinja2Templates(directory="templates")
|
| 18 |
+
|
| 19 |
+
# 设置监控
|
| 20 |
+
setup_monitoring(app)
|
| 21 |
+
|
| 22 |
+
# 启动清理任务
|
| 23 |
+
start_cleanup_task()
|
| 24 |
+
|
| 25 |
+
# 注册路由
|
| 26 |
+
app.include_router(music.router, prefix="/music", tags=["Music"])
|
| 27 |
+
app.include_router(vlm.router, prefix="/vlm", tags=["VLM"])
|
| 28 |
+
app.include_router(voice_design.router, prefix="/voice-design", tags=["Voice Design"])
|
| 29 |
+
app.include_router(image_generation.router, prefix="/image-generation", tags=["Image Generation"])
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@app.get("/", response_class=HTMLResponse)
|
| 33 |
+
async def home(request: Request):
|
| 34 |
+
return templates.TemplateResponse("index.html", {"request": request})
|
| 35 |
+
|
| 36 |
+
@app.get("/admin", response_class=HTMLResponse)
|
| 37 |
+
async def admin(request: Request):
|
| 38 |
+
from utils.monitor import get_stats
|
| 39 |
+
stats = get_stats()
|
| 40 |
+
return templates.TemplateResponse("admin.html", {"request": request, "stats": stats})
|
| 41 |
+
|
| 42 |
+
@app.get("/health")
|
| 43 |
+
async def health_check():
|
| 44 |
+
return {"status": "healthy", "service": "minimax-tools"}
|
| 45 |
+
|
| 46 |
+
if __name__ == "__main__":
|
| 47 |
+
import uvicorn
|
| 48 |
+
port = int(os.getenv("PORT", 7860))
|
| 49 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
app/routes/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Routes package
|
app/routes/image_generation.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException, Request, Form
|
| 2 |
+
from fastapi.responses import HTMLResponse
|
| 3 |
+
from fastapi.templating import Jinja2Templates
|
| 4 |
+
import requests
|
| 5 |
+
import json
|
| 6 |
+
from utils.rate_limiter import check_rate_limit
|
| 7 |
+
|
| 8 |
+
router = APIRouter()
|
| 9 |
+
templates = Jinja2Templates(directory="templates")
|
| 10 |
+
|
| 11 |
+
@router.get("/", response_class=HTMLResponse)
|
| 12 |
+
async def image_generation_page(request: Request):
|
| 13 |
+
return templates.TemplateResponse("image_generation.html", {"request": request})
|
| 14 |
+
|
| 15 |
+
@router.post("/generate")
|
| 16 |
+
async def generate_image(
|
| 17 |
+
request: Request,
|
| 18 |
+
api_key: str = Form(...),
|
| 19 |
+
prompt: str = Form(...),
|
| 20 |
+
aspect_ratio: str = Form("1:1"),
|
| 21 |
+
n: int = Form(3),
|
| 22 |
+
prompt_optimizer: bool = Form(True)
|
| 23 |
+
):
|
| 24 |
+
# 限流检查
|
| 25 |
+
check_rate_limit(request, max_requests=5, window_seconds=300) # 5分钟5次
|
| 26 |
+
|
| 27 |
+
try:
|
| 28 |
+
url = "https://api.minimaxi.com/v1/image_generation"
|
| 29 |
+
|
| 30 |
+
payload = json.dumps({
|
| 31 |
+
"model": "image-01",
|
| 32 |
+
"prompt": prompt,
|
| 33 |
+
"aspect_ratio": aspect_ratio,
|
| 34 |
+
"response_format": "url",
|
| 35 |
+
"n": n,
|
| 36 |
+
"prompt_optimizer": prompt_optimizer
|
| 37 |
+
})
|
| 38 |
+
|
| 39 |
+
headers = {
|
| 40 |
+
'Authorization': f'Bearer {api_key}',
|
| 41 |
+
'Content-Type': 'application/json'
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
response = requests.post(url, headers=headers, data=payload)
|
| 45 |
+
|
| 46 |
+
# 获取 trace_id
|
| 47 |
+
trace_id = response.headers.get('Trace-ID', '')
|
| 48 |
+
|
| 49 |
+
if response.status_code != 200:
|
| 50 |
+
raise HTTPException(status_code=response.status_code, detail="API调用失败")
|
| 51 |
+
|
| 52 |
+
result = response.json()
|
| 53 |
+
|
| 54 |
+
# 检查返回格式
|
| 55 |
+
if 'data' not in result or 'image_urls' not in result['data']:
|
| 56 |
+
raise HTTPException(status_code=400, detail="API返回数据格式错误:缺少image_urls")
|
| 57 |
+
|
| 58 |
+
return {
|
| 59 |
+
"status": "success",
|
| 60 |
+
"message": "图片生成成功",
|
| 61 |
+
"id": result.get('id', ''),
|
| 62 |
+
"image_urls": result['data']['image_urls'],
|
| 63 |
+
"metadata": result.get('metadata', {}),
|
| 64 |
+
"base_resp": result.get('base_resp', {}),
|
| 65 |
+
"trace_id": trace_id
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
except requests.RequestException as e:
|
| 69 |
+
raise HTTPException(status_code=500, detail=f"网络请求错误: {str(e)}")
|
| 70 |
+
except Exception as e:
|
| 71 |
+
raise HTTPException(status_code=500, detail=f"处理错误: {str(e)}")
|
app/routes/music.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException, Request, Form
|
| 2 |
+
from fastapi.responses import HTMLResponse, FileResponse
|
| 3 |
+
from fastapi.templating import Jinja2Templates
|
| 4 |
+
import requests
|
| 5 |
+
import json
|
| 6 |
+
import subprocess
|
| 7 |
+
import time
|
| 8 |
+
import os
|
| 9 |
+
from utils.rate_limiter import check_rate_limit
|
| 10 |
+
from utils.cleanup import create_user_session_dir
|
| 11 |
+
|
| 12 |
+
router = APIRouter()
|
| 13 |
+
templates = Jinja2Templates(directory="templates")
|
| 14 |
+
|
| 15 |
+
def audio_play_and_save(data: str, session_dir: str) -> str:
|
| 16 |
+
"""处理音频数据并保存"""
|
| 17 |
+
# 转换hex为bytes
|
| 18 |
+
audio_bytes = bytes.fromhex(data)
|
| 19 |
+
|
| 20 |
+
# 保存文件
|
| 21 |
+
timestamp = int(time.time())
|
| 22 |
+
file_name = f'music_{timestamp}.mp3'
|
| 23 |
+
file_path = os.path.join(session_dir, file_name)
|
| 24 |
+
|
| 25 |
+
with open(file_path, 'wb') as file:
|
| 26 |
+
file.write(audio_bytes)
|
| 27 |
+
|
| 28 |
+
return file_path
|
| 29 |
+
|
| 30 |
+
@router.get("/", response_class=HTMLResponse)
|
| 31 |
+
async def music_page(request: Request):
|
| 32 |
+
return templates.TemplateResponse("music.html", {"request": request})
|
| 33 |
+
|
| 34 |
+
@router.post("/generate")
|
| 35 |
+
async def generate_music(
|
| 36 |
+
request: Request,
|
| 37 |
+
api_key: str = Form(...),
|
| 38 |
+
prompt: str = Form(...),
|
| 39 |
+
lyrics: str = Form(...)
|
| 40 |
+
):
|
| 41 |
+
# 限流检查
|
| 42 |
+
check_rate_limit(request, max_requests=5, window_seconds=300) # 5分钟5次
|
| 43 |
+
|
| 44 |
+
# 创建用户会话目录
|
| 45 |
+
session_dir, session_id = create_user_session_dir()
|
| 46 |
+
|
| 47 |
+
try:
|
| 48 |
+
url = "https://api.minimaxi.com/v1/music_generation"
|
| 49 |
+
|
| 50 |
+
payload = json.dumps({
|
| 51 |
+
"model": "music-1.5",
|
| 52 |
+
"prompt": prompt,
|
| 53 |
+
"lyrics": lyrics,
|
| 54 |
+
"audio_setting": {
|
| 55 |
+
"sample_rate": 44100,
|
| 56 |
+
"bitrate": 256000,
|
| 57 |
+
"format": "mp3"
|
| 58 |
+
}
|
| 59 |
+
})
|
| 60 |
+
|
| 61 |
+
headers = {
|
| 62 |
+
'Authorization': 'Bearer ' + api_key,
|
| 63 |
+
'Content-Type': 'application/json'
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
response = requests.post(url, headers=headers, data=payload)
|
| 67 |
+
|
| 68 |
+
if response.status_code != 200:
|
| 69 |
+
raise HTTPException(status_code=response.status_code, detail="API调用失败")
|
| 70 |
+
|
| 71 |
+
result = response.json()
|
| 72 |
+
|
| 73 |
+
if 'data' not in result or 'audio' not in result['data']:
|
| 74 |
+
raise HTTPException(status_code=400, detail="API返回数据格式错误")
|
| 75 |
+
|
| 76 |
+
# 处理音频数据
|
| 77 |
+
audio_hex = result['data']['audio']
|
| 78 |
+
file_path = audio_play_and_save(audio_hex, session_dir)
|
| 79 |
+
|
| 80 |
+
return {
|
| 81 |
+
"status": "success",
|
| 82 |
+
"message": "音乐生成成功",
|
| 83 |
+
"file_url": f"/music/download/{session_id}/{os.path.basename(file_path)}",
|
| 84 |
+
"stream_url": f"/music/stream/{session_id}/{os.path.basename(file_path)}",
|
| 85 |
+
"trace_id": result.get('trace_id', ''),
|
| 86 |
+
"base_resp": result.get('base_resp', {})
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
except requests.RequestException as e:
|
| 90 |
+
raise HTTPException(status_code=500, detail=f"网络请求错误: {str(e)}")
|
| 91 |
+
except Exception as e:
|
| 92 |
+
raise HTTPException(status_code=500, detail=f"处理错误: {str(e)}")
|
| 93 |
+
|
| 94 |
+
@router.get("/download/{session_id}/{filename}")
|
| 95 |
+
async def download_music(session_id: str, filename: str):
|
| 96 |
+
temp_base = os.getenv("TMPDIR", "/tmp")
|
| 97 |
+
file_path = os.path.join(temp_base, session_id, filename)
|
| 98 |
+
if os.path.exists(file_path):
|
| 99 |
+
return FileResponse(file_path, media_type="audio/mpeg", filename=filename)
|
| 100 |
+
else:
|
| 101 |
+
raise HTTPException(status_code=404, detail="文件不存在或已过期")
|
| 102 |
+
|
| 103 |
+
@router.get("/stream/{session_id}/{filename}")
|
| 104 |
+
async def stream_music(session_id: str, filename: str):
|
| 105 |
+
"""流式播放音频文件"""
|
| 106 |
+
temp_base = os.getenv("TMPDIR", "/tmp")
|
| 107 |
+
file_path = os.path.join(temp_base, session_id, filename)
|
| 108 |
+
if os.path.exists(file_path):
|
| 109 |
+
return FileResponse(
|
| 110 |
+
file_path,
|
| 111 |
+
media_type="audio/mpeg",
|
| 112 |
+
headers={
|
| 113 |
+
"Accept-Ranges": "bytes",
|
| 114 |
+
"Content-Disposition": "inline"
|
| 115 |
+
}
|
| 116 |
+
)
|
| 117 |
+
else:
|
| 118 |
+
raise HTTPException(status_code=404, detail="文件不存在或已过期")
|
app/routes/vlm.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException, Request, Form, File, UploadFile
|
| 2 |
+
from fastapi.responses import HTMLResponse
|
| 3 |
+
from fastapi.templating import Jinja2Templates
|
| 4 |
+
import base64
|
| 5 |
+
import requests
|
| 6 |
+
import os
|
| 7 |
+
from utils.rate_limiter import check_rate_limit
|
| 8 |
+
from utils.cleanup import create_user_session_dir
|
| 9 |
+
|
| 10 |
+
router = APIRouter()
|
| 11 |
+
templates = Jinja2Templates(directory="templates")
|
| 12 |
+
|
| 13 |
+
@router.get("/", response_class=HTMLResponse)
|
| 14 |
+
async def vlm_page(request: Request):
|
| 15 |
+
return templates.TemplateResponse("vlm.html", {"request": request})
|
| 16 |
+
|
| 17 |
+
@router.post("/analyze")
|
| 18 |
+
async def analyze_image(
|
| 19 |
+
request: Request,
|
| 20 |
+
api_key: str = Form(...),
|
| 21 |
+
question: str = Form(...),
|
| 22 |
+
image_url: str = Form(None),
|
| 23 |
+
image_file: UploadFile = File(None)
|
| 24 |
+
):
|
| 25 |
+
# 限流检查
|
| 26 |
+
check_rate_limit(request, max_requests=10, window_seconds=300) # 5分钟10次
|
| 27 |
+
|
| 28 |
+
try:
|
| 29 |
+
# 处理图像输入
|
| 30 |
+
image_content = None
|
| 31 |
+
|
| 32 |
+
if image_file:
|
| 33 |
+
# 上传文件处理
|
| 34 |
+
session_dir, session_id = create_user_session_dir()
|
| 35 |
+
file_path = os.path.join(session_dir, image_file.filename)
|
| 36 |
+
|
| 37 |
+
content = await image_file.read()
|
| 38 |
+
with open(file_path, "wb") as f:
|
| 39 |
+
f.write(content)
|
| 40 |
+
|
| 41 |
+
# 转换为base64
|
| 42 |
+
image_data = base64.b64encode(content).decode('utf-8')
|
| 43 |
+
image_content = {
|
| 44 |
+
"type": "image_url",
|
| 45 |
+
"image_url": {
|
| 46 |
+
"url": f"data:image/{image_file.content_type.split('/')[-1]};base64,{image_data}"
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
elif image_url:
|
| 50 |
+
# URL形式
|
| 51 |
+
image_content = {
|
| 52 |
+
"type": "image_url",
|
| 53 |
+
"image_url": {
|
| 54 |
+
"url": image_url
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
else:
|
| 58 |
+
raise HTTPException(status_code=400, detail="请提供图片URL或上传图片文件")
|
| 59 |
+
|
| 60 |
+
# 构建请求
|
| 61 |
+
url = "https://api.minimaxi.com/v1/chat/completions"
|
| 62 |
+
|
| 63 |
+
headers = {
|
| 64 |
+
'Authorization': 'Bearer ' + api_key,
|
| 65 |
+
'Content-Type': 'application/json'
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
messages = [
|
| 69 |
+
{
|
| 70 |
+
"role": "system",
|
| 71 |
+
"content": "MM智能助理是一款由MiniMax自研的,没有调用其他产品的接口的大型语言模型。MiniMax是一家中国科技公司,一直致力于进行大模型相关的研究。"
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
"role": "user",
|
| 75 |
+
"name": "用户",
|
| 76 |
+
"content": [
|
| 77 |
+
{
|
| 78 |
+
"type": "text",
|
| 79 |
+
"text": question
|
| 80 |
+
},
|
| 81 |
+
image_content
|
| 82 |
+
]
|
| 83 |
+
}
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
payload = {
|
| 87 |
+
"model": "MiniMax-Text-01",
|
| 88 |
+
"messages": messages,
|
| 89 |
+
"max_tokens": 4096
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
response = requests.post(url, headers=headers, json=payload)
|
| 93 |
+
|
| 94 |
+
# 获取 trace_id
|
| 95 |
+
trace_id = response.headers.get('Trace-ID', '')
|
| 96 |
+
print("Trace-ID:", trace_id)
|
| 97 |
+
|
| 98 |
+
if response.status_code != 200:
|
| 99 |
+
raise HTTPException(status_code=response.status_code, detail="API调用失败")
|
| 100 |
+
|
| 101 |
+
result = response.json()
|
| 102 |
+
|
| 103 |
+
if 'choices' not in result or not result['choices']:
|
| 104 |
+
raise HTTPException(status_code=400, detail="API返回数据格式错误")
|
| 105 |
+
|
| 106 |
+
return {
|
| 107 |
+
"status": "success",
|
| 108 |
+
"message": "图像分析完成",
|
| 109 |
+
"response": result['choices'][0]['message']['content'],
|
| 110 |
+
"usage": result.get('usage', {}),
|
| 111 |
+
"trace_id": trace_id
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
except requests.RequestException as e:
|
| 115 |
+
raise HTTPException(status_code=500, detail=f"网络请求错误: {str(e)}")
|
| 116 |
+
except Exception as e:
|
| 117 |
+
raise HTTPException(status_code=500, detail=f"处理错误: {str(e)}")
|
app/routes/voice_design.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException, Request, Form
|
| 2 |
+
from fastapi.responses import HTMLResponse, FileResponse
|
| 3 |
+
from fastapi.templating import Jinja2Templates
|
| 4 |
+
import requests
|
| 5 |
+
import json
|
| 6 |
+
import base64
|
| 7 |
+
import time
|
| 8 |
+
import os
|
| 9 |
+
from utils.rate_limiter import check_rate_limit
|
| 10 |
+
from utils.cleanup import create_user_session_dir
|
| 11 |
+
|
| 12 |
+
router = APIRouter()
|
| 13 |
+
templates = Jinja2Templates(directory="templates")
|
| 14 |
+
|
| 15 |
+
def audio_save_from_hex(audio_hex: str, session_dir: str) -> str:
|
| 16 |
+
"""从hex编码保存音频文件"""
|
| 17 |
+
# 转换hex为bytes
|
| 18 |
+
audio_bytes = bytes.fromhex(audio_hex)
|
| 19 |
+
|
| 20 |
+
# 保存文件
|
| 21 |
+
timestamp = int(time.time())
|
| 22 |
+
file_name = f'voice_design_{timestamp}.mp3'
|
| 23 |
+
file_path = os.path.join(session_dir, file_name)
|
| 24 |
+
|
| 25 |
+
with open(file_path, 'wb') as file:
|
| 26 |
+
file.write(audio_bytes)
|
| 27 |
+
|
| 28 |
+
return file_path
|
| 29 |
+
|
| 30 |
+
@router.get("/", response_class=HTMLResponse)
|
| 31 |
+
async def voice_design_page(request: Request):
|
| 32 |
+
return templates.TemplateResponse("voice_design.html", {"request": request})
|
| 33 |
+
|
| 34 |
+
@router.post("/design")
|
| 35 |
+
async def design_voice(
|
| 36 |
+
request: Request,
|
| 37 |
+
api_key: str = Form(...),
|
| 38 |
+
prompt: str = Form(...),
|
| 39 |
+
preview_text: str = Form(...)
|
| 40 |
+
):
|
| 41 |
+
# 限流检查
|
| 42 |
+
check_rate_limit(request, max_requests=5, window_seconds=300) # 5分钟5次
|
| 43 |
+
|
| 44 |
+
# 创建用户会话目录
|
| 45 |
+
session_dir, session_id = create_user_session_dir()
|
| 46 |
+
|
| 47 |
+
try:
|
| 48 |
+
url = "https://api.minimaxi.com/v1/voice_design"
|
| 49 |
+
|
| 50 |
+
payload = {
|
| 51 |
+
"prompt": prompt,
|
| 52 |
+
"preview_text": preview_text
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
headers = {
|
| 56 |
+
'Content-Type': 'application/json',
|
| 57 |
+
'Authorization': f'Bearer {api_key}',
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
response = requests.post(url, headers=headers, json=payload)
|
| 61 |
+
|
| 62 |
+
# 获取 trace_id
|
| 63 |
+
trace_id = response.headers.get('Trace-ID', '')
|
| 64 |
+
|
| 65 |
+
if response.status_code != 200:
|
| 66 |
+
raise HTTPException(status_code=response.status_code, detail="API调用失败")
|
| 67 |
+
|
| 68 |
+
result = response.json()
|
| 69 |
+
|
| 70 |
+
# 检查返回格式并处理trial_audio
|
| 71 |
+
if 'trial_audio' not in result:
|
| 72 |
+
raise HTTPException(status_code=400, detail="API返回数据格式错误:缺少trial_audio")
|
| 73 |
+
|
| 74 |
+
# 处理音频数据
|
| 75 |
+
trial_audio_hex = result['trial_audio']
|
| 76 |
+
file_path = audio_save_from_hex(trial_audio_hex, session_dir)
|
| 77 |
+
|
| 78 |
+
return {
|
| 79 |
+
"status": "success",
|
| 80 |
+
"message": "语音设计完成",
|
| 81 |
+
"voice_id": result.get('voice_id', ''),
|
| 82 |
+
"file_url": f"/voice-design/download/{session_id}/{os.path.basename(file_path)}",
|
| 83 |
+
"stream_url": f"/voice-design/stream/{session_id}/{os.path.basename(file_path)}",
|
| 84 |
+
"base_resp": result.get('base_resp', {}),
|
| 85 |
+
"trace_id": trace_id
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
except requests.RequestException as e:
|
| 89 |
+
raise HTTPException(status_code=500, detail=f"网络请求错误: {str(e)}")
|
| 90 |
+
except Exception as e:
|
| 91 |
+
raise HTTPException(status_code=500, detail=f"处理错误: {str(e)}")
|
| 92 |
+
|
| 93 |
+
@router.get("/download/{session_id}/{filename}")
|
| 94 |
+
async def download_voice(session_id: str, filename: str):
|
| 95 |
+
"""下载语音文件"""
|
| 96 |
+
temp_base = os.getenv("TMPDIR", "/tmp")
|
| 97 |
+
file_path = os.path.join(temp_base, session_id, filename)
|
| 98 |
+
if os.path.exists(file_path):
|
| 99 |
+
return FileResponse(file_path, media_type="audio/mpeg", filename=filename)
|
| 100 |
+
else:
|
| 101 |
+
raise HTTPException(status_code=404, detail="文件不存在或已过期")
|
| 102 |
+
|
| 103 |
+
@router.get("/stream/{session_id}/{filename}")
|
| 104 |
+
async def stream_voice(session_id: str, filename: str):
|
| 105 |
+
"""流式播放语音文件"""
|
| 106 |
+
temp_base = os.getenv("TMPDIR", "/tmp")
|
| 107 |
+
file_path = os.path.join(temp_base, session_id, filename)
|
| 108 |
+
if os.path.exists(file_path):
|
| 109 |
+
return FileResponse(
|
| 110 |
+
file_path,
|
| 111 |
+
media_type="audio/mpeg",
|
| 112 |
+
headers={
|
| 113 |
+
"Accept-Ranges": "bytes",
|
| 114 |
+
"Content-Disposition": "inline"
|
| 115 |
+
}
|
| 116 |
+
)
|
| 117 |
+
else:
|
| 118 |
+
raise HTTPException(status_code=404, detail="文件不存在或已过期")
|
app/templates/admin.html
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}监控面板 - MiniMax Tools{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<h2>📊 监控面板</h2>
|
| 7 |
+
<p>查看系统使用统计和性能指标</p>
|
| 8 |
+
|
| 9 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px;">
|
| 10 |
+
<div style="background: #e3f2fd; padding: 20px; border-radius: 8px; text-align: center;">
|
| 11 |
+
<h3 style="margin: 0; color: #1976d2;">{{ stats.total_calls }}</h3>
|
| 12 |
+
<p style="margin: 5px 0 0 0; color: #666;">总调用次数</p>
|
| 13 |
+
</div>
|
| 14 |
+
|
| 15 |
+
<div style="background: #e8f5e8; padding: 20px; border-radius: 8px; text-align: center;">
|
| 16 |
+
<h3 style="margin: 0; color: #388e3c;">{{ stats.calls_24h }}</h3>
|
| 17 |
+
<p style="margin: 5px 0 0 0; color: #666;">24小时调用</p>
|
| 18 |
+
</div>
|
| 19 |
+
|
| 20 |
+
<div style="background: #fff3e0; padding: 20px; border-radius: 8px; text-align: center;">
|
| 21 |
+
<h3 style="margin: 0; color: #f57c00;">{{ stats.error_count }}</h3>
|
| 22 |
+
<p style="margin: 5px 0 0 0; color: #666;">错误次数</p>
|
| 23 |
+
</div>
|
| 24 |
+
|
| 25 |
+
<div style="background: #fce4ec; padding: 20px; border-radius: 8px; text-align: center;">
|
| 26 |
+
<h3 style="margin: 0; color: #c2185b;">{{ stats.error_rate }}%</h3>
|
| 27 |
+
<p style="margin: 5px 0 0 0; color: #666;">错误率</p>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
|
| 31 |
+
<div style="background: white; padding: 20px; border-radius: 8px; border: 1px solid #ddd;">
|
| 32 |
+
<h3>各端点使用统计 (24小时)</h3>
|
| 33 |
+
{% if stats.endpoint_stats %}
|
| 34 |
+
<table style="width: 100%; border-collapse: collapse; margin-top: 15px;">
|
| 35 |
+
<thead>
|
| 36 |
+
<tr style="background: #f8f9fa;">
|
| 37 |
+
<th style="padding: 12px; text-align: left; border-bottom: 1px solid #ddd;">端点</th>
|
| 38 |
+
<th style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">调用次数</th>
|
| 39 |
+
<th style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">平均响应时间(s)</th>
|
| 40 |
+
</tr>
|
| 41 |
+
</thead>
|
| 42 |
+
<tbody>
|
| 43 |
+
{% for endpoint, count, avg_time in stats.endpoint_stats %}
|
| 44 |
+
<tr>
|
| 45 |
+
<td style="padding: 10px; border-bottom: 1px solid #eee;">{{ endpoint }}</td>
|
| 46 |
+
<td style="padding: 10px; text-align: right; border-bottom: 1px solid #eee;">{{ count }}</td>
|
| 47 |
+
<td style="padding: 10px; text-align: right; border-bottom: 1px solid #eee;">{{ "%.3f"|format(avg_time) }}</td>
|
| 48 |
+
</tr>
|
| 49 |
+
{% endfor %}
|
| 50 |
+
</tbody>
|
| 51 |
+
</table>
|
| 52 |
+
{% else %}
|
| 53 |
+
<p style="color: #666; margin-top: 15px;">暂无数据</p>
|
| 54 |
+
{% endif %}
|
| 55 |
+
</div>
|
| 56 |
+
|
| 57 |
+
<div style="margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
|
| 58 |
+
<h4>系统信息</h4>
|
| 59 |
+
<ul style="margin: 10px 0 0 20px;">
|
| 60 |
+
<li>数据自动清理:每15分钟清理过期文件</li>
|
| 61 |
+
<li>限流策略:各功能模块独立限流</li>
|
| 62 |
+
<li>数据保留:用户文件1小时后自动删除</li>
|
| 63 |
+
<li>监控数据:永久保存API调用统计</li>
|
| 64 |
+
</ul>
|
| 65 |
+
</div>
|
| 66 |
+
|
| 67 |
+
<div style="text-align: center; margin-top: 30px;">
|
| 68 |
+
<button onclick="location.reload()" style="background: #28a745;">刷新数据</button>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
<script>
|
| 72 |
+
// 每30秒自动刷新数据
|
| 73 |
+
setInterval(function() {
|
| 74 |
+
location.reload();
|
| 75 |
+
}, 30000);
|
| 76 |
+
</script>
|
| 77 |
+
{% endblock %}
|
app/templates/base.html
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>{% block title %}MiniMax Tools{% endblock %}</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
body {
|
| 15 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
| 16 |
+
line-height: 1.6;
|
| 17 |
+
color: #333;
|
| 18 |
+
background-color: #f5f5f5;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.container {
|
| 22 |
+
max-width: 800px;
|
| 23 |
+
margin: 0 auto;
|
| 24 |
+
padding: 20px;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.header {
|
| 28 |
+
background: white;
|
| 29 |
+
padding: 20px;
|
| 30 |
+
border-radius: 8px;
|
| 31 |
+
margin-bottom: 20px;
|
| 32 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.nav {
|
| 36 |
+
display: flex;
|
| 37 |
+
gap: 15px;
|
| 38 |
+
margin-top: 15px;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.nav a {
|
| 42 |
+
color: #007bff;
|
| 43 |
+
text-decoration: none;
|
| 44 |
+
padding: 8px 16px;
|
| 45 |
+
border-radius: 4px;
|
| 46 |
+
transition: background-color 0.2s;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.nav a:hover {
|
| 50 |
+
background-color: #e9ecef;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.content {
|
| 54 |
+
background: white;
|
| 55 |
+
padding: 30px;
|
| 56 |
+
border-radius: 8px;
|
| 57 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.form-group {
|
| 61 |
+
margin-bottom: 20px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
label {
|
| 65 |
+
display: block;
|
| 66 |
+
margin-bottom: 5px;
|
| 67 |
+
font-weight: 500;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
input, textarea, select {
|
| 71 |
+
width: 100%;
|
| 72 |
+
padding: 10px;
|
| 73 |
+
border: 1px solid #ddd;
|
| 74 |
+
border-radius: 4px;
|
| 75 |
+
font-size: 14px;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
textarea {
|
| 79 |
+
height: 100px;
|
| 80 |
+
resize: vertical;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
button {
|
| 84 |
+
background: #007bff;
|
| 85 |
+
color: white;
|
| 86 |
+
border: none;
|
| 87 |
+
padding: 12px 24px;
|
| 88 |
+
border-radius: 4px;
|
| 89 |
+
cursor: pointer;
|
| 90 |
+
font-size: 16px;
|
| 91 |
+
transition: background-color 0.2s;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
button:hover {
|
| 95 |
+
background: #0056b3;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
button:disabled {
|
| 99 |
+
background: #6c757d;
|
| 100 |
+
cursor: not-allowed;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.result {
|
| 104 |
+
margin-top: 20px;
|
| 105 |
+
padding: 15px;
|
| 106 |
+
border-radius: 4px;
|
| 107 |
+
background: #d4edda;
|
| 108 |
+
border: 1px solid #c3e6cb;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.error {
|
| 112 |
+
background: #f8d7da;
|
| 113 |
+
border: 1px solid #f5c6cb;
|
| 114 |
+
color: #721c24;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.loading {
|
| 118 |
+
display: none;
|
| 119 |
+
text-align: center;
|
| 120 |
+
margin: 20px 0;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.spinner {
|
| 124 |
+
border: 3px solid #f3f3f3;
|
| 125 |
+
border-top: 3px solid #007bff;
|
| 126 |
+
border-radius: 50%;
|
| 127 |
+
width: 30px;
|
| 128 |
+
height: 30px;
|
| 129 |
+
animation: spin 1s linear infinite;
|
| 130 |
+
margin: 0 auto;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
@keyframes spin {
|
| 134 |
+
0% { transform: rotate(0deg); }
|
| 135 |
+
100% { transform: rotate(360deg); }
|
| 136 |
+
}
|
| 137 |
+
</style>
|
| 138 |
+
</head>
|
| 139 |
+
<body>
|
| 140 |
+
<div class="container">
|
| 141 |
+
<div class="header">
|
| 142 |
+
<h1>MiniMax Tools</h1>
|
| 143 |
+
<nav class="nav">
|
| 144 |
+
<a href="/">首页</a>
|
| 145 |
+
<a href="/music">音乐生成</a>
|
| 146 |
+
<a href="/image-generation">图片生成</a>
|
| 147 |
+
<a href="/vlm">图像分析</a>
|
| 148 |
+
<a href="/voice-design">语音设计</a>
|
| 149 |
+
</nav>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
<div class="content">
|
| 153 |
+
{% block content %}{% endblock %}
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
<script>
|
| 158 |
+
function showLoading() {
|
| 159 |
+
document.querySelector('.loading').style.display = 'block';
|
| 160 |
+
document.querySelector('button[type="submit"]').disabled = true;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
function hideLoading() {
|
| 164 |
+
document.querySelector('.loading').style.display = 'none';
|
| 165 |
+
document.querySelector('button[type="submit"]').disabled = false;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
function showResult(message, isError = false) {
|
| 169 |
+
const resultDiv = document.getElementById('result');
|
| 170 |
+
if (resultDiv) {
|
| 171 |
+
resultDiv.className = isError ? 'result error' : 'result';
|
| 172 |
+
resultDiv.textContent = message;
|
| 173 |
+
resultDiv.style.display = 'block';
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
</script>
|
| 177 |
+
</body>
|
| 178 |
+
</html>
|
app/templates/image_generation.html
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}图片生成 - MiniMax Tools{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<h2>🖼️ 图片生成</h2>
|
| 7 |
+
<p>使用 MiniMax API 根据文本描述生成高质量图片</p>
|
| 8 |
+
|
| 9 |
+
<form id="imageForm" onsubmit="generateImage(event)">
|
| 10 |
+
<div class="form-group">
|
| 11 |
+
<label for="api_key">API Key *</label>
|
| 12 |
+
<input type="password" id="api_key" name="api_key" required
|
| 13 |
+
placeholder="请输入您的 MiniMax API Key">
|
| 14 |
+
</div>
|
| 15 |
+
|
| 16 |
+
<div class="form-group">
|
| 17 |
+
<label for="prompt">图片描述 *</label>
|
| 18 |
+
<textarea id="prompt" name="prompt" required
|
| 19 |
+
placeholder="详细描述您想要生成的图片,如风格、内容、场景等">men Dressing in white t shirt, full-body stand front view image :25, outdoor, Venice beach sign, full-body image, Los Angeles, Fashion photography of 90s, documentary, Film grain, photorealistic</textarea>
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
<div class="form-group">
|
| 23 |
+
<label for="aspect_ratio">宽高比</label>
|
| 24 |
+
<select id="aspect_ratio" name="aspect_ratio">
|
| 25 |
+
<option value="1:1" selected>1:1 (正方形)</option>
|
| 26 |
+
<option value="16:9">16:9 (宽屏)</option>
|
| 27 |
+
<option value="9:16">9:16 (竖屏)</option>
|
| 28 |
+
<option value="4:3">4:3 (标准)</option>
|
| 29 |
+
<option value="3:2">3:2 (经典)</option>
|
| 30 |
+
<option value="2:3">2:3 (竖版经典)</option>
|
| 31 |
+
<option value="3:4">3:4 (竖版标准)</option>
|
| 32 |
+
<option value="21:9">21:9 (超宽屏)</option>
|
| 33 |
+
</select>
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
<div class="form-group">
|
| 37 |
+
<label for="n">生成数量</label>
|
| 38 |
+
<select id="n" name="n">
|
| 39 |
+
<option value="1">1张</option>
|
| 40 |
+
<option value="2">2张</option>
|
| 41 |
+
<option value="3" selected>3张</option>
|
| 42 |
+
<option value="4">4张</option>
|
| 43 |
+
</select>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<div class="form-group">
|
| 47 |
+
<label>
|
| 48 |
+
<input type="checkbox" id="prompt_optimizer" name="prompt_optimizer" checked>
|
| 49 |
+
启用提示词优化
|
| 50 |
+
</label>
|
| 51 |
+
<small style="color: #666; display: block; margin-top: 5px;">
|
| 52 |
+
自动优化您的提示词以获得更好的生成效果
|
| 53 |
+
</small>
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
<button type="submit">生成图片</button>
|
| 57 |
+
</form>
|
| 58 |
+
|
| 59 |
+
<div class="loading">
|
| 60 |
+
<div class="spinner"></div>
|
| 61 |
+
<p>正在生成图片,请稍候...</p>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
<div id="result" style="display: none;"></div>
|
| 65 |
+
|
| 66 |
+
<script>
|
| 67 |
+
async function generateImage(event) {
|
| 68 |
+
event.preventDefault();
|
| 69 |
+
showLoading();
|
| 70 |
+
|
| 71 |
+
const formData = new FormData(event.target);
|
| 72 |
+
|
| 73 |
+
try {
|
| 74 |
+
const response = await fetch('/image-generation/generate', {
|
| 75 |
+
method: 'POST',
|
| 76 |
+
body: formData
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
const result = await response.json();
|
| 80 |
+
|
| 81 |
+
if (response.ok) {
|
| 82 |
+
const resultDiv = document.getElementById('result');
|
| 83 |
+
resultDiv.className = 'result';
|
| 84 |
+
|
| 85 |
+
let imagesHtml = '';
|
| 86 |
+
if (result.image_urls && result.image_urls.length > 0) {
|
| 87 |
+
imagesHtml = result.image_urls.map((url, index) => `
|
| 88 |
+
<div style="margin-bottom: 15px;">
|
| 89 |
+
<p style="font-weight: bold; margin-bottom: 8px;">图片 ${index + 1}:</p>
|
| 90 |
+
<img src="${url}" alt="生成的图片 ${index + 1}"
|
| 91 |
+
style="max-width: 100%; height: auto; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
| 92 |
+
<div style="margin-top: 8px;">
|
| 93 |
+
<a href="${url}" target="_blank"
|
| 94 |
+
style="color: #007bff; text-decoration: none; background: #e9ecef; padding: 6px 12px; border-radius: 4px; font-size: 14px; margin-right: 10px;">
|
| 95 |
+
🔗 查看原图
|
| 96 |
+
</a>
|
| 97 |
+
<button onclick="downloadImage('${url}', 'generated_image_${index + 1}.jpg')"
|
| 98 |
+
style="background: #28a745; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 14px;">
|
| 99 |
+
📥 下载图片
|
| 100 |
+
</button>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
`).join('');
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
resultDiv.innerHTML = `
|
| 107 |
+
<p style="color: green; font-weight: bold;">✅ 图片生成成功!</p>
|
| 108 |
+
|
| 109 |
+
${result.id ? `
|
| 110 |
+
<div style="background: #e7f3ff; padding: 12px; border-radius: 6px; margin: 15px 0; border-left: 4px solid #007bff;">
|
| 111 |
+
<p style="margin: 0; font-weight: bold; color: #007bff;">🆔 生成ID</p>
|
| 112 |
+
<p style="margin: 5px 0 0 0; font-family: monospace; font-size: 14px; background: white; padding: 8px; border-radius: 4px;">${result.id}</p>
|
| 113 |
+
</div>
|
| 114 |
+
` : ''}
|
| 115 |
+
|
| 116 |
+
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin: 15px 0;">
|
| 117 |
+
<p style="margin-bottom: 15px;"><strong>🖼️ 生成结果:</strong></p>
|
| 118 |
+
${imagesHtml}
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
${result.metadata ? `
|
| 122 |
+
<div style="background: #d4edda; padding: 10px; border-radius: 4px; margin: 10px 0; border: 1px solid #c3e6cb;">
|
| 123 |
+
<p style="margin: 0; color: #155724; font-size: 14px;">
|
| 124 |
+
📊 生成统计: 成功 ${result.metadata.success_count || 0} 张,失败 ${result.metadata.failed_count || 0} 张
|
| 125 |
+
</p>
|
| 126 |
+
</div>
|
| 127 |
+
` : ''}
|
| 128 |
+
|
| 129 |
+
${result.base_resp && result.base_resp.status_code === 0 ? `
|
| 130 |
+
<div style="background: #d4edda; padding: 10px; border-radius: 4px; margin: 10px 0; border: 1px solid #c3e6cb;">
|
| 131 |
+
<p style="margin: 0; color: #155724; font-size: 14px;">
|
| 132 |
+
✅ API状态: ${result.base_resp.status_msg || 'success'}
|
| 133 |
+
</p>
|
| 134 |
+
</div>
|
| 135 |
+
` : ''}
|
| 136 |
+
|
| 137 |
+
${result.trace_id ? `<p style="font-size: 12px; color: #6c757d;">追踪ID: ${result.trace_id}</p>` : ''}
|
| 138 |
+
`;
|
| 139 |
+
resultDiv.style.display = 'block';
|
| 140 |
+
} else {
|
| 141 |
+
showResult(`错误: ${result.detail}`, true);
|
| 142 |
+
}
|
| 143 |
+
} catch (error) {
|
| 144 |
+
showResult(`网络错误: ${error.message}`, true);
|
| 145 |
+
} finally {
|
| 146 |
+
hideLoading();
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
function downloadImage(url, filename) {
|
| 151 |
+
const link = document.createElement('a');
|
| 152 |
+
link.href = url;
|
| 153 |
+
link.download = filename;
|
| 154 |
+
link.target = '_blank';
|
| 155 |
+
document.body.appendChild(link);
|
| 156 |
+
link.click();
|
| 157 |
+
document.body.removeChild(link);
|
| 158 |
+
}
|
| 159 |
+
</script>
|
| 160 |
+
{% endblock %}
|
app/templates/index.html
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}MiniMax Tools - 首页{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<h2>欢迎使用 MiniMax Tools</h2>
|
| 7 |
+
<p>这是一个基于 MiniMax API 的工具集合,包含以下功能:</p>
|
| 8 |
+
|
| 9 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-top: 30px;">
|
| 10 |
+
<div style="border: 1px solid #ddd; padding: 20px; border-radius: 8px;">
|
| 11 |
+
<h3>🎵 音乐生成</h3>
|
| 12 |
+
<p>基于文本提示和歌词生成音乐</p>
|
| 13 |
+
<a href="/music" style="color: #007bff; text-decoration: none;">开始使用 →</a>
|
| 14 |
+
</div>
|
| 15 |
+
|
| 16 |
+
<div style="border: 1px solid #ddd; padding: 20px; border-radius: 8px;">
|
| 17 |
+
<h3>🖼️ 图片生成</h3>
|
| 18 |
+
<p>基于文本描述生成高质量图片</p>
|
| 19 |
+
<a href="/image-generation" style="color: #007bff; text-decoration: none;">开始使用 →</a>
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
<div style="border: 1px solid #ddd; padding: 20px; border-radius: 8px;">
|
| 23 |
+
<h3>👁️ 图像分析</h3>
|
| 24 |
+
<p>使用视觉语言模型分析图片内容</p>
|
| 25 |
+
<a href="/vlm" style="color: #007bff; text-decoration: none;">开始使用 →</a>
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
<div style="border: 1px solid #ddd; padding: 20px; border-radius: 8px;">
|
| 30 |
+
<h3>🔊 语音设计</h3>
|
| 31 |
+
<p>根据描述设计定制化语音</p>
|
| 32 |
+
<a href="/voice-design" style="color: #007bff; text-decoration: none;">开始使用 →</a>
|
| 33 |
+
</div>
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
</div>
|
| 37 |
+
|
| 38 |
+
<div style="margin-top: 40px; padding: 20px; background: #e3f2fd; border-radius: 8px;">
|
| 39 |
+
<h3>使用说明</h3>
|
| 40 |
+
<ul style="margin-left: 20px; margin-top: 10px;">
|
| 41 |
+
<li>所有功能都需要您提供自己的 MiniMax API Key</li>
|
| 42 |
+
<li>用户数据会在使用后自动清理,无长期存储</li>
|
| 43 |
+
<li>本工具仅供测试,随时可能停服更新,如需商用请自行部署</li>
|
| 44 |
+
<li>开源版本:<a href="https://github.com/backearth1/minimax-tools.git" target="_blank" style="color: #007bff;">https://github.com/backearth1/minimax-tools.git</a></li>
|
| 45 |
+
</ul>
|
| 46 |
+
</div>
|
| 47 |
+
{% endblock %}
|
app/templates/music.html
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}音乐生成 - MiniMax Tools{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<h2>🎵 音乐生成</h2>
|
| 7 |
+
<p>使用 MiniMax API 根据提示词和歌词生成音乐</p>
|
| 8 |
+
|
| 9 |
+
<form id="musicForm" onsubmit="generateMusic(event)">
|
| 10 |
+
<div class="form-group">
|
| 11 |
+
<label for="api_key">API Key *</label>
|
| 12 |
+
<input type="password" id="api_key" name="api_key" required
|
| 13 |
+
placeholder="请输入您的 MiniMax API Key">
|
| 14 |
+
</div>
|
| 15 |
+
|
| 16 |
+
<div class="form-group">
|
| 17 |
+
<label for="prompt">音乐风格描述 *</label>
|
| 18 |
+
<input type="text" id="prompt" name="prompt" required
|
| 19 |
+
placeholder="例如:独立民谣,忧郁,内省,渴望,独自漫步,咖啡馆"
|
| 20 |
+
value="独立民谣,忧郁,内省,渴望,独自漫步,咖啡馆">
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<div class="form-group">
|
| 24 |
+
<label for="lyrics">歌词 *</label>
|
| 25 |
+
<textarea id="lyrics" name="lyrics" required
|
| 26 |
+
placeholder="请输入歌词,支持 [verse]、[chorus] 等标记">[verse]
|
| 27 |
+
街灯微亮晚风轻抚
|
| 28 |
+
影子拉长独自漫步
|
| 29 |
+
旧外套裹着深深忧郁
|
| 30 |
+
不知去向渴望何处
|
| 31 |
+
[chorus]
|
| 32 |
+
推开木门香气弥漫
|
| 33 |
+
熟悉的角落陌生人看</textarea>
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
<button type="submit">生成音乐</button>
|
| 37 |
+
</form>
|
| 38 |
+
|
| 39 |
+
<div class="loading">
|
| 40 |
+
<div class="spinner"></div>
|
| 41 |
+
<p>正在生成音乐,请稍候...</p>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<div id="result" style="display: none;"></div>
|
| 45 |
+
|
| 46 |
+
<script>
|
| 47 |
+
async function generateMusic(event) {
|
| 48 |
+
event.preventDefault();
|
| 49 |
+
showLoading();
|
| 50 |
+
|
| 51 |
+
const formData = new FormData(event.target);
|
| 52 |
+
|
| 53 |
+
try {
|
| 54 |
+
const response = await fetch('/music/generate', {
|
| 55 |
+
method: 'POST',
|
| 56 |
+
body: formData
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
const result = await response.json();
|
| 60 |
+
|
| 61 |
+
if (response.ok) {
|
| 62 |
+
showResult(`音乐生成成功!`, false);
|
| 63 |
+
|
| 64 |
+
// 显示播放器和下载链接
|
| 65 |
+
const resultDiv = document.getElementById('result');
|
| 66 |
+
resultDiv.innerHTML = `
|
| 67 |
+
<p style="color: green; font-weight: bold;">✅ 音乐生成成功!</p>
|
| 68 |
+
|
| 69 |
+
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin: 15px 0;">
|
| 70 |
+
<p style="margin-bottom: 10px;"><strong>🎵 在线播放:</strong></p>
|
| 71 |
+
<audio controls style="width: 100%; margin-bottom: 10px;">
|
| 72 |
+
<source src="${result.stream_url}" type="audio/mpeg">
|
| 73 |
+
您的浏览器不支持音频播放。
|
| 74 |
+
</audio>
|
| 75 |
+
|
| 76 |
+
<div style="display: flex; gap: 10px; align-items: center;">
|
| 77 |
+
<a href="${result.file_url}" download style="color: #007bff; text-decoration: none; background: #e9ecef; padding: 8px 16px; border-radius: 4px; font-size: 14px;">
|
| 78 |
+
📥 下载音乐文件
|
| 79 |
+
</a>
|
| 80 |
+
<span style="color: #6c757d; font-size: 12px;">文件将在1小时后自动删除</span>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
${result.trace_id ? `<p style="font-size: 12px; color: #6c757d;">追踪ID: ${result.trace_id}</p>` : ''}
|
| 85 |
+
`;
|
| 86 |
+
} else {
|
| 87 |
+
showResult(`错误: ${result.detail}`, true);
|
| 88 |
+
}
|
| 89 |
+
} catch (error) {
|
| 90 |
+
showResult(`网络错误: ${error.message}`, true);
|
| 91 |
+
} finally {
|
| 92 |
+
hideLoading();
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
</script>
|
| 96 |
+
{% endblock %}
|
app/templates/vlm.html
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}图像分析 - MiniMax Tools{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<h2>👁️ 图像分析</h2>
|
| 7 |
+
<p>使用 MiniMax 视觉语言模型分析图片内容</p>
|
| 8 |
+
|
| 9 |
+
<form id="vlmForm" onsubmit="analyzeImage(event)">
|
| 10 |
+
<div class="form-group">
|
| 11 |
+
<label for="api_key">API Key *</label>
|
| 12 |
+
<input type="password" id="api_key" name="api_key" required
|
| 13 |
+
placeholder="请输入您的 MiniMax API Key">
|
| 14 |
+
</div>
|
| 15 |
+
|
| 16 |
+
<div class="form-group">
|
| 17 |
+
<label for="question">问题 *</label>
|
| 18 |
+
<input type="text" id="question" name="question" required
|
| 19 |
+
placeholder="请描述您想了解图片的哪些内容"
|
| 20 |
+
value="这个图代表的是什么呢">
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;">
|
| 24 |
+
<div class="form-group">
|
| 25 |
+
<label for="image_url">图片URL</label>
|
| 26 |
+
<input type="url" id="image_url" name="image_url"
|
| 27 |
+
placeholder="https://example.com/image.jpg">
|
| 28 |
+
</div>
|
| 29 |
+
|
| 30 |
+
<div class="form-group">
|
| 31 |
+
<label for="image_file">或上传图片</label>
|
| 32 |
+
<input type="file" id="image_file" name="image_file"
|
| 33 |
+
accept="image/*">
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
|
| 37 |
+
<p style="font-size: 14px; color: #666; margin-bottom: 20px;">
|
| 38 |
+
* 请提供图片URL或上传图片文件(二选一)
|
| 39 |
+
</p>
|
| 40 |
+
|
| 41 |
+
<button type="submit">分析图像</button>
|
| 42 |
+
</form>
|
| 43 |
+
|
| 44 |
+
<div class="loading">
|
| 45 |
+
<div class="spinner"></div>
|
| 46 |
+
<p>正在分析图像,请稍候...</p>
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
<div id="result" style="display: none;"></div>
|
| 50 |
+
|
| 51 |
+
<script>
|
| 52 |
+
async function analyzeImage(event) {
|
| 53 |
+
event.preventDefault();
|
| 54 |
+
showLoading();
|
| 55 |
+
|
| 56 |
+
const formData = new FormData(event.target);
|
| 57 |
+
const imageUrl = formData.get('image_url');
|
| 58 |
+
const imageFile = formData.get('image_file');
|
| 59 |
+
|
| 60 |
+
// 验证输入
|
| 61 |
+
if (!imageUrl && (!imageFile || imageFile.size === 0)) {
|
| 62 |
+
showResult('请提供图片URL或上传图片文件', true);
|
| 63 |
+
hideLoading();
|
| 64 |
+
return;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
try {
|
| 68 |
+
const response = await fetch('/vlm/analyze', {
|
| 69 |
+
method: 'POST',
|
| 70 |
+
body: formData
|
| 71 |
+
});
|
| 72 |
+
|
| 73 |
+
const result = await response.json();
|
| 74 |
+
|
| 75 |
+
if (response.ok) {
|
| 76 |
+
const resultDiv = document.getElementById('result');
|
| 77 |
+
resultDiv.className = 'result';
|
| 78 |
+
resultDiv.innerHTML = `
|
| 79 |
+
<p style="color: green; font-weight: bold;">✅ 图像分析完成!</p>
|
| 80 |
+
<div style="background: #f8f9fa; padding: 15px; border-radius: 4px; margin-top: 10px;">
|
| 81 |
+
<strong>分析结果:</strong>
|
| 82 |
+
<p style="margin-top: 8px; line-height: 1.6;">${result.response}</p>
|
| 83 |
+
</div>
|
| 84 |
+
${result.usage ? `<p><small>使用情况: ${JSON.stringify(result.usage)}</small></p>` : ''}
|
| 85 |
+
${result.trace_id ? `<p><small>追踪ID: ${result.trace_id}</small></p>` : ''}
|
| 86 |
+
`;
|
| 87 |
+
resultDiv.style.display = 'block';
|
| 88 |
+
} else {
|
| 89 |
+
showResult(`错误: ${result.detail}`, true);
|
| 90 |
+
}
|
| 91 |
+
} catch (error) {
|
| 92 |
+
showResult(`网络错误: ${error.message}`, true);
|
| 93 |
+
} finally {
|
| 94 |
+
hideLoading();
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
// 当选择文件时清空URL,反之亦然
|
| 99 |
+
document.getElementById('image_file').addEventListener('change', function() {
|
| 100 |
+
if (this.files.length > 0) {
|
| 101 |
+
document.getElementById('image_url').value = '';
|
| 102 |
+
}
|
| 103 |
+
});
|
| 104 |
+
|
| 105 |
+
document.getElementById('image_url').addEventListener('input', function() {
|
| 106 |
+
if (this.value) {
|
| 107 |
+
document.getElementById('image_file').value = '';
|
| 108 |
+
}
|
| 109 |
+
});
|
| 110 |
+
</script>
|
| 111 |
+
{% endblock %}
|
app/templates/voice_design.html
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}语音设计 - MiniMax Tools{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<h2>🔊 语音设计</h2>
|
| 7 |
+
<p>根据描述设计个性化语音,打造独特的语音风格</p>
|
| 8 |
+
|
| 9 |
+
<form id="voiceDesignForm" onsubmit="designVoice(event)">
|
| 10 |
+
<div class="form-group">
|
| 11 |
+
<label for="api_key">API Key *</label>
|
| 12 |
+
<input type="password" id="api_key" name="api_key" required
|
| 13 |
+
placeholder="请输入您的 MiniMax API Key">
|
| 14 |
+
</div>
|
| 15 |
+
|
| 16 |
+
<div class="form-group">
|
| 17 |
+
<label for="prompt">语音风格描述 *</label>
|
| 18 |
+
<textarea id="prompt" name="prompt" required
|
| 19 |
+
placeholder="详细描述您想要的语音特征,如音色、语速、情感等">讲述悬疑故事的播音员,声音低沉富有磁性,语速时快时慢,营造紧张神秘的氛围。</textarea>
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
<div class="form-group">
|
| 23 |
+
<label for="preview_text">预览文本 *</label>
|
| 24 |
+
<textarea id="preview_text" name="preview_text" required
|
| 25 |
+
placeholder="用于预览语音效果的文本内容">夜深了,古屋里只有他一人。窗外传来若有若无的脚步声,他屏住呼吸,慢慢地,慢慢地,走向那扇吱呀作响的门……</textarea>
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<button type="submit">设计语音</button>
|
| 29 |
+
</form>
|
| 30 |
+
|
| 31 |
+
<div class="loading">
|
| 32 |
+
<div class="spinner"></div>
|
| 33 |
+
<p>正在设计语音,请稍候...</p>
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
<div id="result" style="display: none;"></div>
|
| 37 |
+
|
| 38 |
+
<script>
|
| 39 |
+
async function designVoice(event) {
|
| 40 |
+
event.preventDefault();
|
| 41 |
+
showLoading();
|
| 42 |
+
|
| 43 |
+
const formData = new FormData(event.target);
|
| 44 |
+
|
| 45 |
+
try {
|
| 46 |
+
const response = await fetch('/voice-design/design', {
|
| 47 |
+
method: 'POST',
|
| 48 |
+
body: formData
|
| 49 |
+
});
|
| 50 |
+
|
| 51 |
+
const result = await response.json();
|
| 52 |
+
|
| 53 |
+
if (response.ok) {
|
| 54 |
+
const resultDiv = document.getElementById('result');
|
| 55 |
+
resultDiv.className = 'result';
|
| 56 |
+
resultDiv.innerHTML = `
|
| 57 |
+
<p style="color: green; font-weight: bold;">✅ 语音设计完成!</p>
|
| 58 |
+
|
| 59 |
+
${result.voice_id ? `
|
| 60 |
+
<div style="background: #e7f3ff; padding: 12px; border-radius: 6px; margin: 15px 0; border-left: 4px solid #007bff;">
|
| 61 |
+
<p style="margin: 0; font-weight: bold; color: #007bff;">🎤 Voice ID</p>
|
| 62 |
+
<p style="margin: 5px 0 0 0; font-family: monospace; font-size: 14px; background: white; padding: 8px; border-radius: 4px;">${result.voice_id}</p>
|
| 63 |
+
</div>
|
| 64 |
+
` : ''}
|
| 65 |
+
|
| 66 |
+
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin: 15px 0;">
|
| 67 |
+
<p style="margin-bottom: 10px;"><strong>🔊 试听预览:</strong></p>
|
| 68 |
+
<audio controls style="width: 100%; margin-bottom: 10px;">
|
| 69 |
+
<source src="${result.stream_url}" type="audio/mpeg">
|
| 70 |
+
您的浏览器不支持音频播放。
|
| 71 |
+
</audio>
|
| 72 |
+
|
| 73 |
+
<div style="display: flex; gap: 10px; align-items: center;">
|
| 74 |
+
<a href="${result.file_url}" download style="color: #007bff; text-decoration: none; background: #e9ecef; padding: 8px 16px; border-radius: 4px; font-size: 14px;">
|
| 75 |
+
📥 下载音频文件
|
| 76 |
+
</a>
|
| 77 |
+
<span style="color: #6c757d; font-size: 12px;">文件将在1小时后自动删除</span>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
${result.base_resp && result.base_resp.status_code === 0 ? `
|
| 82 |
+
<div style="background: #d4edda; padding: 10px; border-radius: 4px; margin: 10px 0; border: 1px solid #c3e6cb;">
|
| 83 |
+
<p style="margin: 0; color: #155724; font-size: 14px;">
|
| 84 |
+
✅ API状态: ${result.base_resp.status_msg || 'success'}
|
| 85 |
+
</p>
|
| 86 |
+
</div>
|
| 87 |
+
` : ''}
|
| 88 |
+
|
| 89 |
+
${result.trace_id ? `<p style="font-size: 12px; color: #6c757d;">追踪ID: ${result.trace_id}</p>` : ''}
|
| 90 |
+
|
| 91 |
+
<p style="margin-top: 15px; color: #666; font-size: 14px;">
|
| 92 |
+
💡 语音设计完成,您可以使用生成的 Voice ID 进行语音合成
|
| 93 |
+
</p>
|
| 94 |
+
`;
|
| 95 |
+
resultDiv.style.display = 'block';
|
| 96 |
+
} else {
|
| 97 |
+
showResult(`错误: ${result.detail}`, true);
|
| 98 |
+
}
|
| 99 |
+
} catch (error) {
|
| 100 |
+
showResult(`网络错误: ${error.message}`, true);
|
| 101 |
+
} finally {
|
| 102 |
+
hideLoading();
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
</script>
|
| 106 |
+
{% endblock %}
|
app/utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Utils package
|
app/utils/cleanup.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import shutil
|
| 3 |
+
import time
|
| 4 |
+
import threading
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
import asyncio
|
| 7 |
+
|
| 8 |
+
def cleanup_user_data(max_age_hours: int = 1):
|
| 9 |
+
"""清理用户临时数据
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
max_age_hours: 文件最大保存时间(小时)
|
| 13 |
+
"""
|
| 14 |
+
temp_dir = os.getenv("TMPDIR", "/tmp")
|
| 15 |
+
if not os.path.exists(temp_dir):
|
| 16 |
+
return
|
| 17 |
+
|
| 18 |
+
cutoff_time = time.time() - (max_age_hours * 3600)
|
| 19 |
+
cleaned_count = 0
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
for item in os.listdir(temp_dir):
|
| 23 |
+
item_path = os.path.join(temp_dir, item)
|
| 24 |
+
|
| 25 |
+
# 检查文件/目录的修改时间
|
| 26 |
+
if os.path.getmtime(item_path) < cutoff_time:
|
| 27 |
+
if os.path.isdir(item_path):
|
| 28 |
+
shutil.rmtree(item_path)
|
| 29 |
+
else:
|
| 30 |
+
os.remove(item_path)
|
| 31 |
+
cleaned_count += 1
|
| 32 |
+
|
| 33 |
+
print(f"[{datetime.now()}] 清理完成: 删除了 {cleaned_count} 个过期文件/目录")
|
| 34 |
+
|
| 35 |
+
except Exception as e:
|
| 36 |
+
print(f"[{datetime.now()}] 清理任务出错: {e}")
|
| 37 |
+
|
| 38 |
+
def periodic_cleanup():
|
| 39 |
+
"""定期清理任务"""
|
| 40 |
+
while True:
|
| 41 |
+
try:
|
| 42 |
+
cleanup_user_data()
|
| 43 |
+
# 清理限流记录
|
| 44 |
+
from .rate_limiter import cleanup_expired_records
|
| 45 |
+
cleanup_expired_records()
|
| 46 |
+
|
| 47 |
+
# 每15分钟清理一次
|
| 48 |
+
time.sleep(900)
|
| 49 |
+
except Exception as e:
|
| 50 |
+
print(f"定期清理任务出错: {e}")
|
| 51 |
+
time.sleep(900)
|
| 52 |
+
|
| 53 |
+
def start_cleanup_task():
|
| 54 |
+
"""启动清理任务"""
|
| 55 |
+
cleanup_thread = threading.Thread(target=periodic_cleanup, daemon=True)
|
| 56 |
+
cleanup_thread.start()
|
| 57 |
+
print("清理任务已启动")
|
| 58 |
+
|
| 59 |
+
def create_user_session_dir():
|
| 60 |
+
"""为用户会话创建临时目录"""
|
| 61 |
+
import uuid
|
| 62 |
+
session_id = str(uuid.uuid4())
|
| 63 |
+
temp_base = os.getenv("TMPDIR", "/tmp")
|
| 64 |
+
session_dir = os.path.join(temp_base, session_id)
|
| 65 |
+
os.makedirs(session_dir, exist_ok=True)
|
| 66 |
+
return session_dir, session_id
|
app/utils/monitor.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import time
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
from contextlib import contextmanager
|
| 5 |
+
from fastapi import Request, Response
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
DB_PATH = os.path.join(os.getenv("TMPDIR", "/tmp"), "monitor.db")
|
| 9 |
+
|
| 10 |
+
def init_db():
|
| 11 |
+
"""初始化监控数据库"""
|
| 12 |
+
conn = sqlite3.connect(DB_PATH)
|
| 13 |
+
cursor = conn.cursor()
|
| 14 |
+
cursor.execute("""
|
| 15 |
+
CREATE TABLE IF NOT EXISTS api_calls (
|
| 16 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 17 |
+
endpoint TEXT NOT NULL,
|
| 18 |
+
ip_address TEXT NOT NULL,
|
| 19 |
+
user_agent TEXT,
|
| 20 |
+
status_code INTEGER,
|
| 21 |
+
response_time REAL,
|
| 22 |
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 23 |
+
)
|
| 24 |
+
""")
|
| 25 |
+
conn.commit()
|
| 26 |
+
conn.close()
|
| 27 |
+
|
| 28 |
+
@contextmanager
|
| 29 |
+
def get_db():
|
| 30 |
+
"""数据库连接上下文管理器"""
|
| 31 |
+
conn = sqlite3.connect(DB_PATH)
|
| 32 |
+
try:
|
| 33 |
+
yield conn
|
| 34 |
+
finally:
|
| 35 |
+
conn.close()
|
| 36 |
+
|
| 37 |
+
def log_api_call(endpoint: str, ip: str, user_agent: str, status_code: int, response_time: float):
|
| 38 |
+
"""记录API调用"""
|
| 39 |
+
with get_db() as conn:
|
| 40 |
+
cursor = conn.cursor()
|
| 41 |
+
cursor.execute("""
|
| 42 |
+
INSERT INTO api_calls (endpoint, ip_address, user_agent, status_code, response_time)
|
| 43 |
+
VALUES (?, ?, ?, ?, ?)
|
| 44 |
+
""", (endpoint, ip, user_agent, status_code, response_time))
|
| 45 |
+
conn.commit()
|
| 46 |
+
|
| 47 |
+
def get_stats():
|
| 48 |
+
"""获取统计数据"""
|
| 49 |
+
with get_db() as conn:
|
| 50 |
+
cursor = conn.cursor()
|
| 51 |
+
|
| 52 |
+
# 总调用次数
|
| 53 |
+
cursor.execute("SELECT COUNT(*) FROM api_calls")
|
| 54 |
+
total_calls = cursor.fetchone()[0]
|
| 55 |
+
|
| 56 |
+
# 24小时内调用次数
|
| 57 |
+
yesterday = datetime.now() - timedelta(hours=24)
|
| 58 |
+
cursor.execute("SELECT COUNT(*) FROM api_calls WHERE timestamp > ?", (yesterday,))
|
| 59 |
+
calls_24h = cursor.fetchone()[0]
|
| 60 |
+
|
| 61 |
+
# 各端点统计
|
| 62 |
+
cursor.execute("""
|
| 63 |
+
SELECT endpoint, COUNT(*) as count, AVG(response_time) as avg_time
|
| 64 |
+
FROM api_calls
|
| 65 |
+
WHERE timestamp > ?
|
| 66 |
+
GROUP BY endpoint
|
| 67 |
+
ORDER BY count DESC
|
| 68 |
+
""", (yesterday,))
|
| 69 |
+
endpoint_stats = cursor.fetchall()
|
| 70 |
+
|
| 71 |
+
# 错误统计
|
| 72 |
+
cursor.execute("""
|
| 73 |
+
SELECT COUNT(*) FROM api_calls
|
| 74 |
+
WHERE status_code >= 400 AND timestamp > ?
|
| 75 |
+
""", (yesterday,))
|
| 76 |
+
error_count = cursor.fetchone()[0]
|
| 77 |
+
|
| 78 |
+
return {
|
| 79 |
+
"total_calls": total_calls,
|
| 80 |
+
"calls_24h": calls_24h,
|
| 81 |
+
"endpoint_stats": endpoint_stats,
|
| 82 |
+
"error_count": error_count,
|
| 83 |
+
"error_rate": round(error_count / max(calls_24h, 1) * 100, 2)
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
def setup_monitoring(app):
|
| 87 |
+
"""设置监控中间件"""
|
| 88 |
+
init_db()
|
| 89 |
+
|
| 90 |
+
@app.middleware("http")
|
| 91 |
+
async def monitor_middleware(request: Request, call_next):
|
| 92 |
+
start_time = time.time()
|
| 93 |
+
response = await call_next(request)
|
| 94 |
+
process_time = time.time() - start_time
|
| 95 |
+
|
| 96 |
+
# 记录API调用
|
| 97 |
+
log_api_call(
|
| 98 |
+
endpoint=request.url.path,
|
| 99 |
+
ip=request.client.host,
|
| 100 |
+
user_agent=request.headers.get("user-agent", ""),
|
| 101 |
+
status_code=response.status_code,
|
| 102 |
+
response_time=process_time
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
response.headers["X-Process-Time"] = str(process_time)
|
| 106 |
+
return response
|
app/utils/rate_limiter.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
from collections import defaultdict
|
| 3 |
+
from fastapi import HTTPException, Request
|
| 4 |
+
import threading
|
| 5 |
+
|
| 6 |
+
# 内存中的限流计数器
|
| 7 |
+
rate_limit_store = defaultdict(list)
|
| 8 |
+
lock = threading.Lock()
|
| 9 |
+
|
| 10 |
+
def check_rate_limit(request: Request, max_requests: int = 10, window_seconds: int = 60):
|
| 11 |
+
"""检查限流
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
request: FastAPI请求对象
|
| 15 |
+
max_requests: 时间窗口内最大请求数
|
| 16 |
+
window_seconds: 时间窗口(秒)
|
| 17 |
+
"""
|
| 18 |
+
client_ip = request.client.host
|
| 19 |
+
current_time = time.time()
|
| 20 |
+
|
| 21 |
+
with lock:
|
| 22 |
+
# 清理过期记录
|
| 23 |
+
rate_limit_store[client_ip] = [
|
| 24 |
+
timestamp for timestamp in rate_limit_store[client_ip]
|
| 25 |
+
if current_time - timestamp < window_seconds
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
# 检查是否超过限制
|
| 29 |
+
if len(rate_limit_store[client_ip]) >= max_requests:
|
| 30 |
+
raise HTTPException(
|
| 31 |
+
status_code=429,
|
| 32 |
+
detail=f"Rate limit exceeded. Max {max_requests} requests per {window_seconds} seconds."
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
# 记录当前请求
|
| 36 |
+
rate_limit_store[client_ip].append(current_time)
|
| 37 |
+
|
| 38 |
+
def cleanup_expired_records():
|
| 39 |
+
"""清理过期的限流记录"""
|
| 40 |
+
current_time = time.time()
|
| 41 |
+
with lock:
|
| 42 |
+
expired_keys = []
|
| 43 |
+
for ip, timestamps in rate_limit_store.items():
|
| 44 |
+
# 保留1小时内的记录
|
| 45 |
+
recent_timestamps = [t for t in timestamps if current_time - t < 3600]
|
| 46 |
+
if recent_timestamps:
|
| 47 |
+
rate_limit_store[ip] = recent_timestamps
|
| 48 |
+
else:
|
| 49 |
+
expired_keys.append(ip)
|
| 50 |
+
|
| 51 |
+
# 删除完全过期的IP记录
|
| 52 |
+
for ip in expired_keys:
|
| 53 |
+
del rate_limit_store[ip]
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.104.1
|
| 2 |
+
uvicorn==0.24.0
|
| 3 |
+
jinja2==3.1.2
|
| 4 |
+
python-multipart==0.0.6
|
| 5 |
+
requests==2.31.0
|
| 6 |
+
aiofiles==23.2.1
|
| 7 |
+
httpx==0.25.2
|
| 8 |
+
pillow==10.1.0
|