Ray commited on
Commit
b17403a
·
0 Parent(s):

Initial commit: MiniMax Tools for Hugging Face Space

Browse files
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