"""食物图片识别分析器""" 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()