init proj
This commit is contained in:
51
.dockerignore
Normal file
51
.dockerignore
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
*.egg-info
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
.venv
|
||||||
|
venv
|
||||||
|
env
|
||||||
|
ENV
|
||||||
|
|
||||||
|
# 环境变量文件(应该通过环境变量或 secrets 传递)
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
env.example
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# 文档
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# 其他
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.pytest_cache
|
||||||
|
.coverage
|
||||||
|
htmlcov
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile*
|
||||||
|
.dockerignore
|
||||||
|
docker-compose*.yml
|
||||||
|
|
||||||
|
# 锁定文件(可选,如果使用 uv 可以保留)
|
||||||
|
# uv.lock
|
||||||
|
|
||||||
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# 环境变量
|
||||||
|
.env
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.8
|
||||||
32
Dockerfile
Normal file
32
Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
FROM 172.16.102.3:30648/astral-sh/uv:python3.14-bookworm
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PIP_NO_CACHE_DIR=1 \
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
|
# 安装系统依赖
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
gcc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 复制项目文件
|
||||||
|
COPY pyproject.toml ./
|
||||||
|
COPY app.py ./
|
||||||
|
|
||||||
|
RUN uv sync
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD python -c "import httpx; httpx.get('http://localhost:8000/health', timeout=5)" || exit 1
|
||||||
|
|
||||||
|
# 启动应用
|
||||||
|
CMD ["uv", "run", "app.py"]
|
||||||
|
|
||||||
179
README.md
179
README.md
@@ -1,3 +1,178 @@
|
|||||||
# crop-disease
|
# 病虫害识别 API
|
||||||
|
|
||||||
|
基于 OpenRouter 的 Qwen3 VL 8B Instruct 模型构建的病虫害识别 FastAPI 服务。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 🖼️ 支持图片上传识别病虫害
|
||||||
|
- 🔍 智能识别植物病虫害类型和症状
|
||||||
|
- 💡 提供详细的防治建议
|
||||||
|
- 📡 支持 base64 编码图片识别
|
||||||
|
- 🚀 基于 FastAPI 的高性能异步接口
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
1. 克隆项目并进入目录
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd hm-qwen3-vl
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
或者使用 uv(推荐):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 配置环境变量
|
||||||
|
|
||||||
|
复制 `env.example` 为 `.env` 并填入你的 OpenRouter API 密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
编辑 `.env` 文件,填入你的 API 密钥:
|
||||||
|
|
||||||
|
```
|
||||||
|
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
获取 API 密钥:访问 [OpenRouter](https://openrouter.ai/keys) 注册并获取 API 密钥。
|
||||||
|
|
||||||
|
## 运行服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
或者使用 uvicorn 直接运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
服务启动后,访问以下地址:
|
||||||
|
|
||||||
|
- API 文档:http://localhost:8000/docs
|
||||||
|
- 健康检查:http://localhost:8000/health
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
### 1. 图片上传识别
|
||||||
|
|
||||||
|
**POST** `/api/v1/identify`
|
||||||
|
|
||||||
|
上传图片文件进行病虫害识别。
|
||||||
|
|
||||||
|
**请求参数:**
|
||||||
|
- `file` (file, required): 植物图片文件(支持 jpg, jpeg, png)
|
||||||
|
- `question` (string, optional): 自定义问题,不提供则使用默认提示
|
||||||
|
|
||||||
|
**示例(使用 curl):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8000/api/v1/identify" \
|
||||||
|
-H "accept: application/json" \
|
||||||
|
-H "Content-Type: multipart/form-data" \
|
||||||
|
-F "file=@/path/to/plant_image.jpg" \
|
||||||
|
-F "question=请识别这张图片中的病虫害问题"
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例(使用 Python requests):**
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
url = "http://localhost:8000/api/v1/identify"
|
||||||
|
files = {"file": open("plant_image.jpg", "rb")}
|
||||||
|
data = {"question": "请识别这张图片中的病虫害问题"}
|
||||||
|
|
||||||
|
response = requests.post(url, files=files, data=data)
|
||||||
|
print(response.json())
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"result": "根据图片分析,该植物叶片上出现了...",
|
||||||
|
"image_info": {
|
||||||
|
"format": "JPEG",
|
||||||
|
"size": [1920, 1080],
|
||||||
|
"mode": "RGB"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Base64 图片识别
|
||||||
|
|
||||||
|
**POST** `/api/v1/identify/base64`
|
||||||
|
|
||||||
|
使用 base64 编码的图片进行识别。
|
||||||
|
|
||||||
|
**请求体(JSON):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"image_base64": "iVBORw0KGgoAAAANS...",
|
||||||
|
"question": "请识别这张图片中的病虫害问题"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8000/api/v1/identify/base64" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"image_base64": "base64_encoded_image_string",
|
||||||
|
"question": "请识别病虫害"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 健康检查
|
||||||
|
|
||||||
|
**GET** `/health`
|
||||||
|
|
||||||
|
检查服务状态。
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"model": "qwen/qwen-3-vl-8b-instruct"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用 Swagger UI
|
||||||
|
|
||||||
|
启动服务后,访问 http://localhost:8000/docs 可以使用交互式 API 文档进行测试。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **API 密钥安全**:请妥善保管你的 OpenRouter API 密钥,不要将其提交到版本控制系统
|
||||||
|
2. **图片大小**:图片会自动缩放到最大 4096x4096 像素
|
||||||
|
3. **请求超时**:API 请求超时时间设置为 60 秒
|
||||||
|
4. **费用**:使用 OpenRouter API 可能会产生费用,请查看 [OpenRouter 定价](https://openrouter.ai/models)
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- FastAPI:现代、快速的 Web 框架
|
||||||
|
- OpenRouter:统一的 AI 模型 API 接口
|
||||||
|
- Qwen3 VL 8B Instruct:多模态视觉语言模型
|
||||||
|
- Pillow:图片处理库
|
||||||
|
- httpx:异步 HTTP 客户端
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
基于Qwen3 VL 模型的病虫害识别 FastAPI 服务
|
|
||||||
266
app.py
Normal file
266
app.py
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
"""
|
||||||
|
基于 OpenRouter Qwen3 VL 8B Instruct 模型的病虫害识别 FastAPI 服务
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import Optional
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastapi import FastAPI, File, UploadFile, HTTPException
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
# 加载环境变量
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="病虫害识别 API",
|
||||||
|
description="基于 OpenRouter Qwen3 VL 8B Instruct 模型的病虫害识别服务",
|
||||||
|
version="1.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
# OpenRouter API 配置
|
||||||
|
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
|
||||||
|
OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions"
|
||||||
|
MODEL_NAME = os.getenv("OPENROUTER_MODEL", "qwen/qwen3-vl-8b-instruct") # 可通过环境变量配置
|
||||||
|
|
||||||
|
if not OPENROUTER_API_KEY:
|
||||||
|
raise ValueError("请设置 OPENROUTER_API_KEY 环境变量")
|
||||||
|
|
||||||
|
|
||||||
|
class Base64ImageRequest(BaseModel):
|
||||||
|
"""Base64 图片识别请求模型"""
|
||||||
|
image_base64: str
|
||||||
|
question: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
def encode_image_to_base64(image: Image.Image) -> str:
|
||||||
|
"""将 PIL Image 转换为 base64 编码字符串"""
|
||||||
|
buffered = BytesIO()
|
||||||
|
# 转换为 RGB 模式(如果不是的话)
|
||||||
|
if image.mode != "RGB":
|
||||||
|
image = image.convert("RGB")
|
||||||
|
image.save(buffered, format="JPEG")
|
||||||
|
# 确保使用 UTF-8 编码解码 base64
|
||||||
|
img_str = base64.b64encode(buffered.getvalue()).decode('utf-8')
|
||||||
|
return img_str
|
||||||
|
|
||||||
|
|
||||||
|
async def call_openrouter_api(image_base64: str, user_message: str) -> str:
|
||||||
|
"""调用 OpenRouter API"""
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
# "HTTP-Referer": "https://github.com/your-repo", # 可选:用于跟踪
|
||||||
|
# "X-Title": "Pest Disease Identification Service", # 可选:应用名称(使用英文避免编码问题)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": MODEL_NAME,
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "你是一位专业的农业病虫害识别专家。请仔细分析用户提供的植物图片,识别可能存在的病虫害问题,并提供详细的诊断信息,包括:1. 病虫害类型和名称 2. 严重程度 3. 可能的病因 4. 防治建议。请用中文回答。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {
|
||||||
|
"url": f"data:image/jpeg;base64,{image_base64}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": user_message
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max_tokens": 2000,
|
||||||
|
"temperature": 0.7,
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
|
try:
|
||||||
|
response = await client.post(OPENROUTER_API_URL, json=payload, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
if "choices" in result and len(result["choices"]) > 0:
|
||||||
|
return result["choices"][0]["message"]["content"]
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=500, detail="API 响应格式错误")
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
error_text = e.response.text
|
||||||
|
try:
|
||||||
|
error_text.encode('utf-8')
|
||||||
|
except (UnicodeEncodeError, AttributeError):
|
||||||
|
error_text = repr(e.response.text)
|
||||||
|
raise HTTPException(status_code=e.response.status_code, detail=f"OpenRouter API 错误: {error_text}")
|
||||||
|
except httpx.RequestError as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
try:
|
||||||
|
error_msg.encode('utf-8')
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
error_msg = repr(e)
|
||||||
|
raise HTTPException(status_code=500, detail=f"请求错误: {error_msg}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
"""根路径,返回 API 信息"""
|
||||||
|
return {
|
||||||
|
"message": "病虫害识别 API",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"model": MODEL_NAME,
|
||||||
|
"endpoints": {
|
||||||
|
"识别病虫害": "/api/v1/identify",
|
||||||
|
"健康检查": "/health"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""健康检查端点"""
|
||||||
|
return {"status": "healthy", "model": MODEL_NAME}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/identify")
|
||||||
|
async def identify_pest_disease(
|
||||||
|
file: UploadFile = File(..., description="植物图片文件"),
|
||||||
|
question: Optional[str] = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
病虫害识别接口
|
||||||
|
|
||||||
|
- **file**: 上传的植物图片(支持 jpg, jpeg, png 格式)
|
||||||
|
- **question**: 可选的自定义问题,如果不提供则使用默认提示
|
||||||
|
"""
|
||||||
|
# 验证文件类型
|
||||||
|
if not file.content_type or not file.content_type.startswith("image/"):
|
||||||
|
raise HTTPException(status_code=400, detail="请上传图片文件(jpg, jpeg, png)")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 读取并验证图片
|
||||||
|
image_data = await file.read()
|
||||||
|
image = Image.open(BytesIO(image_data))
|
||||||
|
|
||||||
|
# 验证图片大小(可选:限制最大尺寸)
|
||||||
|
max_size = (4096, 4096)
|
||||||
|
if image.size[0] > max_size[0] or image.size[1] > max_size[1]:
|
||||||
|
image.thumbnail(max_size, Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
# 转换为 base64
|
||||||
|
image_base64 = encode_image_to_base64(image)
|
||||||
|
|
||||||
|
# 构建用户消息
|
||||||
|
user_message = question or "请识别这张图片中的植物是否存在病虫害问题,如果存在,请详细说明病虫害类型、症状、严重程度和防治建议。"
|
||||||
|
|
||||||
|
# 调用 OpenRouter API
|
||||||
|
result = await call_openrouter_api(image_base64, user_message)
|
||||||
|
|
||||||
|
return JSONResponse(content={
|
||||||
|
"success": True,
|
||||||
|
"result": result,
|
||||||
|
"image_info": {
|
||||||
|
"format": image.format,
|
||||||
|
"size": image.size,
|
||||||
|
"mode": image.mode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except UnicodeEncodeError as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"处理图片时出错: 编码错误 - {repr(e)}")
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
# 确保错误信息可以正确编码
|
||||||
|
try:
|
||||||
|
error_msg.encode('utf-8')
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
error_msg = repr(e)
|
||||||
|
raise HTTPException(status_code=500, detail=f"处理图片时出错: {error_msg}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/identify/base64")
|
||||||
|
async def identify_pest_disease_base64(request: Base64ImageRequest):
|
||||||
|
"""
|
||||||
|
使用 base64 编码图片的病虫害识别接口
|
||||||
|
|
||||||
|
- **image_base64**: base64 编码的图片数据(不包含 data:image 前缀)
|
||||||
|
- **question**: 可选的自定义问题
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 清理 base64 字符串(移除可能的 data:image 前缀和空白字符)
|
||||||
|
base64_str = request.image_base64.strip()
|
||||||
|
if "," in base64_str:
|
||||||
|
# 如果包含 data:image 前缀,提取 base64 部分
|
||||||
|
base64_str = base64_str.split(",")[-1]
|
||||||
|
|
||||||
|
# 确保 base64 字符串只包含有效的 base64 字符
|
||||||
|
# 移除所有空白字符(包括换行符)
|
||||||
|
base64_str = ''.join(base64_str.split())
|
||||||
|
|
||||||
|
# 解码 base64 图片
|
||||||
|
try:
|
||||||
|
# 确保字符串是有效的 base64 格式
|
||||||
|
if not base64_str:
|
||||||
|
raise ValueError("Base64 字符串为空")
|
||||||
|
image_data = base64.b64decode(base64_str, validate=True)
|
||||||
|
except binascii.Error as decode_error:
|
||||||
|
raise ValueError(f"Base64 解码失败: 无效的 base64 格式")
|
||||||
|
except Exception as decode_error:
|
||||||
|
error_msg = str(decode_error)
|
||||||
|
try:
|
||||||
|
error_msg.encode('utf-8')
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
error_msg = repr(decode_error)
|
||||||
|
raise ValueError(f"Base64 解码失败: {error_msg}")
|
||||||
|
|
||||||
|
image = Image.open(BytesIO(image_data))
|
||||||
|
|
||||||
|
# 验证图片大小
|
||||||
|
max_size = (4096, 4096)
|
||||||
|
if image.size[0] > max_size[0] or image.size[1] > max_size[1]:
|
||||||
|
image.thumbnail(max_size, Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
# 重新编码
|
||||||
|
image_base64_encoded = encode_image_to_base64(image)
|
||||||
|
|
||||||
|
# 构建用户消息
|
||||||
|
user_message = request.question or "请识别这张图片中的植物是否存在病虫害问题,如果存在,请详细说明病虫害类型、症状、严重程度和防治建议。"
|
||||||
|
|
||||||
|
# 调用 OpenRouter API
|
||||||
|
result = await call_openrouter_api(image_base64_encoded, user_message)
|
||||||
|
|
||||||
|
return JSONResponse(content={
|
||||||
|
"success": True,
|
||||||
|
"result": result,
|
||||||
|
"image_info": {
|
||||||
|
"format": image.format,
|
||||||
|
"size": image.size,
|
||||||
|
"mode": image.mode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except UnicodeEncodeError as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"处理图片时出错: 编码错误 - {repr(e)}")
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
# 确保错误信息可以正确编码
|
||||||
|
try:
|
||||||
|
error_msg.encode('utf-8')
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
error_msg = repr(e)
|
||||||
|
raise HTTPException(status_code=500, detail=f"处理图片时出错: {error_msg}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
7
env.example
Normal file
7
env.example
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# OpenRouter API 密钥
|
||||||
|
# 从 https://openrouter.ai/keys 获取
|
||||||
|
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
||||||
|
|
||||||
|
# OpenRouter 模型名称(可选,默认为 qwen/qwen3-vl-8b-instruct)
|
||||||
|
# OPENROUTER_MODEL=qwen/qwen3-vl-8b-instruct
|
||||||
|
|
||||||
14
pyproject.toml
Normal file
14
pyproject.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[project]
|
||||||
|
name = "hm-qwen3-vl"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "基于 OpenRouter Qwen3 VL 8B 的病虫害识别 FastAPI 服务"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi>=0.104.0",
|
||||||
|
"uvicorn[standard]>=0.24.0",
|
||||||
|
"httpx>=0.25.0",
|
||||||
|
"python-multipart>=0.0.6",
|
||||||
|
"pillow>=10.0.0",
|
||||||
|
"python-dotenv>=1.0.0",
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user