Files
DDUp/src/vitals/vision/analyzer.py
liweiliang0905@gmail.com eb88d2638f feat: API Keys 支持数据库存储和 Web 界面管理
- database.py: 新增 get_api_key/set_api_key/delete_api_key/get_all_api_keys 函数
- qwen.py/deepseek.py/analyzer.py: 改用 db.get_api_key() 读取配置
- app.py: 新增管理员 API 接口 (/api/admin/api-keys)
- settings 页面: 管理员可见的 API 密钥配置区域

API Key 优先级: 数据库 > 环境变量

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 22:33:50 +08:00

155 lines
4.6 KiB
Python

"""食物图片识别分析器"""
import base64
from pathlib import Path
from typing import Optional
from ..core.calories import estimate_meal_calories
class FoodAnalyzer:
"""食物分析器基类"""
def analyze(self, image_path: Path) -> dict:
"""
分析食物图片
返回:
{
"description": "米饭、红烧排骨、西兰花",
"total_calories": 680,
"total_protein": 25.0,
"total_carbs": 85.0,
"total_fat": 20.0,
"items": [...]
}
"""
raise NotImplementedError
class ClaudeFoodAnalyzer(FoodAnalyzer):
"""使用 Claude Vision API 的食物分析器"""
def __init__(self, api_key: Optional[str] = None):
from ..core import database as db
self.api_key = api_key or db.get_api_key("anthropic")
def analyze(self, image_path: Path) -> dict:
"""使用 Claude Vision 分析食物图片"""
if not self.api_key:
raise ValueError("需要设置 ANTHROPIC_API_KEY 环境变量")
from anthropic import Anthropic
client = Anthropic(api_key=self.api_key)
# 读取并编码图片
image_data = self._encode_image(image_path)
media_type = self._get_media_type(image_path)
# 调用 Claude Vision API
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[
{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": media_type,
"data": image_data,
},
},
{
"type": "text",
"text": """请分析这张食物图片,识别出所有食物。
要求:
1. 列出所有可识别的食物,用中文名称
2. 估计每种食物的大致份量
3. 按照 "食物名称" 的格式返回,多个食物用加号连接
例如返回格式:米饭+红烧排骨+西兰花
只返回食物列表,不需要其他解释。"""
},
],
}
],
)
# 解析响应
description = message.content[0].text.strip()
# 使用卡路里计算模块估算营养成分
result = estimate_meal_calories(description)
result["description"] = description
return result
def _encode_image(self, image_path: Path) -> str:
"""将图片编码为 base64"""
with open(image_path, "rb") as f:
return base64.standard_b64encode(f.read()).decode("utf-8")
def _get_media_type(self, image_path: Path) -> str:
"""获取图片的 MIME 类型"""
suffix = image_path.suffix.lower()
media_types = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
}
return media_types.get(suffix, "image/jpeg")
class LocalFoodAnalyzer(FoodAnalyzer):
"""本地食物分析器(简单实现,基于文件名)"""
def analyze(self, image_path: Path) -> dict:
"""
简单的本地分析器
实际使用时可以替换为本地 AI 模型
"""
# 目前仅返回空结果,提示用户手动输入
return {
"description": "",
"total_calories": 0,
"total_protein": 0,
"total_carbs": 0,
"total_fat": 0,
"items": [],
"note": "本地分析器暂不支持图片识别,请手动输入食物描述",
}
def get_analyzer(
provider: str = "qwen",
api_key: Optional[str] = None,
use_claude: bool = False, # 保留向后兼容
) -> FoodAnalyzer:
"""
获取食物分析器
provider: "qwen" | "deepseek" | "claude" | "local"
"""
# 向后兼容: 如果使用旧参数 use_claude=True
if use_claude:
provider = "claude"
if provider == "qwen":
from .providers.qwen import get_qwen_analyzer
return get_qwen_analyzer(api_key)
elif provider == "deepseek":
from .providers.deepseek import get_deepseek_analyzer
return get_deepseek_analyzer(api_key)
elif provider == "claude":
return ClaudeFoodAnalyzer(api_key)
else:
return LocalFoodAnalyzer()