xwwww commited on
Commit
214f8ca
·
1 Parent(s): 23d7c24
Files changed (13) hide show
  1. .idea/.gitignore +5 -0
  2. .idea/PicExam.iml +12 -0
  3. .idea/modules.xml +8 -0
  4. .idea/vcs.xml +6 -0
  5. Dockerfile +28 -4
  6. README.md +250 -2
  7. app.py +534 -4
  8. example_usage.py +261 -0
  9. quick_test.py +178 -0
  10. requirements.txt +8 -0
  11. start_local.py +104 -0
  12. static/index.html +552 -0
  13. test_api.py +195 -0
.idea/.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # 默认忽略的文件
2
+ /shelf/
3
+ /workspace.xml
4
+ # 基于编辑器的 HTTP 客户端请求
5
+ /httpRequests/
.idea/PicExam.iml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="WEB_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$">
5
+ <excludeFolder url="file://$MODULE_DIR$/.tmp" />
6
+ <excludeFolder url="file://$MODULE_DIR$/temp" />
7
+ <excludeFolder url="file://$MODULE_DIR$/tmp" />
8
+ </content>
9
+ <orderEntry type="inheritedJdk" />
10
+ <orderEntry type="sourceFolder" forTests="false" />
11
+ </component>
12
+ </module>
.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/PicExam.iml" filepath="$PROJECT_DIR$/.idea/PicExam.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
Dockerfile CHANGED
@@ -1,16 +1,40 @@
1
  # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
- # you will also find guides on how best to write your Dockerfile
3
 
4
- FROM python:3.9
5
 
 
 
 
 
 
 
 
 
 
6
  RUN useradd -m -u 1000 user
7
  USER user
8
  ENV PATH="/home/user/.local/bin:$PATH"
9
 
 
10
  WORKDIR /app
11
 
 
 
 
 
 
 
 
12
  COPY --chown=user ./requirements.txt requirements.txt
13
- RUN pip install --no-cache-dir --upgrade -r requirements.txt
 
14
 
 
15
  COPY --chown=user . /app
16
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
 
 
 
 
1
  # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ # Dockerfile for Qwen-VL PicExam API with CPU inference optimization
3
 
4
+ FROM python:3.10-slim
5
 
6
+ # 安装系统依赖
7
+ RUN apt-get update && apt-get install -y \
8
+ git \
9
+ wget \
10
+ curl \
11
+ build-essential \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ # 创建用户
15
  RUN useradd -m -u 1000 user
16
  USER user
17
  ENV PATH="/home/user/.local/bin:$PATH"
18
 
19
+ # 设置工作目录
20
  WORKDIR /app
21
 
22
+ # 设置环境变量优化内存使用
23
+ ENV PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:512
24
+ ENV TOKENIZERS_PARALLELISM=false
25
+ ENV OMP_NUM_THREADS=4
26
+ ENV MKL_NUM_THREADS=4
27
+
28
+ # 复制并安装 Python 依赖
29
  COPY --chown=user ./requirements.txt requirements.txt
30
+ RUN pip install --no-cache-dir --upgrade pip && \
31
+ pip install --no-cache-dir --upgrade -r requirements.txt
32
 
33
+ # 复制应用代码
34
  COPY --chown=user . /app
35
+
36
+ # 暴露端口
37
+ EXPOSE 7860
38
+
39
+ # 启动命令,增加内存和超时配置
40
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--timeout-keep-alive", "300", "--workers", "1"]
README.md CHANGED
@@ -6,7 +6,255 @@ colorTo: red
6
  sdk: docker
7
  pinned: false
8
  license: apache-2.0
9
- short_description: PicExam
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  sdk: docker
7
  pinned: false
8
  license: apache-2.0
9
+ short_description: 基于 Qwen-VL 的图像理解 API,支持 16GB 内存 + CPU 推理
10
  ---
11
 
12
+ # 🏆 PicExam - Qwen-VL 图像理解 API
13
+
14
+ 基于 Qwen2-VL-2B-Instruct 模型的图像理解 API,专门优化用于 16GB 内存 + 纯 CPU 推理环境。
15
+
16
+ ## ✨ 特性
17
+
18
+ - 🧠 **智能图像理解**: 基于阿里巴巴 Qwen2-VL-2B-Instruct 模型
19
+ - 💻 **CPU 优化**: 专门针对 CPU 推理进行优化,无需 GPU
20
+ - 🔧 **内存友好**: 适配 16GB 内存环境,包含内存监控和优化
21
+ - 🚀 **易于部署**: 支持本地运行和 Hugging Face Spaces 部署
22
+ - 📝 **多种接口**: 支持文件上传和 base64 图片输入
23
+ - 📊 **实时监控**: 内置内存使用监控和缓存管理
24
+
25
+ ## 🛠️ 系统要求
26
+
27
+ - **内存**: 16GB RAM(推荐)
28
+ - **处理器**: 多核 CPU(推荐 4 核以上)
29
+ - **存储**: 至少 10GB 可用空间(用于模型下载)
30
+ - **Python**: 3.10+
31
+
32
+ ## 🚀 快速开始
33
+
34
+ ### 本地运行
35
+
36
+ 1. **克隆项目**
37
+ ```bash
38
+ git clone <repository-url>
39
+ cd PicExam
40
+ ```
41
+
42
+ 2. **安装依赖**
43
+ ```bash
44
+ pip install -r requirements.txt
45
+ ```
46
+
47
+ 3. **启动服务**
48
+ ```bash
49
+ python start_local.py
50
+ ```
51
+
52
+ 或者直接使用 uvicorn:
53
+ ```bash
54
+ uvicorn app:app --host 0.0.0.0 --port 7860
55
+ ```
56
+
57
+ 4. **访问 API**
58
+ - API 服务: http://localhost:7860
59
+ - 交互式文档: http://localhost:7860/docs
60
+
61
+ ### Docker 部署
62
+
63
+ ```bash
64
+ docker build -t picexam .
65
+ docker run -p 7860:7860 picexam
66
+ ```
67
+
68
+ ### Hugging Face Spaces 部署
69
+
70
+ 1. 将代码推送到 Hugging Face Spaces 仓库
71
+ 2. 确保 `README.md` 中的 YAML 配置正确
72
+ 3. Spaces 会自动构建和部署
73
+
74
+ ## 📖 API 使用说明
75
+
76
+ ### 🌐 浏览器访问
77
+
78
+ - **主页面**: http://localhost:7860/ - 显示完整的 API 端点定义和使用方法
79
+ - **Web 界面**: http://localhost:7860/web - 图形化操作界面,支持拖拽上传
80
+ - **API 文档**: http://localhost:7860/docs - 交互式 API 文档 (Swagger UI)
81
+
82
+ ### 📡 API 端点
83
+
84
+ #### 1. API 信息获取
85
+ ```bash
86
+ curl http://localhost:7860/
87
+ ```
88
+ 返回完整的 API 端点定义、使用方法和示例
89
+
90
+ #### 2. 健康检查
91
+ ```bash
92
+ curl http://localhost:7860/health
93
+ ```
94
+
95
+ #### 3. 图片分析(文件上传)
96
+ ```bash
97
+ curl -X POST "http://localhost:7860/analyze_image" \
98
+ -F "image=@your_image.jpg" \
99
+ -F "question=请描述这张图片的内容"
100
+ ```
101
+
102
+ #### 4. 图片分析(Base64 表单)
103
+ ```bash
104
+ curl -X POST "http://localhost:7860/analyze_image_base64" \
105
+ -F "image_base64=data:image/jpeg;base64,/9j/4AAQ..." \
106
+ -F "question=这张图片中有什么?"
107
+ ```
108
+
109
+ #### 5. 图片分析(JSON API)⭐ 推荐
110
+ ```bash
111
+ curl -X POST "http://localhost:7860/analyze" \
112
+ -H "Content-Type: application/json" \
113
+ -d '{
114
+ "image": "data:image/jpeg;base64,/9j/4AAQ...",
115
+ "prompt": "请详细描述这张图片的内容"
116
+ }'
117
+ ```
118
+
119
+ **JSON API 响应格式:**
120
+ ```json
121
+ {
122
+ "success": true,
123
+ "prompt": "请详细描述这张图片的内容",
124
+ "response": "这张图片显示了...",
125
+ "processing_time": 8.45,
126
+ "image_info": {
127
+ "size": "1024x768",
128
+ "mode": "RGB",
129
+ "format": "JPEG"
130
+ }
131
+ }
132
+ ```
133
+
134
+ #### 6. 内存状态监控
135
+ ```bash
136
+ curl http://localhost:7860/memory_status
137
+ ```
138
+
139
+ #### 7. 清理缓存
140
+ ```bash
141
+ curl -X POST http://localhost:7860/clear_cache
142
+ ```
143
+
144
+ ### 🐍 Python 调用示例
145
+
146
+ ```python
147
+ import requests
148
+ import base64
149
+
150
+ # 1. JSON API 调用(推荐)
151
+ def analyze_image_json(image_base64, prompt):
152
+ response = requests.post('http://localhost:7860/analyze',
153
+ json={
154
+ "image": image_base64,
155
+ "prompt": prompt
156
+ })
157
+ return response.json()
158
+
159
+ # 2. 文件上传调用
160
+ def analyze_image_file(image_path, question):
161
+ with open(image_path, 'rb') as f:
162
+ response = requests.post('http://localhost:7860/analyze_image',
163
+ files={"image": f},
164
+ data={"question": question})
165
+ return response.json()
166
+
167
+ # 使用示例
168
+ result = analyze_image_json("data:image/jpeg;base64,/9j/4AAQ...", "描述这张图片")
169
+ print(result['response'])
170
+ ```
171
+
172
+ ## 🧪 测试
173
+
174
+ ### 快速测试
175
+ ```bash
176
+ python quick_test.py
177
+ ```
178
+
179
+ ### 完整功能测试
180
+ ```bash
181
+ python test_api.py
182
+ ```
183
+
184
+ ### 使用示例演示
185
+ ```bash
186
+ python example_usage.py
187
+ ```
188
+
189
+ 测试包括:
190
+ - ✅ API 信息获取
191
+ - ✅ 健康检查
192
+ - ✅ 内存状态监控
193
+ - ✅ 图片分析(文件上传)
194
+ - ✅ 图片分析(Base64 表单)
195
+ - ✅ 图片分析(JSON API)
196
+ - ✅ 缓存清理
197
+
198
+ ## ⚙️ 配置优化
199
+
200
+ ### 内存优化设置
201
+
202
+ 项目已包含以下内存优化配置:
203
+
204
+ - `torch_dtype=torch.float16`: 使用半精度浮点数
205
+ - `low_cpu_mem_usage=True`: 启用低内存使用模式
206
+ - `use_cache=False`: 禁用 KV 缓存
207
+ - 环境变量优化: `PYTORCH_CUDA_ALLOC_CONF`, `TOKENIZERS_PARALLELISM`
208
+
209
+ ### 性能调优
210
+
211
+ - `OMP_NUM_THREADS=4`: 限制 OpenMP 线程数
212
+ - `MKL_NUM_THREADS=4`: 限制 MKL 线程数
213
+ - 单 worker 模式避免内存重复
214
+
215
+ ## 📊 性能指标
216
+
217
+ 在 16GB 内存环境下的典型性能:
218
+
219
+ - **模型加载时间**: 30-60 秒(首次)
220
+ - **推理时间**: 5-15 秒/图片(取决于 CPU)
221
+ - **内存使用**: 8-12GB(包含模型和系统)
222
+ - **支持图片格式**: JPEG, PNG, WebP 等
223
+
224
+ ## 🔧 故障排除
225
+
226
+ ### 常见问题
227
+
228
+ 1. **内存不足**
229
+ - 关闭其他程序释放内存
230
+ - 使用 `/clear_cache` 接口清理缓存
231
+
232
+ 2. **模型下载慢**
233
+ - 配置 Hugging Face 镜像源
234
+ - 使用代理或 VPN
235
+
236
+ 3. **推理速度慢**
237
+ - 确保 CPU 有足够核心数
238
+ - 检查系统负载
239
+
240
+ ### 日志查看
241
+
242
+ 应用使用标准 Python logging,可通过以下方式查看详细日志:
243
+
244
+ ```bash
245
+ export PYTHONPATH=.
246
+ python -c "import logging; logging.basicConfig(level=logging.DEBUG)"
247
+ uvicorn app:app --log-level debug
248
+ ```
249
+
250
+ ## 📄 许可证
251
+
252
+ Apache License 2.0
253
+
254
+ ## 🤝 贡献
255
+
256
+ 欢迎提交 Issue 和 Pull Request!
257
+
258
+ ## 📞 支持
259
+
260
+ 如有问题,请在 GitHub Issues 中提出。
app.py CHANGED
@@ -1,7 +1,537 @@
1
- from fastapi import FastAPI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- app = FastAPI()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  @app.get("/")
6
- def greet_json():
7
- return {"Hello": "World!"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import psutil
4
+ import gc
5
+ import time
6
+ import json
7
+ from fastapi import FastAPI, File, UploadFile, Form, Request
8
+ from fastapi.responses import JSONResponse, FileResponse
9
+ from fastapi.staticfiles import StaticFiles
10
+ from pydantic import BaseModel
11
+ from transformers import Qwen2VLForConditionalGeneration, AutoTokenizer, AutoProcessor
12
+ from qwen_vl_utils import process_vision_info
13
+ from PIL import Image
14
+ import io
15
+ import base64
16
+ import logging
17
 
18
+ # 配置日志
19
+ logging.basicConfig(level=logging.INFO)
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # 数据模型
23
+ class AnalyzeRequest(BaseModel):
24
+ image: str # base64 编码的图片
25
+ prompt: str = "请描述这张图片的内容" # 提示词/问题
26
+
27
+ class AnalyzeResponse(BaseModel):
28
+ success: bool
29
+ prompt: str
30
+ response: str
31
+ processing_time: float
32
+ image_info: dict = None
33
+ error: str = None
34
+
35
+ app = FastAPI(title="Qwen-VL PicExam API", description="基于 Qwen2-VL-2B-Instruct 的图像理解 API")
36
+
37
+ # 挂载静态文件
38
+ app.mount("/static", StaticFiles(directory="static"), name="static")
39
+
40
+ # 全局变量存储模型和处理器
41
+ model = None
42
+ processor = None
43
+ tokenizer = None
44
+
45
+ def load_model():
46
+ """加载 Qwen2-VL-2B-Instruct 模型(CPU 版本,适合 16GB 内存)"""
47
+ global model, processor, tokenizer
48
+
49
+ try:
50
+ logger.info("开始加载 Qwen2-VL-2B-Instruct 模型...")
51
+
52
+ model_name = "Qwen/Qwen2-VL-2B-Instruct"
53
+
54
+ # 设置环境变量优化内存使用
55
+ os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "max_split_size_mb:512"
56
+ os.environ["TOKENIZERS_PARALLELISM"] = "false" # 避免分词器并行导致的内存问题
57
+
58
+ # 加载处理器和分词器
59
+ processor = AutoProcessor.from_pretrained(
60
+ model_name,
61
+ trust_remote_code=True
62
+ )
63
+ tokenizer = AutoTokenizer.from_pretrained(
64
+ model_name,
65
+ trust_remote_code=True
66
+ )
67
+
68
+ # 加载模型到 CPU,使用内存优化配置
69
+ model = Qwen2VLForConditionalGeneration.from_pretrained(
70
+ model_name,
71
+ torch_dtype=torch.float16, # 使用 float16 减少内存使用
72
+ device_map="cpu", # 强制使用 CPU
73
+ low_cpu_mem_usage=True, # 低内存使用模式
74
+ trust_remote_code=True,
75
+ # 额外的内存优化选项
76
+ use_cache=False, # 禁用 KV 缓存以节省内存
77
+ attn_implementation="eager", # 使用 eager attention 实现
78
+ )
79
+
80
+ # 设置为评估模式
81
+ model.eval()
82
+
83
+ # 清理不必要的内存
84
+ if torch.cuda.is_available():
85
+ torch.cuda.empty_cache()
86
+
87
+ logger.info("模型加载成功!")
88
+ logger.info(f"模型参数数量: {sum(p.numel() for p in model.parameters()) / 1e6:.1f}M")
89
+
90
+ return True
91
+
92
+ except Exception as e:
93
+ logger.error(f"模型加载失败: {str(e)}")
94
+ return False
95
+
96
+ # 启动时加载模型
97
+ @app.on_event("startup")
98
+ async def startup_event():
99
+ """应用启动时加载模型"""
100
+ success = load_model()
101
+ if not success:
102
+ logger.error("模型加载失败,应用可能无法正常工作")
103
 
104
  @app.get("/")
105
+ def api_documentation():
106
+ """API 文档和端点说明"""
107
+ return {
108
+ "service": "Qwen-VL PicExam API",
109
+ "description": "基于 Qwen2-VL-2B-Instruct 的图像理解 API,支持 16GB 内存 + CPU 推理",
110
+ "version": "1.0.0",
111
+ "model": "Qwen2-VL-2B-Instruct",
112
+ "status": {
113
+ "service": "running",
114
+ "model_loaded": model is not None,
115
+ "inference_mode": "CPU"
116
+ },
117
+ "endpoints": {
118
+ "GET /": {
119
+ "description": "获取 API 文档和端点信息",
120
+ "response": "JSON 格式的 API 说明"
121
+ },
122
+ "GET /health": {
123
+ "description": "健康检查接口",
124
+ "response": "服务状态信息"
125
+ },
126
+ "GET /web": {
127
+ "description": "Web 界面",
128
+ "response": "HTML 页面,提供图形化操作界面"
129
+ },
130
+ "POST /analyze_image": {
131
+ "description": "分析上传的图片文件",
132
+ "parameters": {
133
+ "image": "图片文件 (multipart/form-data)",
134
+ "question": "关于图片的问题 (可选,默认为描述图片内容)"
135
+ },
136
+ "example": "curl -X POST '/analyze_image' -F 'image=@photo.jpg' -F 'question=这张图片中有什么?'"
137
+ },
138
+ "POST /analyze_image_base64": {
139
+ "description": "分析 base64 编码的图片",
140
+ "parameters": {
141
+ "image_base64": "base64 编码的图片数据",
142
+ "question": "关于图片的问题 (可���)"
143
+ },
144
+ "example": "curl -X POST '/analyze_image_base64' -F 'image_base64=data:image/jpeg;base64,/9j/4AAQ...' -F 'question=描述这张图片'"
145
+ },
146
+ "POST /analyze": {
147
+ "description": "简化的图片分析接口 (JSON 格式)",
148
+ "parameters": {
149
+ "image": "base64 编码的图片数据",
150
+ "prompt": "提示词/问题"
151
+ },
152
+ "example": "curl -X POST '/analyze' -H 'Content-Type: application/json' -d '{\"image\":\"data:image/jpeg;base64,...\",\"prompt\":\"描述图片\"}'"
153
+ },
154
+ "GET /memory_status": {
155
+ "description": "获取内存使用状态",
156
+ "response": "系统内存和模型内存使用情况"
157
+ },
158
+ "POST /clear_cache": {
159
+ "description": "清理内存缓存",
160
+ "response": "缓存清理结果"
161
+ }
162
+ },
163
+ "usage_examples": {
164
+ "curl_file_upload": "curl -X POST 'http://localhost:7860/analyze_image' -F 'image=@your_image.jpg' -F 'question=请描述这张图片'",
165
+ "curl_base64": "curl -X POST 'http://localhost:7860/analyze_image_base64' -F 'image_base64=data:image/jpeg;base64,/9j/4AAQ...' -F 'question=这张图片中有什么?'",
166
+ "curl_json": "curl -X POST 'http://localhost:7860/analyze' -H 'Content-Type: application/json' -d '{\"image\":\"data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAA...\",\"prompt\":\"请详细描述这张图片的内容\"}'"
167
+ },
168
+ "supported_formats": ["JPEG", "PNG", "WebP", "BMP", "GIF"],
169
+ "memory_requirements": "16GB RAM recommended for optimal performance",
170
+ "inference_time": "5-15 seconds per image (depends on CPU)",
171
+ "documentation": "Visit /docs for interactive API documentation"
172
+ }
173
+
174
+ @app.get("/health")
175
+ def health_check():
176
+ """简单的健康检查接口"""
177
+ return {
178
+ "status": "healthy",
179
+ "service": "Qwen-VL PicExam API",
180
+ "model_loaded": model is not None,
181
+ "timestamp": time.time()
182
+ }
183
+
184
+ @app.post("/analyze_image")
185
+ async def analyze_image(
186
+ image: UploadFile = File(...),
187
+ question: str = Form("请描述这张图片的内容")
188
+ ):
189
+ """
190
+ 分析上传的图片并回答问题
191
+
192
+ Args:
193
+ image: 上传的图片文件
194
+ question: 关于图片的问题(默认为描述图片内容)
195
+
196
+ Returns:
197
+ JSON 响应包含分析结果
198
+ """
199
+ if model is None or processor is None:
200
+ return JSONResponse(
201
+ status_code=503,
202
+ content={"error": "模型未加载,请稍后重试"}
203
+ )
204
+
205
+ try:
206
+ # 读取图片
207
+ image_bytes = await image.read()
208
+ pil_image = Image.open(io.BytesIO(image_bytes))
209
+
210
+ # 确保图片是 RGB 格式
211
+ if pil_image.mode != 'RGB':
212
+ pil_image = pil_image.convert('RGB')
213
+
214
+ # 准备消息格式
215
+ messages = [
216
+ {
217
+ "role": "user",
218
+ "content": [
219
+ {
220
+ "type": "image",
221
+ "image": pil_image,
222
+ },
223
+ {"type": "text", "text": question},
224
+ ],
225
+ }
226
+ ]
227
+
228
+ # 处理输入
229
+ text = processor.apply_chat_template(
230
+ messages, tokenize=False, add_generation_prompt=True
231
+ )
232
+ image_inputs, video_inputs = process_vision_info(messages)
233
+ inputs = processor(
234
+ text=[text],
235
+ images=image_inputs,
236
+ videos=video_inputs,
237
+ padding=True,
238
+ return_tensors="pt",
239
+ )
240
+
241
+ # 生成回答
242
+ with torch.no_grad():
243
+ generated_ids = model.generate(
244
+ **inputs,
245
+ max_new_tokens=512,
246
+ do_sample=False,
247
+ temperature=0.7,
248
+ top_p=0.9,
249
+ pad_token_id=processor.tokenizer.eos_token_id
250
+ )
251
+
252
+ generated_ids_trimmed = [
253
+ out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
254
+ ]
255
+
256
+ output_text = processor.batch_decode(
257
+ generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
258
+ )[0]
259
+
260
+ return {
261
+ "success": True,
262
+ "question": question,
263
+ "answer": output_text,
264
+ "image_info": {
265
+ "filename": image.filename,
266
+ "size": f"{pil_image.size[0]}x{pil_image.size[1]}",
267
+ "mode": pil_image.mode
268
+ }
269
+ }
270
+
271
+ except Exception as e:
272
+ logger.error(f"图片分析失败: {str(e)}")
273
+ return JSONResponse(
274
+ status_code=500,
275
+ content={"error": f"图片分析失败: {str(e)}"}
276
+ )
277
+
278
+ @app.post("/analyze_image_base64")
279
+ async def analyze_image_base64(
280
+ image_base64: str = Form(...),
281
+ question: str = Form("请��述这张图片的内容")
282
+ ):
283
+ """
284
+ 分析 base64 编码的图片并回答问题
285
+
286
+ Args:
287
+ image_base64: base64 编码的图片数据
288
+ question: 关于图片的问题
289
+
290
+ Returns:
291
+ JSON 响应包含分析结果
292
+ """
293
+ if model is None or processor is None:
294
+ return JSONResponse(
295
+ status_code=503,
296
+ content={"error": "模型未加载,请稍后重试"}
297
+ )
298
+
299
+ try:
300
+ # 解码 base64 图片
301
+ if image_base64.startswith('data:image'):
302
+ # 移除 data:image/xxx;base64, 前缀
303
+ image_base64 = image_base64.split(',')[1]
304
+
305
+ image_bytes = base64.b64decode(image_base64)
306
+ pil_image = Image.open(io.BytesIO(image_bytes))
307
+
308
+ # 确保图片是 RGB 格式
309
+ if pil_image.mode != 'RGB':
310
+ pil_image = pil_image.convert('RGB')
311
+
312
+ # 准备消息格式
313
+ messages = [
314
+ {
315
+ "role": "user",
316
+ "content": [
317
+ {
318
+ "type": "image",
319
+ "image": pil_image,
320
+ },
321
+ {"type": "text", "text": question},
322
+ ],
323
+ }
324
+ ]
325
+
326
+ # 处理输入
327
+ text = processor.apply_chat_template(
328
+ messages, tokenize=False, add_generation_prompt=True
329
+ )
330
+ image_inputs, video_inputs = process_vision_info(messages)
331
+ inputs = processor(
332
+ text=[text],
333
+ images=image_inputs,
334
+ videos=video_inputs,
335
+ padding=True,
336
+ return_tensors="pt",
337
+ )
338
+
339
+ # 生成回答
340
+ with torch.no_grad():
341
+ generated_ids = model.generate(
342
+ **inputs,
343
+ max_new_tokens=512,
344
+ do_sample=False,
345
+ temperature=0.7,
346
+ top_p=0.9,
347
+ pad_token_id=processor.tokenizer.eos_token_id
348
+ )
349
+
350
+ generated_ids_trimmed = [
351
+ out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
352
+ ]
353
+
354
+ output_text = processor.batch_decode(
355
+ generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
356
+ )[0]
357
+
358
+ return {
359
+ "success": True,
360
+ "question": question,
361
+ "answer": output_text,
362
+ "image_info": {
363
+ "size": f"{pil_image.size[0]}x{pil_image.size[1]}",
364
+ "mode": pil_image.mode
365
+ }
366
+ }
367
+
368
+ except Exception as e:
369
+ logger.error(f"图片分析失败: {str(e)}")
370
+ return JSONResponse(
371
+ status_code=500,
372
+ content={"error": f"图片分析失败: {str(e)}"}
373
+ )
374
+
375
+ @app.post("/analyze", response_model=AnalyzeResponse)
376
+ async def analyze_simple(request: AnalyzeRequest):
377
+ """
378
+ 简化的图片分析接口 (JSON 格式)
379
+
380
+ 接收 JSON 格式的请求,包含 base64 图片和提示词
381
+ 返回标准化的分析结果
382
+ """
383
+ if model is None or processor is None:
384
+ return AnalyzeResponse(
385
+ success=False,
386
+ prompt=request.prompt,
387
+ response="",
388
+ processing_time=0,
389
+ error="模型未加载,请稍后重试"
390
+ )
391
+
392
+ start_time = time.time()
393
+
394
+ try:
395
+ # 处理 base64 图片
396
+ image_data = request.image
397
+ if image_data.startswith('data:image'):
398
+ # 移除 data:image/xxx;base64, 前缀
399
+ image_data = image_data.split(',')[1]
400
+
401
+ image_bytes = base64.b64decode(image_data)
402
+ pil_image = Image.open(io.BytesIO(image_bytes))
403
+
404
+ # 确保图片是 RGB 格式
405
+ if pil_image.mode != 'RGB':
406
+ pil_image = pil_image.convert('RGB')
407
+
408
+ # 准备消息格式
409
+ messages = [
410
+ {
411
+ "role": "user",
412
+ "content": [
413
+ {
414
+ "type": "image",
415
+ "image": pil_image,
416
+ },
417
+ {"type": "text", "text": request.prompt},
418
+ ],
419
+ }
420
+ ]
421
+
422
+ # 处理输入
423
+ text = processor.apply_chat_template(
424
+ messages, tokenize=False, add_generation_prompt=True
425
+ )
426
+ image_inputs, video_inputs = process_vision_info(messages)
427
+ inputs = processor(
428
+ text=[text],
429
+ images=image_inputs,
430
+ videos=video_inputs,
431
+ padding=True,
432
+ return_tensors="pt",
433
+ )
434
+
435
+ # 生成回答
436
+ with torch.no_grad():
437
+ generated_ids = model.generate(
438
+ **inputs,
439
+ max_new_tokens=512,
440
+ do_sample=False,
441
+ temperature=0.7,
442
+ top_p=0.9,
443
+ pad_token_id=processor.tokenizer.eos_token_id
444
+ )
445
+
446
+ generated_ids_trimmed = [
447
+ out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
448
+ ]
449
+
450
+ output_text = processor.batch_decode(
451
+ generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
452
+ )[0]
453
+
454
+ processing_time = time.time() - start_time
455
+
456
+ return AnalyzeResponse(
457
+ success=True,
458
+ prompt=request.prompt,
459
+ response=output_text,
460
+ processing_time=processing_time,
461
+ image_info={
462
+ "size": f"{pil_image.size[0]}x{pil_image.size[1]}",
463
+ "mode": pil_image.mode,
464
+ "format": pil_image.format or "Unknown"
465
+ }
466
+ )
467
+
468
+ except Exception as e:
469
+ processing_time = time.time() - start_time
470
+ logger.error(f"图片分析失败: {str(e)}")
471
+
472
+ return AnalyzeResponse(
473
+ success=False,
474
+ prompt=request.prompt,
475
+ response="",
476
+ processing_time=processing_time,
477
+ error=f"图片分析失败: {str(e)}"
478
+ )
479
+
480
+ @app.get("/memory_status")
481
+ def get_memory_status():
482
+ """获取当前内存使用状态"""
483
+ try:
484
+ # 系统内存信息
485
+ memory = psutil.virtual_memory()
486
+
487
+ # PyTorch 内存信息(如果使用 CUDA)
488
+ torch_memory = {}
489
+ if torch.cuda.is_available():
490
+ torch_memory = {
491
+ "cuda_allocated": torch.cuda.memory_allocated() / 1024**3, # GB
492
+ "cuda_reserved": torch.cuda.memory_reserved() / 1024**3, # GB
493
+ "cuda_max_allocated": torch.cuda.max_memory_allocated() / 1024**3, # GB
494
+ }
495
+
496
+ return {
497
+ "system_memory": {
498
+ "total_gb": memory.total / 1024**3,
499
+ "available_gb": memory.available / 1024**3,
500
+ "used_gb": memory.used / 1024**3,
501
+ "percent": memory.percent
502
+ },
503
+ "torch_memory": torch_memory,
504
+ "model_loaded": model is not None,
505
+ "recommendations": {
506
+ "memory_usage_ok": memory.percent < 85,
507
+ "available_for_inference": memory.available / 1024**3 > 2.0
508
+ }
509
+ }
510
+ except Exception as e:
511
+ return JSONResponse(
512
+ status_code=500,
513
+ content={"error": f"获取内存状态失败: {str(e)}"}
514
+ )
515
+
516
+ @app.post("/clear_cache")
517
+ def clear_cache():
518
+ """清理内存缓存"""
519
+ try:
520
+ # Python 垃圾回收
521
+ gc.collect()
522
+
523
+ # PyTorch 缓存清理
524
+ if torch.cuda.is_available():
525
+ torch.cuda.empty_cache()
526
+
527
+ return {"success": True, "message": "缓存清理完成"}
528
+ except Exception as e:
529
+ return JSONResponse(
530
+ status_code=500,
531
+ content={"error": f"缓存清理失败: {str(e)}"}
532
+ )
533
+
534
+ @app.get("/web")
535
+ def web_interface():
536
+ """返回 Web 界面"""
537
+ return FileResponse("static/index.html")
example_usage.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ PicExam API 使用示例
4
+ 演示如何通过不同方式调用 Qwen-VL 图像分析 API
5
+ """
6
+
7
+ import requests
8
+ import base64
9
+ import json
10
+ from PIL import Image
11
+ import io
12
+
13
+ # API 基础 URL
14
+ BASE_URL = "http://localhost:7860"
15
+
16
+ def create_sample_image():
17
+ """创建一个示例图片用于测试"""
18
+ # 创建一个简单的测试图片
19
+ img = Image.new('RGB', (300, 200), color='lightblue')
20
+
21
+ # 添加一些图形
22
+ from PIL import ImageDraw, ImageFont
23
+ draw = ImageDraw.Draw(img)
24
+
25
+ # 绘制矩形
26
+ draw.rectangle([50, 50, 150, 100], fill='red', outline='black', width=2)
27
+
28
+ # 绘制圆形
29
+ draw.ellipse([180, 60, 250, 130], fill='yellow', outline='black', width=2)
30
+
31
+ # 添加文字
32
+ try:
33
+ # 尝试使用默认字体
34
+ draw.text((100, 150), "Sample Image", fill='black')
35
+ except:
36
+ # 如果没有字体,跳过文字
37
+ pass
38
+
39
+ return img
40
+
41
+ def image_to_base64(image):
42
+ """将 PIL 图片转换为 base64 字符串"""
43
+ buffer = io.BytesIO()
44
+ image.save(buffer, format='PNG')
45
+ img_str = base64.b64encode(buffer.getvalue()).decode()
46
+ return f"data:image/png;base64,{img_str}"
47
+
48
+ def test_api_info():
49
+ """测试 API 信息获取"""
50
+ print("🔍 获取 API 信息...")
51
+ try:
52
+ response = requests.get(f"{BASE_URL}/")
53
+ if response.status_code == 200:
54
+ data = response.json()
55
+ print(f"✅ 服务: {data['service']}")
56
+ print(f"✅ 版本: {data['version']}")
57
+ print(f"✅ 模型: {data['model']}")
58
+ print(f"✅ 模型状态: {'已加载' if data['status']['model_loaded'] else '未加载'}")
59
+ print(f"✅ 可用端点数量: {len(data['endpoints'])}")
60
+ return True
61
+ else:
62
+ print(f"❌ 获取 API 信息失败: {response.status_code}")
63
+ return False
64
+ except Exception as e:
65
+ print(f"❌ 连接失败: {e}")
66
+ return False
67
+
68
+ def test_health_check():
69
+ """测试健康检查"""
70
+ print("\n🔍 健康检查...")
71
+ try:
72
+ response = requests.get(f"{BASE_URL}/health")
73
+ if response.status_code == 200:
74
+ data = response.json()
75
+ print(f"✅ 状态: {data['status']}")
76
+ print(f"✅ 模型: {'已加载' if data['model_loaded'] else '未加载'}")
77
+ return True
78
+ else:
79
+ print(f"❌ 健康检查失败: {response.status_code}")
80
+ return False
81
+ except Exception as e:
82
+ print(f"❌ 健康检查异常: {e}")
83
+ return False
84
+
85
+ def test_file_upload():
86
+ """测试文件上传方式"""
87
+ print("\n🔍 测试文件上传分析...")
88
+
89
+ # 创建测试图片
90
+ test_img = create_sample_image()
91
+ test_img.save("temp_test.png")
92
+
93
+ try:
94
+ with open("temp_test.png", "rb") as f:
95
+ files = {"image": ("test.png", f, "image/png")}
96
+ data = {"question": "请描述这张图片中的颜色和形状"}
97
+
98
+ response = requests.post(f"{BASE_URL}/analyze_image", files=files, data=data)
99
+
100
+ if response.status_code == 200:
101
+ result = response.json()
102
+ print(f"✅ 分析成功!")
103
+ print(f" 问题: {result['question']}")
104
+ print(f" 回答: {result['answer']}")
105
+ print(f" 图片信息: {result['image_info']}")
106
+ return True
107
+ else:
108
+ print(f"❌ 文件上传分析失败: {response.status_code}")
109
+ print(f" 错误: {response.text}")
110
+ return False
111
+
112
+ except Exception as e:
113
+ print(f"❌ 文件上传测试异常: {e}")
114
+ return False
115
+ finally:
116
+ # 清理临时文件
117
+ try:
118
+ import os
119
+ os.remove("temp_test.png")
120
+ except:
121
+ pass
122
+
123
+ def test_base64_form():
124
+ """测试 base64 表单方式"""
125
+ print("\n🔍 测试 base64 表单分析...")
126
+
127
+ try:
128
+ # 创建测试图片并转换为 base64
129
+ test_img = create_sample_image()
130
+ img_base64 = image_to_base64(test_img)
131
+
132
+ data = {
133
+ "image_base64": img_base64,
134
+ "question": "这张图片中有什么几何形状?"
135
+ }
136
+
137
+ response = requests.post(f"{BASE_URL}/analyze_image_base64", data=data)
138
+
139
+ if response.status_code == 200:
140
+ result = response.json()
141
+ print(f"✅ 分析成功!")
142
+ print(f" 问题: {result['question']}")
143
+ print(f" 回答: {result['answer']}")
144
+ print(f" 图片信息: {result['image_info']}")
145
+ return True
146
+ else:
147
+ print(f"❌ base64 表单分析失败: {response.status_code}")
148
+ print(f" 错误: {response.text}")
149
+ return False
150
+
151
+ except Exception as e:
152
+ print(f"❌ base64 表单测试异常: {e}")
153
+ return False
154
+
155
+ def test_json_api():
156
+ """测试 JSON API 方式"""
157
+ print("\n🔍 测试 JSON API 分析...")
158
+
159
+ try:
160
+ # 创建测试图片并转换为 base64
161
+ test_img = create_sample_image()
162
+ img_base64 = image_to_base64(test_img)
163
+
164
+ request_data = {
165
+ "image": img_base64,
166
+ "prompt": "请详细分析这张图片的构成元素,包括颜色、形状和布局"
167
+ }
168
+
169
+ response = requests.post(
170
+ f"{BASE_URL}/analyze",
171
+ headers={"Content-Type": "application/json"},
172
+ json=request_data
173
+ )
174
+
175
+ if response.status_code == 200:
176
+ result = response.json()
177
+ print(f"✅ 分析成功!")
178
+ print(f" 提示词: {result['prompt']}")
179
+ print(f" 响应: {result['response']}")
180
+ print(f" 处理时间: {result['processing_time']:.2f}秒")
181
+ print(f" 图片信息: {result['image_info']}")
182
+ return True
183
+ else:
184
+ print(f"❌ JSON API 分析失败: {response.status_code}")
185
+ print(f" 错误: {response.text}")
186
+ return False
187
+
188
+ except Exception as e:
189
+ print(f"❌ JSON API 测试异常: {e}")
190
+ return False
191
+
192
+ def test_memory_status():
193
+ """测试内存状态"""
194
+ print("\n🔍 检查内存状态...")
195
+ try:
196
+ response = requests.get(f"{BASE_URL}/memory_status")
197
+ if response.status_code == 200:
198
+ data = response.json()
199
+ memory = data['system_memory']
200
+ print(f"✅ 系统内存: {memory['used_gb']:.2f}GB / {memory['total_gb']:.2f}GB ({memory['percent']:.1f}%)")
201
+ print(f"✅ 可用内存: {memory['available_gb']:.2f}GB")
202
+ print(f"✅ 内存状态: {'正常' if data['recommendations']['memory_usage_ok'] else '紧张'}")
203
+ return True
204
+ else:
205
+ print(f"❌ 内存状态检查失败: {response.status_code}")
206
+ return False
207
+ except Exception as e:
208
+ print(f"❌ 内存状态检查异常: {e}")
209
+ return False
210
+
211
+ def main():
212
+ """主测试函数"""
213
+ print("🚀 PicExam API 使用示例")
214
+ print("=" * 60)
215
+
216
+ tests = [
217
+ ("API 信息获取", test_api_info),
218
+ ("健康检查", test_health_check),
219
+ ("内存状态", test_memory_status),
220
+ ("文件上传分析", test_file_upload),
221
+ ("Base64 表单分析", test_base64_form),
222
+ ("JSON API 分析", test_json_api),
223
+ ]
224
+
225
+ results = []
226
+ for test_name, test_func in tests:
227
+ try:
228
+ result = test_func()
229
+ results.append((test_name, result))
230
+ if result:
231
+ print(f"✅ {test_name} - 成功")
232
+ else:
233
+ print(f"❌ {test_name} - 失败")
234
+ except Exception as e:
235
+ print(f"❌ {test_name} - 异常: {e}")
236
+ results.append((test_name, False))
237
+
238
+ print("-" * 40)
239
+
240
+ # 总结
241
+ print("\n📊 测试结果总结:")
242
+ passed = sum(1 for _, result in results if result)
243
+ total = len(results)
244
+
245
+ for test_name, result in results:
246
+ status = "✅ 通过" if result else "❌ 失败"
247
+ print(f" {test_name}: {status}")
248
+
249
+ print(f"\n总计: {passed}/{total} 测试通过")
250
+
251
+ if passed == total:
252
+ print("🎉 所有测试都通过了!API 运行正常。")
253
+ print("\n💡 使用提示:")
254
+ print(f" - Web 界面: {BASE_URL}/web")
255
+ print(f" - API 文档: {BASE_URL}/docs")
256
+ print(f" - API 信息: {BASE_URL}/")
257
+ else:
258
+ print("⚠️ 部分测试失败,请检查服务状态。")
259
+
260
+ if __name__ == "__main__":
261
+ main()
quick_test.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ 快速测试脚本 - 验证 PicExam API 的核心功能
4
+ """
5
+
6
+ import requests
7
+ import time
8
+ import json
9
+
10
+ def wait_for_service(max_wait=60):
11
+ """等待服务启动"""
12
+ print("⏳ 等待服务启动...")
13
+ start_time = time.time()
14
+
15
+ while time.time() - start_time < max_wait:
16
+ try:
17
+ response = requests.get("http://localhost:7860/health", timeout=5)
18
+ if response.status_code == 200:
19
+ print("✅ 服务已启动")
20
+ return True
21
+ except:
22
+ pass
23
+
24
+ print(".", end="", flush=True)
25
+ time.sleep(2)
26
+
27
+ print("\n❌ 服务启动超时")
28
+ return False
29
+
30
+ def test_endpoints():
31
+ """测试主要端点"""
32
+ print("\n🔍 测试 API 端点...")
33
+
34
+ endpoints = [
35
+ ("GET /", "API 信息"),
36
+ ("GET /health", "健康检查"),
37
+ ("GET /memory_status", "内存状态"),
38
+ ("GET /web", "Web 界面"),
39
+ ("GET /docs", "API 文档")
40
+ ]
41
+
42
+ results = []
43
+ for endpoint, description in endpoints:
44
+ try:
45
+ method, path = endpoint.split(" ", 1)
46
+ url = f"http://localhost:7860{path}"
47
+
48
+ if method == "GET":
49
+ response = requests.get(url, timeout=10)
50
+
51
+ if response.status_code == 200:
52
+ print(f"✅ {endpoint} - {description}")
53
+ results.append(True)
54
+ else:
55
+ print(f"❌ {endpoint} - {description} (状态码: {response.status_code})")
56
+ results.append(False)
57
+
58
+ except Exception as e:
59
+ print(f"❌ {endpoint} - {description} (错误: {e})")
60
+ results.append(False)
61
+
62
+ return results
63
+
64
+ def show_api_info():
65
+ """显示 API 信息"""
66
+ print("\n📋 API 信息:")
67
+ try:
68
+ response = requests.get("http://localhost:7860/")
69
+ if response.status_code == 200:
70
+ data = response.json()
71
+ print(f" 服务: {data['service']}")
72
+ print(f" 版本: {data['version']}")
73
+ print(f" 模型: {data['model']}")
74
+ print(f" 状态: {'✅ 模型已加载' if data['status']['model_loaded'] else '⏳ 模型加载中'}")
75
+ print(f" 推理模式: {data['status']['inference_mode']}")
76
+
77
+ print(f"\n📚 可用端点 ({len(data['endpoints'])} 个):")
78
+ for endpoint, info in data['endpoints'].items():
79
+ print(f" {endpoint}: {info['description']}")
80
+
81
+ return True
82
+ else:
83
+ print("❌ 无法获取 API 信息")
84
+ return False
85
+ except Exception as e:
86
+ print(f"❌ 获取 API 信息失败: {e}")
87
+ return False
88
+
89
+ def show_usage_examples():
90
+ """显示使用示例"""
91
+ print("\n💡 使用示例:")
92
+ print("=" * 50)
93
+
94
+ examples = [
95
+ {
96
+ "title": "1. 浏览器访问",
97
+ "commands": [
98
+ "Web 界面: http://localhost:7860/web",
99
+ "API 文档: http://localhost:7860/docs",
100
+ "API 信息: http://localhost:7860/"
101
+ ]
102
+ },
103
+ {
104
+ "title": "2. 文件上传分析",
105
+ "commands": [
106
+ "curl -X POST 'http://localhost:7860/analyze_image' \\",
107
+ " -F 'image=@your_image.jpg' \\",
108
+ " -F 'question=请描述这张图片'"
109
+ ]
110
+ },
111
+ {
112
+ "title": "3. Base64 图片分析",
113
+ "commands": [
114
+ "curl -X POST 'http://localhost:7860/analyze_image_base64' \\",
115
+ " -F 'image_base64=data:image/jpeg;base64,/9j/4AAQ...' \\",
116
+ " -F 'question=这张图片中有什么?'"
117
+ ]
118
+ },
119
+ {
120
+ "title": "4. JSON API 调用",
121
+ "commands": [
122
+ "curl -X POST 'http://localhost:7860/analyze' \\",
123
+ " -H 'Content-Type: application/json' \\",
124
+ " -d '{\"image\":\"data:image/jpeg;base64,...\",\"prompt\":\"描述图片\"}'",
125
+ ]
126
+ },
127
+ {
128
+ "title": "5. Python 调用示例",
129
+ "commands": [
130
+ "python example_usage.py # 运行完整示例",
131
+ "python test_api.py # 运行详细测试"
132
+ ]
133
+ }
134
+ ]
135
+
136
+ for example in examples:
137
+ print(f"\n{example['title']}:")
138
+ for cmd in example['commands']:
139
+ print(f" {cmd}")
140
+
141
+ def main():
142
+ """主函数"""
143
+ print("🚀 PicExam API 快速测试")
144
+ print("=" * 50)
145
+
146
+ # 等待服务启动
147
+ if not wait_for_service():
148
+ print("❌ 服务未启动,请先运行:")
149
+ print(" python start_local.py")
150
+ print(" 或")
151
+ print(" uvicorn app:app --host 0.0.0.0 --port 7860")
152
+ return
153
+
154
+ # 显示 API 信息
155
+ show_api_info()
156
+
157
+ # 测试端点
158
+ results = test_endpoints()
159
+
160
+ # 显示测试结果
161
+ passed = sum(results)
162
+ total = len(results)
163
+ print(f"\n📊 端点测试结果: {passed}/{total} 通过")
164
+
165
+ if passed == total:
166
+ print("🎉 所有基础端点都正常工作!")
167
+ else:
168
+ print("⚠️ 部分端点可能有问题")
169
+
170
+ # 显示使用示例
171
+ show_usage_examples()
172
+
173
+ print("\n" + "=" * 50)
174
+ print("✨ 快速测试完成!")
175
+ print("💡 提示: 运行 'python example_usage.py' 进行完整的功能测试")
176
+
177
+ if __name__ == "__main__":
178
+ main()
requirements.txt CHANGED
@@ -1,2 +1,10 @@
1
  fastapi
2
  uvicorn[standard]
 
 
 
 
 
 
 
 
 
1
  fastapi
2
  uvicorn[standard]
3
+ torch>=2.0.0
4
+ transformers>=4.37.0
5
+ accelerate
6
+ qwen-vl-utils
7
+ Pillow
8
+ requests
9
+ numpy
10
+ psutil
start_local.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ 本地启动 Qwen-VL PicExam API 的脚本
4
+ 适用于 16GB 内存 + CPU 推理环境
5
+ """
6
+
7
+ import subprocess
8
+ import sys
9
+ import os
10
+ import time
11
+ import psutil
12
+
13
+ def check_memory():
14
+ """检查系统内存是否足够"""
15
+ memory = psutil.virtual_memory()
16
+ total_gb = memory.total / 1024**3
17
+ available_gb = memory.available / 1024**3
18
+
19
+ print(f"💾 系统内存状态:")
20
+ print(f" 总内存: {total_gb:.1f}GB")
21
+ print(f" 可用内存: {available_gb:.1f}GB")
22
+ print(f" 使用率: {memory.percent:.1f}%")
23
+
24
+ if total_gb < 15:
25
+ print("⚠️ 警告: 系统内存少于 16GB,可能影响模型运行")
26
+ return False
27
+
28
+ if available_gb < 8:
29
+ print("⚠️ 警告: 可用内存少于 8GB,建议关闭其他程序")
30
+ return False
31
+
32
+ print("✅ 内存检查通过")
33
+ return True
34
+
35
+ def install_dependencies():
36
+ """安装依赖包"""
37
+ print("📦 安装依赖包...")
38
+ try:
39
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
40
+ print("✅ 依赖包安装完成")
41
+ return True
42
+ except subprocess.CalledProcessError as e:
43
+ print(f"❌ 依赖包安装失败: {e}")
44
+ return False
45
+
46
+ def start_server():
47
+ """启动服务器"""
48
+ print("🚀 启动 Qwen-VL PicExam API 服务器...")
49
+ print("📝 注意: 首次启动会下载模型,可能需要较长时间")
50
+ print("🔗 服务器启动后可访问: http://localhost:7860")
51
+ print("📚 API 文档: http://localhost:7860/docs")
52
+ print("-" * 50)
53
+
54
+ try:
55
+ # 设置环境变量
56
+ env = os.environ.copy()
57
+ env["PYTORCH_CUDA_ALLOC_CONF"] = "max_split_size_mb:512"
58
+ env["TOKENIZERS_PARALLELISM"] = "false"
59
+ env["OMP_NUM_THREADS"] = "4"
60
+ env["MKL_NUM_THREADS"] = "4"
61
+
62
+ # 启动服务器
63
+ subprocess.run([
64
+ sys.executable, "-m", "uvicorn",
65
+ "app:app",
66
+ "--host", "0.0.0.0",
67
+ "--port", "7860",
68
+ "--reload",
69
+ "--timeout-keep-alive", "300"
70
+ ], env=env)
71
+
72
+ except KeyboardInterrupt:
73
+ print("\n🛑 服务器已停止")
74
+ except Exception as e:
75
+ print(f"❌ 服务器启动失败: {e}")
76
+
77
+ def main():
78
+ """主函数"""
79
+ print("🤖 Qwen-VL PicExam API 本地启动器")
80
+ print("=" * 50)
81
+ print("📋 配置信息:")
82
+ print(" - 模型: Qwen2-VL-2B-Instruct")
83
+ print(" - 推理: CPU 模式")
84
+ print(" - 内存优化: 启用")
85
+ print(" - 端口: 7860")
86
+ print("=" * 50)
87
+
88
+ # 检查内存
89
+ if not check_memory():
90
+ response = input("是否继续启动? (y/N): ")
91
+ if response.lower() != 'y':
92
+ print("启动已取消")
93
+ return
94
+
95
+ # 安装依赖
96
+ if not install_dependencies():
97
+ print("❌ 无法安装依赖,启动失败")
98
+ return
99
+
100
+ # 启动服务器
101
+ start_server()
102
+
103
+ if __name__ == "__main__":
104
+ main()
static/index.html ADDED
@@ -0,0 +1,552 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>PicExam - Qwen-VL 图像理解</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 800px;
23
+ margin: 0 auto;
24
+ background: white;
25
+ border-radius: 15px;
26
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1);
27
+ overflow: hidden;
28
+ }
29
+
30
+ .header {
31
+ background: linear-gradient(135deg, #ff6b6b, #ee5a24);
32
+ color: white;
33
+ padding: 30px;
34
+ text-align: center;
35
+ }
36
+
37
+ .header h1 {
38
+ font-size: 2.5em;
39
+ margin-bottom: 10px;
40
+ }
41
+
42
+ .header p {
43
+ font-size: 1.1em;
44
+ opacity: 0.9;
45
+ }
46
+
47
+ .content {
48
+ padding: 30px;
49
+ }
50
+
51
+ .upload-area {
52
+ border: 3px dashed #ddd;
53
+ border-radius: 10px;
54
+ padding: 40px;
55
+ text-align: center;
56
+ margin-bottom: 20px;
57
+ transition: all 0.3s ease;
58
+ cursor: pointer;
59
+ }
60
+
61
+ .upload-area:hover {
62
+ border-color: #667eea;
63
+ background-color: #f8f9ff;
64
+ }
65
+
66
+ .upload-area.dragover {
67
+ border-color: #667eea;
68
+ background-color: #f0f2ff;
69
+ }
70
+
71
+ .upload-icon {
72
+ font-size: 3em;
73
+ color: #ddd;
74
+ margin-bottom: 15px;
75
+ }
76
+
77
+ .form-group {
78
+ margin-bottom: 20px;
79
+ }
80
+
81
+ label {
82
+ display: block;
83
+ margin-bottom: 8px;
84
+ font-weight: 600;
85
+ color: #333;
86
+ }
87
+
88
+ input[type="file"], textarea {
89
+ width: 100%;
90
+ padding: 12px;
91
+ border: 2px solid #ddd;
92
+ border-radius: 8px;
93
+ font-size: 16px;
94
+ transition: border-color 0.3s ease;
95
+ }
96
+
97
+ input[type="file"]:focus, textarea:focus {
98
+ outline: none;
99
+ border-color: #667eea;
100
+ }
101
+
102
+ textarea {
103
+ resize: vertical;
104
+ min-height: 80px;
105
+ }
106
+
107
+ .btn {
108
+ background: linear-gradient(135deg, #667eea, #764ba2);
109
+ color: white;
110
+ border: none;
111
+ padding: 15px 30px;
112
+ border-radius: 8px;
113
+ font-size: 16px;
114
+ font-weight: 600;
115
+ cursor: pointer;
116
+ transition: all 0.3s ease;
117
+ width: 100%;
118
+ }
119
+
120
+ .btn:hover {
121
+ transform: translateY(-2px);
122
+ box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
123
+ }
124
+
125
+ .btn:disabled {
126
+ background: #ccc;
127
+ cursor: not-allowed;
128
+ transform: none;
129
+ box-shadow: none;
130
+ }
131
+
132
+ .result {
133
+ margin-top: 30px;
134
+ padding: 20px;
135
+ background: #f8f9fa;
136
+ border-radius: 10px;
137
+ border-left: 5px solid #667eea;
138
+ }
139
+
140
+ .result h3 {
141
+ color: #333;
142
+ margin-bottom: 15px;
143
+ }
144
+
145
+ .result-content {
146
+ background: white;
147
+ padding: 15px;
148
+ border-radius: 8px;
149
+ border: 1px solid #e9ecef;
150
+ }
151
+
152
+ .loading {
153
+ display: none;
154
+ text-align: center;
155
+ padding: 20px;
156
+ }
157
+
158
+ .spinner {
159
+ border: 4px solid #f3f3f3;
160
+ border-top: 4px solid #667eea;
161
+ border-radius: 50%;
162
+ width: 40px;
163
+ height: 40px;
164
+ animation: spin 1s linear infinite;
165
+ margin: 0 auto 15px;
166
+ }
167
+
168
+ @keyframes spin {
169
+ 0% { transform: rotate(0deg); }
170
+ 100% { transform: rotate(360deg); }
171
+ }
172
+
173
+ .error {
174
+ background: #ffe6e6;
175
+ border-left-color: #ff4757;
176
+ color: #c44569;
177
+ }
178
+
179
+ .preview-image {
180
+ max-width: 100%;
181
+ max-height: 300px;
182
+ border-radius: 8px;
183
+ margin: 15px 0;
184
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
185
+ }
186
+
187
+ .status-bar {
188
+ background: #f8f9fa;
189
+ padding: 15px;
190
+ border-radius: 8px;
191
+ margin-bottom: 20px;
192
+ display: flex;
193
+ justify-content: space-between;
194
+ align-items: center;
195
+ }
196
+
197
+ .status-indicator {
198
+ display: flex;
199
+ align-items: center;
200
+ gap: 8px;
201
+ }
202
+
203
+ .status-dot {
204
+ width: 10px;
205
+ height: 10px;
206
+ border-radius: 50%;
207
+ background: #28a745;
208
+ }
209
+
210
+ .status-dot.loading {
211
+ background: #ffc107;
212
+ animation: pulse 1.5s infinite;
213
+ }
214
+
215
+ .status-dot.error {
216
+ background: #dc3545;
217
+ }
218
+
219
+ @keyframes pulse {
220
+ 0%, 100% { opacity: 1; }
221
+ 50% { opacity: 0.5; }
222
+ }
223
+ </style>
224
+ </head>
225
+ <body>
226
+ <div class="container">
227
+ <div class="header">
228
+ <h1>🏆 PicExam</h1>
229
+ <p>基于 Qwen-VL 的智能图像理解系统</p>
230
+ <div style="margin-top: 15px; font-size: 0.9em;">
231
+ <a href="/docs" style="color: white; text-decoration: none; margin-right: 15px;">📚 API 文档</a>
232
+ <a href="/" style="color: white; text-decoration: none; margin-right: 15px;">🔗 API 端点</a>
233
+ <a href="/memory_status" style="color: white; text-decoration: none;">💾 内存状态</a>
234
+ </div>
235
+ </div>
236
+
237
+ <div class="content">
238
+ <div class="status-bar">
239
+ <div class="status-indicator">
240
+ <div class="status-dot" id="statusDot"></div>
241
+ <span id="statusText">检查服务状态...</span>
242
+ </div>
243
+ <button onclick="checkStatus()" style="background: none; border: 1px solid #ddd; padding: 5px 10px; border-radius: 5px; cursor: pointer;">刷新</button>
244
+ </div>
245
+
246
+ <form id="uploadForm">
247
+ <div class="form-group">
248
+ <label for="imageFile">选择图片</label>
249
+ <div class="upload-area" id="uploadArea">
250
+ <div class="upload-icon">📷</div>
251
+ <p>点击选择图片或拖拽图片到此处</p>
252
+ <p style="font-size: 0.9em; color: #666; margin-top: 10px;">支持 JPG, PNG, WebP 格式</p>
253
+ </div>
254
+ <input type="file" id="imageFile" accept="image/*" style="display: none;">
255
+ <img id="previewImage" class="preview-image" style="display: none;">
256
+ </div>
257
+
258
+ <div class="form-group">
259
+ <label for="question">问题描述</label>
260
+ <textarea id="question" placeholder="请输入您想问的关于图片的问题,例如:请描述这张图片的内容、图片中有什么物体、图片的颜色如何等...">请描述这张图片的内容</textarea>
261
+ </div>
262
+
263
+ <button type="submit" class="btn" id="submitBtn">
264
+ 🔍 分析图片
265
+ </button>
266
+ </form>
267
+
268
+ <div class="loading" id="loading">
269
+ <div class="spinner"></div>
270
+ <p>正在分析图片,请稍候...</p>
271
+ </div>
272
+
273
+ <div id="result" style="display: none;"></div>
274
+
275
+ <!-- API 测试区域 -->
276
+ <div style="margin-top: 40px; padding-top: 30px; border-top: 2px solid #eee;">
277
+ <h2 style="color: #333; margin-bottom: 20px;">🔧 API 测试工具</h2>
278
+
279
+ <div style="background: #f8f9fa; padding: 20px; border-radius: 10px; margin-bottom: 20px;">
280
+ <h3 style="color: #555; margin-bottom: 15px;">JSON API 测试 (/analyze)</h3>
281
+ <div style="margin-bottom: 15px;">
282
+ <label style="display: block; margin-bottom: 5px; font-weight: 600;">提示词:</label>
283
+ <input type="text" id="jsonPrompt" value="请详细描述这张图片的内容" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 5px;">
284
+ </div>
285
+ <button onclick="testJsonAPI()" style="background: #28a745; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer;">测试 JSON API</button>
286
+ <div id="jsonResult" style="margin-top: 15px; display: none;"></div>
287
+ </div>
288
+
289
+ <div style="background: #f8f9fa; padding: 20px; border-radius: 10px;">
290
+ <h3 style="color: #555; margin-bottom: 15px;">API 端点信息</h3>
291
+ <button onclick="showAPIInfo()" style="background: #17a2b8; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer;">获取 API 信息</button>
292
+ <div id="apiInfo" style="margin-top: 15px; display: none;"></div>
293
+ </div>
294
+ </div>
295
+ </div>
296
+ </div>
297
+
298
+ <script>
299
+ // 检查服务状态
300
+ async function checkStatus() {
301
+ const statusDot = document.getElementById('statusDot');
302
+ const statusText = document.getElementById('statusText');
303
+
304
+ statusDot.className = 'status-dot loading';
305
+ statusText.textContent = '检查中...';
306
+
307
+ try {
308
+ const response = await fetch('/');
309
+ const data = await response.json();
310
+
311
+ if (data.model_loaded) {
312
+ statusDot.className = 'status-dot';
313
+ statusText.textContent = '服务正常,模型已加载';
314
+ } else {
315
+ statusDot.className = 'status-dot error';
316
+ statusText.textContent = '服务运行中,模型加载中...';
317
+ }
318
+ } catch (error) {
319
+ statusDot.className = 'status-dot error';
320
+ statusText.textContent = '服务连接失败';
321
+ }
322
+ }
323
+
324
+ // 页面加载时检查状态
325
+ window.addEventListener('load', checkStatus);
326
+
327
+ // 文件上传处理
328
+ const uploadArea = document.getElementById('uploadArea');
329
+ const fileInput = document.getElementById('imageFile');
330
+ const previewImage = document.getElementById('previewImage');
331
+
332
+ uploadArea.addEventListener('click', () => fileInput.click());
333
+
334
+ uploadArea.addEventListener('dragover', (e) => {
335
+ e.preventDefault();
336
+ uploadArea.classList.add('dragover');
337
+ });
338
+
339
+ uploadArea.addEventListener('dragleave', () => {
340
+ uploadArea.classList.remove('dragover');
341
+ });
342
+
343
+ uploadArea.addEventListener('drop', (e) => {
344
+ e.preventDefault();
345
+ uploadArea.classList.remove('dragover');
346
+
347
+ const files = e.dataTransfer.files;
348
+ if (files.length > 0) {
349
+ fileInput.files = files;
350
+ handleFileSelect();
351
+ }
352
+ });
353
+
354
+ fileInput.addEventListener('change', handleFileSelect);
355
+
356
+ function handleFileSelect() {
357
+ const file = fileInput.files[0];
358
+ if (file) {
359
+ const reader = new FileReader();
360
+ reader.onload = (e) => {
361
+ previewImage.src = e.target.result;
362
+ previewImage.style.display = 'block';
363
+ uploadArea.innerHTML = `
364
+ <div class="upload-icon">✅</div>
365
+ <p>已选择: ${file.name}</p>
366
+ <p style="font-size: 0.9em; color: #666;">点击重新选择</p>
367
+ `;
368
+ };
369
+ reader.readAsDataURL(file);
370
+ }
371
+ }
372
+
373
+ // 表单提交处理
374
+ document.getElementById('uploadForm').addEventListener('submit', async (e) => {
375
+ e.preventDefault();
376
+
377
+ const file = fileInput.files[0];
378
+ const question = document.getElementById('question').value;
379
+
380
+ if (!file) {
381
+ alert('请先选择一张图片');
382
+ return;
383
+ }
384
+
385
+ const submitBtn = document.getElementById('submitBtn');
386
+ const loading = document.getElementById('loading');
387
+ const result = document.getElementById('result');
388
+
389
+ // 显示加载状态
390
+ submitBtn.disabled = true;
391
+ loading.style.display = 'block';
392
+ result.style.display = 'none';
393
+
394
+ try {
395
+ const formData = new FormData();
396
+ formData.append('image', file);
397
+ formData.append('question', question);
398
+
399
+ const response = await fetch('/analyze_image', {
400
+ method: 'POST',
401
+ body: formData
402
+ });
403
+
404
+ const data = await response.json();
405
+
406
+ if (data.success) {
407
+ result.innerHTML = `
408
+ <div class="result">
409
+ <h3>📝 分析结果</h3>
410
+ <div class="result-content">
411
+ <p><strong>问题:</strong> ${data.question}</p>
412
+ <p><strong>回答:</strong> ${data.answer}</p>
413
+ <p><strong>图片信息:</strong> ${data.image_info.filename} (${data.image_info.size})</p>
414
+ </div>
415
+ </div>
416
+ `;
417
+ } else {
418
+ throw new Error(data.error || '分析失败');
419
+ }
420
+ } catch (error) {
421
+ result.innerHTML = `
422
+ <div class="result error">
423
+ <h3>❌ 分析失败</h3>
424
+ <div class="result-content">
425
+ <p>${error.message}</p>
426
+ </div>
427
+ </div>
428
+ `;
429
+ } finally {
430
+ submitBtn.disabled = false;
431
+ loading.style.display = 'none';
432
+ result.style.display = 'block';
433
+ }
434
+ });
435
+
436
+ // JSON API 测试
437
+ async function testJsonAPI() {
438
+ const file = fileInput.files[0];
439
+ const prompt = document.getElementById('jsonPrompt').value;
440
+ const resultDiv = document.getElementById('jsonResult');
441
+
442
+ if (!file) {
443
+ alert('请先选择一张图片');
444
+ return;
445
+ }
446
+
447
+ try {
448
+ // 将图片转换为 base64
449
+ const base64 = await fileToBase64(file);
450
+
451
+ const requestData = {
452
+ image: base64,
453
+ prompt: prompt
454
+ };
455
+
456
+ resultDiv.innerHTML = '<p>🔄 正在调用 JSON API...</p>';
457
+ resultDiv.style.display = 'block';
458
+
459
+ const response = await fetch('/analyze', {
460
+ method: 'POST',
461
+ headers: {
462
+ 'Content-Type': 'application/json'
463
+ },
464
+ body: JSON.stringify(requestData)
465
+ });
466
+
467
+ const data = await response.json();
468
+
469
+ if (data.success) {
470
+ resultDiv.innerHTML = `
471
+ <div style="background: #d4edda; border: 1px solid #c3e6cb; padding: 15px; border-radius: 5px;">
472
+ <h4 style="color: #155724; margin-bottom: 10px;">✅ JSON API 调用成功</h4>
473
+ <p><strong>提示词:</strong> ${data.prompt}</p>
474
+ <p><strong>响应:</strong> ${data.response}</p>
475
+ <p><strong>处理时间:</strong> ${data.processing_time.toFixed(2)}秒</p>
476
+ <p><strong>图片信息:</strong> ${data.image_info.size} (${data.image_info.mode})</p>
477
+ </div>
478
+ `;
479
+ } else {
480
+ resultDiv.innerHTML = `
481
+ <div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 5px;">
482
+ <h4 style="color: #721c24; margin-bottom: 10px;">❌ JSON API 调用失败</h4>
483
+ <p><strong>错误:</strong> ${data.error}</p>
484
+ </div>
485
+ `;
486
+ }
487
+ } catch (error) {
488
+ resultDiv.innerHTML = `
489
+ <div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 5px;">
490
+ <h4 style="color: #721c24; margin-bottom: 10px;">❌ 请求失败</h4>
491
+ <p><strong>错误:</strong> ${error.message}</p>
492
+ </div>
493
+ `;
494
+ }
495
+ }
496
+
497
+ // 显示 API 信息
498
+ async function showAPIInfo() {
499
+ const infoDiv = document.getElementById('apiInfo');
500
+
501
+ try {
502
+ infoDiv.innerHTML = '<p>🔄 获取 API 信息...</p>';
503
+ infoDiv.style.display = 'block';
504
+
505
+ const response = await fetch('/');
506
+ const data = await response.json();
507
+
508
+ let endpointsHtml = '';
509
+ for (const [endpoint, info] of Object.entries(data.endpoints)) {
510
+ endpointsHtml += `
511
+ <div style="margin-bottom: 15px; padding: 10px; background: white; border-radius: 5px; border-left: 3px solid #667eea;">
512
+ <h5 style="color: #333; margin-bottom: 5px;">${endpoint}</h5>
513
+ <p style="color: #666; margin-bottom: 5px;">${info.description}</p>
514
+ ${info.example ? `<code style="background: #f1f1f1; padding: 2px 5px; border-radius: 3px; font-size: 0.9em;">${info.example}</code>` : ''}
515
+ </div>
516
+ `;
517
+ }
518
+
519
+ infoDiv.innerHTML = `
520
+ <div style="background: #e7f3ff; border: 1px solid #b3d9ff; padding: 15px; border-radius: 5px;">
521
+ <h4 style="color: #0056b3; margin-bottom: 15px;">📋 API 端点信息</h4>
522
+ <p><strong>服务:</strong> ${data.service}</p>
523
+ <p><strong>版本:</strong> ${data.version}</p>
524
+ <p><strong>模型:</strong> ${data.model}</p>
525
+ <p><strong>状态:</strong> ${data.status.model_loaded ? '✅ 模型已加载' : '⏳ 模型加载中'}</p>
526
+ <hr style="margin: 15px 0; border: none; border-top: 1px solid #ccc;">
527
+ <h5 style="color: #333; margin-bottom: 10px;">可用端点:</h5>
528
+ ${endpointsHtml}
529
+ </div>
530
+ `;
531
+ } catch (error) {
532
+ infoDiv.innerHTML = `
533
+ <div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 5px;">
534
+ <h4 style="color: #721c24; margin-bottom: 10px;">❌ 获取 API 信息失败</h4>
535
+ <p><strong>错误:</strong> ${error.message}</p>
536
+ </div>
537
+ `;
538
+ }
539
+ }
540
+
541
+ // 文件转 base64 工具函数
542
+ function fileToBase64(file) {
543
+ return new Promise((resolve, reject) => {
544
+ const reader = new FileReader();
545
+ reader.readAsDataURL(file);
546
+ reader.onload = () => resolve(reader.result);
547
+ reader.onerror = error => reject(error);
548
+ });
549
+ }
550
+ </script>
551
+ </body>
552
+ </html>
test_api.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ 测试 Qwen-VL PicExam API 的脚本
4
+ 用于验证模型加载和推理功能
5
+ """
6
+
7
+ import requests
8
+ import base64
9
+ import json
10
+ import time
11
+ from PIL import Image
12
+ import io
13
+
14
+ def test_health_check():
15
+ """测试健康检查接口"""
16
+ print("🔍 测试健康检查接口...")
17
+ try:
18
+ response = requests.get("http://localhost:7860/")
19
+ print(f"状态码: {response.status_code}")
20
+ print(f"响应: {response.json()}")
21
+ return response.status_code == 200
22
+ except Exception as e:
23
+ print(f"❌ 健康检查失败: {e}")
24
+ return False
25
+
26
+ def test_memory_status():
27
+ """测试内存状态接口"""
28
+ print("\n🔍 测试内存状态接口...")
29
+ try:
30
+ response = requests.get("http://localhost:7860/memory_status")
31
+ print(f"状态码: {response.status_code}")
32
+ data = response.json()
33
+ print(f"系统内存: {data['system_memory']['used_gb']:.2f}GB / {data['system_memory']['total_gb']:.2f}GB ({data['system_memory']['percent']:.1f}%)")
34
+ print(f"模型已加载: {data['model_loaded']}")
35
+ print(f"内存使用正常: {data['recommendations']['memory_usage_ok']}")
36
+ return response.status_code == 200
37
+ except Exception as e:
38
+ print(f"❌ 内存状态检查失败: {e}")
39
+ return False
40
+
41
+ def create_test_image():
42
+ """创建一个简单的测试图片"""
43
+ # 创建一个简单的彩色图片
44
+ img = Image.new('RGB', (200, 200), color='red')
45
+
46
+ # 添加一些简单的图形
47
+ from PIL import ImageDraw
48
+ draw = ImageDraw.Draw(img)
49
+ draw.rectangle([50, 50, 150, 150], fill='blue')
50
+ draw.ellipse([75, 75, 125, 125], fill='yellow')
51
+
52
+ return img
53
+
54
+ def image_to_base64(image):
55
+ """将 PIL 图片转换为 base64 字符串"""
56
+ buffer = io.BytesIO()
57
+ image.save(buffer, format='PNG')
58
+ img_str = base64.b64encode(buffer.getvalue()).decode()
59
+ return f"data:image/png;base64,{img_str}"
60
+
61
+ def test_image_analysis():
62
+ """测试图片分析功能"""
63
+ print("\n🔍 测试图片分析功能...")
64
+
65
+ # 创建测试图片
66
+ test_img = create_test_image()
67
+
68
+ # 保存为临时文件
69
+ test_img.save("test_image.png")
70
+
71
+ try:
72
+ # 测试文件上传接口
73
+ print("测试文件上传接口...")
74
+ with open("test_image.png", "rb") as f:
75
+ files = {"image": ("test_image.png", f, "image/png")}
76
+ data = {"question": "请描述这张图片中的颜色和形状"}
77
+
78
+ start_time = time.time()
79
+ response = requests.post("http://localhost:7860/analyze_image", files=files, data=data)
80
+ end_time = time.time()
81
+
82
+ print(f"状态码: {response.status_code}")
83
+ print(f"推理时间: {end_time - start_time:.2f}秒")
84
+
85
+ if response.status_code == 200:
86
+ result = response.json()
87
+ print(f"问题: {result['question']}")
88
+ print(f"回答: {result['answer']}")
89
+ print(f"图片信息: {result['image_info']}")
90
+ return True
91
+ else:
92
+ print(f"❌ 请求失败: {response.text}")
93
+ return False
94
+
95
+ except Exception as e:
96
+ print(f"❌ 图片分析测试失败: {e}")
97
+ return False
98
+
99
+ def test_base64_analysis():
100
+ """测试 base64 图片分析功能"""
101
+ print("\n🔍 测试 base64 图片分析功能...")
102
+
103
+ try:
104
+ # 创建测试图片并转换为 base64
105
+ test_img = create_test_image()
106
+ img_base64 = image_to_base64(test_img)
107
+
108
+ data = {
109
+ "image_base64": img_base64,
110
+ "question": "这张图片中有什么几何形状?"
111
+ }
112
+
113
+ start_time = time.time()
114
+ response = requests.post("http://localhost:7860/analyze_image_base64", data=data)
115
+ end_time = time.time()
116
+
117
+ print(f"状态码: {response.status_code}")
118
+ print(f"推理时间: {end_time - start_time:.2f}秒")
119
+
120
+ if response.status_code == 200:
121
+ result = response.json()
122
+ print(f"问题: {result['question']}")
123
+ print(f"回答: {result['answer']}")
124
+ print(f"图片信息: {result['image_info']}")
125
+ return True
126
+ else:
127
+ print(f"❌ 请求失败: {response.text}")
128
+ return False
129
+
130
+ except Exception as e:
131
+ print(f"❌ base64 图片分析测试失败: {e}")
132
+ return False
133
+
134
+ def test_cache_clear():
135
+ """测试缓存清理功能"""
136
+ print("\n🔍 测试缓存清理功能...")
137
+ try:
138
+ response = requests.post("http://localhost:7860/clear_cache")
139
+ print(f"状态码: {response.status_code}")
140
+ print(f"响应: {response.json()}")
141
+ return response.status_code == 200
142
+ except Exception as e:
143
+ print(f"❌ 缓存清理测试失败: {e}")
144
+ return False
145
+
146
+ def main():
147
+ """主测试函数"""
148
+ print("🚀 开始测试 Qwen-VL PicExam API")
149
+ print("=" * 50)
150
+
151
+ # 等待服务启动
152
+ print("⏳ 等待服务启动...")
153
+ time.sleep(5)
154
+
155
+ tests = [
156
+ ("健康检查", test_health_check),
157
+ ("内存状态", test_memory_status),
158
+ ("图片分析(文件上传)", test_image_analysis),
159
+ ("图片分析(base64)", test_base64_analysis),
160
+ ("缓存清理", test_cache_clear),
161
+ ]
162
+
163
+ results = []
164
+ for test_name, test_func in tests:
165
+ try:
166
+ result = test_func()
167
+ results.append((test_name, result))
168
+ if result:
169
+ print(f"✅ {test_name} 测试通过")
170
+ else:
171
+ print(f"❌ {test_name} 测试失败")
172
+ except Exception as e:
173
+ print(f"❌ {test_name} 测试异常: {e}")
174
+ results.append((test_name, False))
175
+
176
+ print("-" * 30)
177
+
178
+ # 总结
179
+ print("\n📊 测试结果总结:")
180
+ passed = sum(1 for _, result in results if result)
181
+ total = len(results)
182
+
183
+ for test_name, result in results:
184
+ status = "✅ 通过" if result else "❌ 失败"
185
+ print(f" {test_name}: {status}")
186
+
187
+ print(f"\n总计: {passed}/{total} 测试通过")
188
+
189
+ if passed == total:
190
+ print("🎉 所有测试都通过了!API 运行正常。")
191
+ else:
192
+ print("⚠️ 部分测试失败,请检查日志。")
193
+
194
+ if __name__ == "__main__":
195
+ main()