Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, UploadFile, File, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from PIL import Image | |
| import io | |
| import base64 | |
| from transformers import pipeline | |
| import requests | |
| import json | |
| from typing import Dict, Any | |
| from pydantic import BaseModel | |
| import uvicorn | |
| app = FastAPI(title="Health Assistant AI - Food Recognition API") | |
| # CORS設定 | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # 生產環境請設定具體的域名 | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # 初始化Hugging Face模型 | |
| try: | |
| # 使用nateraw/food專門的食物分類模型 | |
| food_classifier = pipeline( | |
| "image-classification", | |
| model="nateraw/food", | |
| device=-1 # 使用CPU,如果有GPU可以設為0 | |
| ) | |
| print("nateraw/food 食物辨識模型載入成功") | |
| except Exception as e: | |
| print(f"模型載入失敗: {e}") | |
| food_classifier = None | |
| # 食物營養資料庫(擴展版,涵蓋nateraw/food模型常見的食物類型) | |
| NUTRITION_DATABASE = { | |
| # 水果類 | |
| "apple": {"name": "蘋果", "calories_per_100g": 52, "protein": 0.3, "carbs": 14, "fat": 0.2, "fiber": 2.4, "sugar": 10.4, "vitamin_c": 4.6}, | |
| "banana": {"name": "香蕉", "calories_per_100g": 89, "protein": 1.1, "carbs": 23, "fat": 0.3, "fiber": 2.6, "sugar": 12.2, "potassium": 358}, | |
| "orange": {"name": "橘子", "calories_per_100g": 47, "protein": 0.9, "carbs": 12, "fat": 0.1, "fiber": 2.4, "sugar": 9.4, "vitamin_c": 53.2}, | |
| "strawberry": {"name": "草莓", "calories_per_100g": 32, "protein": 0.7, "carbs": 7.7, "fat": 0.3, "fiber": 2, "sugar": 4.9, "vitamin_c": 58.8}, | |
| "grape": {"name": "葡萄", "calories_per_100g": 62, "protein": 0.6, "carbs": 16.8, "fat": 0.2, "fiber": 0.9, "sugar": 16.1}, | |
| # 主食類 | |
| "bread": {"name": "麵包", "calories_per_100g": 265, "protein": 9, "carbs": 49, "fat": 3.2, "fiber": 2.7, "sodium": 491}, | |
| "rice": {"name": "米飯", "calories_per_100g": 130, "protein": 2.7, "carbs": 28, "fat": 0.3, "fiber": 0.4}, | |
| "pasta": {"name": "義大利麵", "calories_per_100g": 131, "protein": 5, "carbs": 25, "fat": 1.1, "fiber": 1.8}, | |
| "noodles": {"name": "麵條", "calories_per_100g": 138, "protein": 4.5, "carbs": 25, "fat": 2.2, "fiber": 1.2}, | |
| "pizza": {"name": "披薩", "calories_per_100g": 266, "protein": 11, "carbs": 33, "fat": 10, "sodium": 598}, | |
| # 肉類 | |
| "chicken": {"name": "雞肉", "calories_per_100g": 165, "protein": 31, "carbs": 0, "fat": 3.6, "iron": 0.9}, | |
| "beef": {"name": "牛肉", "calories_per_100g": 250, "protein": 26, "carbs": 0, "fat": 15, "iron": 2.6, "zinc": 4.8}, | |
| "pork": {"name": "豬肉", "calories_per_100g": 242, "protein": 27, "carbs": 0, "fat": 14, "thiamine": 0.7}, | |
| "fish": {"name": "魚肉", "calories_per_100g": 206, "protein": 22, "carbs": 0, "fat": 12, "omega_3": "豐富"}, | |
| # 蔬菜類 | |
| "broccoli": {"name": "花椰菜", "calories_per_100g": 34, "protein": 2.8, "carbs": 7, "fat": 0.4, "fiber": 2.6, "vitamin_c": 89.2}, | |
| "carrot": {"name": "胡蘿蔔", "calories_per_100g": 41, "protein": 0.9, "carbs": 10, "fat": 0.2, "fiber": 2.8, "vitamin_a": 835}, | |
| "tomato": {"name": "番茄", "calories_per_100g": 18, "protein": 0.9, "carbs": 3.9, "fat": 0.2, "fiber": 1.2, "vitamin_c": 13.7}, | |
| "lettuce": {"name": "萵苣", "calories_per_100g": 15, "protein": 1.4, "carbs": 2.9, "fat": 0.2, "fiber": 1.3, "folate": 38}, | |
| # 飲品類 | |
| "coffee": {"name": "咖啡", "calories_per_100g": 2, "protein": 0.3, "carbs": 0, "fat": 0, "caffeine": 95}, | |
| "tea": {"name": "茶", "calories_per_100g": 1, "protein": 0, "carbs": 0.3, "fat": 0, "antioxidants": "豐富"}, | |
| "milk": {"name": "牛奶", "calories_per_100g": 42, "protein": 3.4, "carbs": 5, "fat": 1, "calcium": 113}, | |
| "juice": {"name": "果汁", "calories_per_100g": 45, "protein": 0.7, "carbs": 11, "fat": 0.2, "vitamin_c": "因果汁種類而異"}, | |
| # 甜點類 | |
| "cake": {"name": "蛋糕", "calories_per_100g": 257, "protein": 4, "carbs": 46, "fat": 6, "sugar": 35}, | |
| "cookie": {"name": "餅乾", "calories_per_100g": 502, "protein": 5.9, "carbs": 64, "fat": 25, "sugar": 39}, | |
| "ice_cream": {"name": "冰淇淋", "calories_per_100g": 207, "protein": 3.5, "carbs": 24, "fat": 11, "sugar": 21}, | |
| "chocolate": {"name": "巧克力", "calories_per_100g": 546, "protein": 4.9, "carbs": 61, "fat": 31, "sugar": 48}, | |
| # 其他常見食物 | |
| "egg": {"name": "雞蛋", "calories_per_100g": 155, "protein": 13, "carbs": 1.1, "fat": 11, "choline": 294}, | |
| "cheese": {"name": "起司", "calories_per_100g": 113, "protein": 7, "carbs": 1, "fat": 9, "calcium": 200}, | |
| "yogurt": {"name": "優格", "calories_per_100g": 59, "protein": 10, "carbs": 3.6, "fat": 0.4, "probiotics": "豐富"}, | |
| "nuts": {"name": "堅果", "calories_per_100g": 607, "protein": 15, "carbs": 7, "fat": 54, "vitamin_e": 26}, | |
| "salad": {"name": "沙拉", "calories_per_100g": 20, "protein": 1.5, "carbs": 4, "fat": 0.2, "fiber": 2, "vitamins": "多種維生素"} | |
| } | |
| # 回應模型 | |
| class FoodAnalysisResponse(BaseModel): | |
| success: bool | |
| food_name: str | |
| confidence: float | |
| nutrition_info: Dict[str, Any] | |
| ai_suggestions: list | |
| message: str | |
| class HealthResponse(BaseModel): | |
| status: str | |
| message: str | |
| def get_nutrition_info(food_name: str) -> Dict[str, Any]: | |
| """根據食物名稱獲取營養資訊""" | |
| # 將食物名稱轉為小寫並清理 | |
| food_key = food_name.lower().strip() | |
| # 移除常見的修飾詞和格式化字符 | |
| food_key = food_key.replace("_", " ").replace("-", " ") | |
| # 直接匹配 | |
| if food_key in NUTRITION_DATABASE: | |
| return NUTRITION_DATABASE[food_key] | |
| # 模糊匹配 - 檢查是否包含關鍵字 | |
| for key, value in NUTRITION_DATABASE.items(): | |
| if key in food_key or food_key in key: | |
| return value | |
| # 也檢查中文名稱 | |
| if value["name"] in food_name: | |
| return value | |
| # 更智能的匹配 - 處理複合詞 | |
| food_words = food_key.split() | |
| for word in food_words: | |
| for key, value in NUTRITION_DATABASE.items(): | |
| if word == key or word in key: | |
| return value | |
| # 特殊情況處理 | |
| special_mappings = { | |
| "french fries": "potato", | |
| "hamburger": "beef", | |
| "sandwich": "bread", | |
| "soda": "juice", | |
| "water": {"name": "水", "calories_per_100g": 0, "protein": 0, "carbs": 0, "fat": 0}, | |
| "soup": {"name": "湯", "calories_per_100g": 50, "protein": 2, "carbs": 8, "fat": 1, "sodium": 400} | |
| } | |
| for special_key, mapping in special_mappings.items(): | |
| if special_key in food_key: | |
| if isinstance(mapping, str): | |
| return NUTRITION_DATABASE.get(mapping, {"name": food_name, "message": "營養資料不完整"}) | |
| else: | |
| return mapping | |
| # 如果沒有找到,返回預設值 | |
| return { | |
| "name": food_name, | |
| "calories_per_100g": "未知", | |
| "protein": "未知", | |
| "carbs": "未知", | |
| "fat": "未知", | |
| "message": f"抱歉,暫時沒有「{food_name}」的詳細營養資料,建議查詢專業營養資料庫" | |
| } | |
| def generate_ai_suggestions(food_name: str, nutrition_info: Dict) -> list: | |
| """根據食物和營養資訊生成AI建議""" | |
| suggestions = [] | |
| food_name_lower = food_name.lower() | |
| # 檢查是否有完整的營養資訊 | |
| if isinstance(nutrition_info.get("calories_per_100g"), (int, float)): | |
| calories = nutrition_info["calories_per_100g"] | |
| # 熱量相關建議 | |
| if calories > 400: | |
| suggestions.append("⚠️ 這是高熱量食物,建議控制份量,搭配運動") | |
| elif calories > 200: | |
| suggestions.append("🍽️ 中等熱量食物,適量食用,建議搭配蔬菜") | |
| elif calories < 50: | |
| suggestions.append("✅ 低熱量食物,適合減重期間食用") | |
| # 營養素相關建議 | |
| protein = nutrition_info.get("protein", 0) | |
| if isinstance(protein, (int, float)) and protein > 20: | |
| suggestions.append("💪 高蛋白食物,有助於肌肉發展和修復") | |
| fiber = nutrition_info.get("fiber", 0) | |
| if isinstance(fiber, (int, float)) and fiber > 3: | |
| suggestions.append("🌿 富含纖維,有助於消化健康和增加飽足感") | |
| sugar = nutrition_info.get("sugar", 0) | |
| if isinstance(sugar, (int, float)) and sugar > 20: | |
| suggestions.append("🍯 含糖量較高,建議適量食用,避免血糖快速上升") | |
| # 特殊營養素 | |
| if nutrition_info.get("vitamin_c", 0) > 30: | |
| suggestions.append("🍊 富含維生素C,有助於增強免疫力和抗氧化") | |
| if nutrition_info.get("calcium", 0) > 100: | |
| suggestions.append("🦴 富含鈣質,有助於骨骼和牙齒健康") | |
| if nutrition_info.get("omega_3"): | |
| suggestions.append("🐟 含有Omega-3脂肪酸,對心血管健康有益") | |
| # 根據食物類型給出特定建議 | |
| if any(fruit in food_name_lower for fruit in ["apple", "banana", "orange", "strawberry", "grape"]): | |
| suggestions.append("🍎 建議在餐前或運動前食用,提供天然糖分和維生素") | |
| elif any(meat in food_name_lower for meat in ["chicken", "beef", "pork", "fish"]): | |
| suggestions.append("🥩 建議搭配蔬菜食用,選擇健康的烹調方式(烤、蒸、煮)") | |
| elif any(sweet in food_name_lower for sweet in ["cake", "cookie", "ice_cream", "chocolate"]): | |
| suggestions.append("🍰 甜點建議偶爾享用,可在運動後適量食用") | |
| suggestions.append("💡 可以考慮與朋友分享,減少單次攝取量") | |
| elif any(drink in food_name_lower for drink in ["coffee", "tea"]): | |
| suggestions.append("☕ 建議控制咖啡因攝取量,避免影響睡眠") | |
| elif "salad" in food_name_lower: | |
| suggestions.append("🥗 很棒的選擇!可以添加堅果或橄欖油增加健康脂肪") | |
| # 通用健康建議 | |
| if not suggestions: | |
| suggestions.extend([ | |
| "🍽️ 建議均衡飲食,搭配多樣化的食物", | |
| "💧 記得多喝水,保持身體水分充足", | |
| "🏃♂️ 搭配適量運動,維持健康生活型態" | |
| ]) | |
| else: | |
| # 添加一些通用的健康提醒 | |
| suggestions.append("💧 記得多喝水,幫助營養吸收") | |
| if len(suggestions) < 4: | |
| suggestions.append("⚖️ 注意食物份量,適量攝取是健康飲食的關鍵") | |
| return suggestions[:5] # 限制建議數量,避免過多 | |
| async def root(): | |
| """API根路径""" | |
| return HealthResponse( | |
| status="success", | |
| message="Health Assistant AI - Food Recognition API is running!" | |
| ) | |
| async def health_check(): | |
| """健康檢查端點""" | |
| model_status = "正常" if food_classifier else "模型載入失敗" | |
| return HealthResponse( | |
| status="success", | |
| message=f"API運行正常,模型狀態: {model_status}" | |
| ) | |
| async def analyze_food(file: UploadFile = File(...)): | |
| """分析上傳的食物圖片""" | |
| try: | |
| # 檢查模型是否載入成功 | |
| if not food_classifier: | |
| raise HTTPException(status_code=500, detail="AI模型尚未載入,請稍後再試") | |
| # 檢查文件類型 | |
| if not file.content_type.startswith("image/"): | |
| raise HTTPException(status_code=400, detail="請上傳圖片文件") | |
| # 讀取圖片 | |
| image_data = await file.read() | |
| image = Image.open(io.BytesIO(image_data)) | |
| # 確保圖片是RGB格式 | |
| if image.mode != "RGB": | |
| image = image.convert("RGB") | |
| # 使用AI模型進行食物辨識 | |
| results = food_classifier(image) | |
| # 獲取最高信心度的結果 | |
| top_result = results[0] | |
| food_name = top_result["label"] | |
| confidence = top_result["score"] | |
| # 獲取營養資訊 | |
| nutrition_info = get_nutrition_info(food_name) | |
| # 生成AI建議 | |
| ai_suggestions = generate_ai_suggestions(food_name, nutrition_info) | |
| return FoodAnalysisResponse( | |
| success=True, | |
| food_name=food_name, | |
| confidence=round(confidence * 100, 2), | |
| nutrition_info=nutrition_info, | |
| ai_suggestions=ai_suggestions, | |
| message="食物分析完成" | |
| ) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"分析失敗: {str(e)}") | |
| async def analyze_food_base64(image_data: dict): | |
| """分析base64編碼的食物圖片""" | |
| try: | |
| # 檢查模型是否載入成功 | |
| if not food_classifier: | |
| raise HTTPException(status_code=500, detail="AI模型尚未載入,請稍後再試") | |
| # 解碼base64圖片 | |
| base64_string = image_data.get("image", "") | |
| if not base64_string: | |
| raise HTTPException(status_code=400, detail="缺少圖片資料") | |
| # 移除base64前綴(如果有的話) | |
| if "," in base64_string: | |
| base64_string = base64_string.split(",")[1] | |
| # 解碼圖片 | |
| image_bytes = base64.b64decode(base64_string) | |
| image = Image.open(io.BytesIO(image_bytes)) | |
| # 確保圖片是RGB格式 | |
| if image.mode != "RGB": | |
| image = image.convert("RGB") | |
| # 使用AI模型進行食物辨識 | |
| results = food_classifier(image) | |
| # 獲取最高信心度的結果 | |
| top_result = results[0] | |
| food_name = top_result["label"] | |
| confidence = top_result["score"] | |
| # 獲取營養資訊 | |
| nutrition_info = get_nutrition_info(food_name) | |
| # 生成AI建議 | |
| ai_suggestions = generate_ai_suggestions(food_name, nutrition_info) | |
| return FoodAnalysisResponse( | |
| success=True, | |
| food_name=food_name, | |
| confidence=round(confidence * 100, 2), | |
| nutrition_info=nutrition_info, | |
| ai_suggestions=ai_suggestions, | |
| message="食物分析完成" | |
| ) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"分析失敗: {str(e)}") | |
| if __name__ == "__main__": | |
| uvicorn.run(app, host="0.0.0.0", port=8000) | |