yuting111222 commited on
Commit
fdc2693
·
1 Parent(s): 7c98348

add backend source

Browse files
.env ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 資料庫設定
2
+ DATABASE_URL=sqlite:///./data/health_assistant.db
3
+
4
+ # Redis 設定
5
+ REDIS_HOST=localhost
6
+ REDIS_PORT=6379
7
+ REDIS_DB=0
8
+
9
+ # API 設定
10
+ API_HOST=localhost
11
+ API_PORT=8000
12
+
13
+ USDA_API_KEY="0bZCszzPSbc5r6RXfm0aHKtWGX2V0SX1hLiMmXwi"
__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
__pycache__/__init__.cpython-313.pyc ADDED
Binary file (167 Bytes). View file
 
__pycache__/database.cpython-313.pyc ADDED
Binary file (1.3 kB). View file
 
__pycache__/init_db.cpython-313.pyc ADDED
Binary file (2.72 kB). View file
 
__pycache__/main.cpython-313.pyc ADDED
Binary file (1.17 kB). View file
 
app.cpython-313.pyc ADDED
Binary file (2.26 kB). View file
 
app.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ import uvicorn
4
+ from datetime import datetime
5
+ from PIL import Image
6
+ import io
7
+ import random
8
+
9
+ app = FastAPI()
10
+
11
+ # 允許跨域請求
12
+ app.add_middleware(
13
+ CORSMiddleware,
14
+ allow_origins=["*"],
15
+ allow_credentials=True,
16
+ allow_methods=["*"],
17
+ allow_headers=["*"],
18
+ )
19
+
20
+ # 模擬食物列表
21
+ FOOD_ITEMS = [
22
+ "牛肉麵",
23
+ "滷肉飯",
24
+ "炒飯",
25
+ "水餃",
26
+ "炸雞",
27
+ "三明治",
28
+ "沙拉",
29
+ "義大利麵",
30
+ "披薩",
31
+ "漢堡"
32
+ ]
33
+
34
+ @app.post("/api/ai/analyze-food")
35
+ async def analyze_food(file: UploadFile = File(...)):
36
+ # 讀取圖片(僅作為示範,不進行實際分析)
37
+ contents = await file.read()
38
+ image = Image.open(io.BytesIO(contents))
39
+
40
+ # 隨機選擇一個食物和信心度
41
+ food = random.choice(FOOD_ITEMS)
42
+ confidence = random.uniform(85.0, 99.9)
43
+
44
+ # 模擬營養資訊
45
+ nutrition = {
46
+ "calories": random.randint(200, 800),
47
+ "protein": random.randint(10, 30),
48
+ "carbs": random.randint(20, 60),
49
+ "fat": random.randint(5, 25)
50
+ }
51
+
52
+ # 返回結果
53
+ return {
54
+ "success": True,
55
+ "analysis_time": datetime.now().isoformat(),
56
+ "top_prediction": {
57
+ "label": food,
58
+ "confidence": confidence,
59
+ "nutrition": nutrition,
60
+ "description": f"這是一道美味的{food},營養豐富且美味可口。"
61
+ }
62
+ }
63
+
64
+ if __name__ == "__main__":
65
+ uvicorn.run(app, host="127.0.0.1", port=8000)
app/main.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from app.routers import ai_router, meal_router
4
+ from app.database import engine, Base
5
+
6
+ # 創建資料庫表
7
+ Base.metadata.create_all(bind=engine)
8
+
9
+ app = FastAPI(title="Health Assistant API")
10
+
11
+ # 配置 CORS
12
+ app.add_middleware(
13
+ CORSMiddleware,
14
+ allow_origins=["http://localhost:5173"], # React 開發伺服器的位址
15
+ allow_credentials=True,
16
+ allow_methods=["*"],
17
+ allow_headers=["*"],
18
+ )
19
+
20
+ # 註冊路由
21
+ app.include_router(ai_router.router)
22
+ app.include_router(meal_router.router)
23
+
24
+ @app.get("/")
25
+ async def root():
26
+ return {"message": "Health Assistant API is running"}
backend/requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ transformers
4
+ torch
5
+ pillow
6
+ opencv-python
7
+ python-multipart
8
+ requests
backend/setup.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ def setup_project():
6
+ """設置專案目錄結構和必要檔案"""
7
+ # 獲取專案根目錄
8
+ project_root = Path(__file__).parent
9
+
10
+ # 創建必要的目錄
11
+ directories = [
12
+ 'data', # 資料庫目錄
13
+ 'logs', # 日誌目錄
14
+ 'uploads' # 上傳檔案目錄
15
+ ]
16
+
17
+ for directory in directories:
18
+ dir_path = project_root / directory
19
+ dir_path.mkdir(exist_ok=True)
20
+ print(f"Created directory: {dir_path}")
21
+
22
+ # 創建 .env 檔案(如果不存在)
23
+ env_file = project_root / '.env'
24
+ if not env_file.exists():
25
+ with open(env_file, 'w', encoding='utf-8') as f:
26
+ f.write("""# 資料庫設定
27
+ DATABASE_URL=sqlite:///./data/health_assistant.db
28
+
29
+ # Redis 設定
30
+ REDIS_HOST=localhost
31
+ REDIS_PORT=6379
32
+ REDIS_DB=0
33
+
34
+ # API 設定
35
+ API_HOST=localhost
36
+ API_PORT=8000
37
+ """)
38
+ print(f"Created .env file: {env_file}")
39
+
40
+ print("\nProject setup completed successfully!")
41
+ print("\nNext steps:")
42
+ print("1. Install required packages: pip install -r requirements.txt")
43
+ print("2. Start the Redis server")
44
+ print("3. Run the application: uvicorn app.main:app --reload")
45
+
46
+ if __name__ == "__main__":
47
+ setup_project()
conftest.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ # Add the backend directory to the Python path
6
+ sys.path.insert(0, str(Path(__file__).parent / "backend"))
7
+ sys.path.insert(0, str(Path(__file__).parent))
database.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.ext.declarative import declarative_base
3
+ from sqlalchemy.orm import sessionmaker
4
+ import os
5
+
6
+ # 確保資料庫目錄存在
7
+ DB_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")
8
+ os.makedirs(DB_DIR, exist_ok=True)
9
+
10
+ # 資料庫 URL
11
+ SQLALCHEMY_DATABASE_URL = f"sqlite:///{os.path.join(DB_DIR, 'health_assistant.db')}"
12
+
13
+ # 創建資料庫引擎
14
+ engine = create_engine(
15
+ SQLALCHEMY_DATABASE_URL,
16
+ connect_args={"check_same_thread": False} # SQLite 特定配置
17
+ )
18
+
19
+ # 創建 SessionLocal 類
20
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
21
+
22
+ # 創建 Base 類
23
+ Base = declarative_base()
24
+
25
+ # 獲取資料庫會話的依賴項
26
+ def get_db():
27
+ db = SessionLocal()
28
+ try:
29
+ yield db
30
+ finally:
31
+ db.close()
food_analyzer.py ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from PIL import Image
4
+ import io
5
+ import base64
6
+ from transformers import pipeline
7
+ import requests
8
+ import json
9
+ from typing import Dict, Any
10
+ from pydantic import BaseModel
11
+ import uvicorn
12
+
13
+ app = FastAPI(title="Health Assistant AI - Food Recognition API")
14
+
15
+ # CORS設定
16
+ app.add_middleware(
17
+ CORSMiddleware,
18
+ allow_origins=["*"], # 生產環境請設定具體的域名
19
+ allow_credentials=True,
20
+ allow_methods=["*"],
21
+ allow_headers=["*"],
22
+ )
23
+
24
+ # 初始化Hugging Face模型
25
+ try:
26
+ # 使用nateraw/food專門的食物分類模型
27
+ food_classifier = pipeline(
28
+ "image-classification",
29
+ model="nateraw/food",
30
+ device=-1 # 使用CPU,如果有GPU可以設為0
31
+ )
32
+ print("nateraw/food 食物辨識模型載入成功")
33
+ except Exception as e:
34
+ print(f"模型載入失敗: {e}")
35
+ food_classifier = None
36
+
37
+ # 食物營養資料庫(擴展版,涵蓋nateraw/food模型常見的食物類型)
38
+ NUTRITION_DATABASE = {
39
+ # 水果類
40
+ "apple": {"name": "蘋果", "calories_per_100g": 52, "protein": 0.3, "carbs": 14, "fat": 0.2, "fiber": 2.4, "sugar": 10.4, "vitamin_c": 4.6},
41
+ "banana": {"name": "香蕉", "calories_per_100g": 89, "protein": 1.1, "carbs": 23, "fat": 0.3, "fiber": 2.6, "sugar": 12.2, "potassium": 358},
42
+ "orange": {"name": "橘子", "calories_per_100g": 47, "protein": 0.9, "carbs": 12, "fat": 0.1, "fiber": 2.4, "sugar": 9.4, "vitamin_c": 53.2},
43
+ "strawberry": {"name": "草莓", "calories_per_100g": 32, "protein": 0.7, "carbs": 7.7, "fat": 0.3, "fiber": 2, "sugar": 4.9, "vitamin_c": 58.8},
44
+ "grape": {"name": "葡萄", "calories_per_100g": 62, "protein": 0.6, "carbs": 16.8, "fat": 0.2, "fiber": 0.9, "sugar": 16.1},
45
+
46
+ # 主食類
47
+ "bread": {"name": "麵包", "calories_per_100g": 265, "protein": 9, "carbs": 49, "fat": 3.2, "fiber": 2.7, "sodium": 491},
48
+ "rice": {"name": "米飯", "calories_per_100g": 130, "protein": 2.7, "carbs": 28, "fat": 0.3, "fiber": 0.4},
49
+ "pasta": {"name": "義大利麵", "calories_per_100g": 131, "protein": 5, "carbs": 25, "fat": 1.1, "fiber": 1.8},
50
+ "noodles": {"name": "麵條", "calories_per_100g": 138, "protein": 4.5, "carbs": 25, "fat": 2.2, "fiber": 1.2},
51
+ "pizza": {"name": "披薩", "calories_per_100g": 266, "protein": 11, "carbs": 33, "fat": 10, "sodium": 598},
52
+
53
+ # 肉類
54
+ "chicken": {"name": "雞肉", "calories_per_100g": 165, "protein": 31, "carbs": 0, "fat": 3.6, "iron": 0.9},
55
+ "beef": {"name": "牛肉", "calories_per_100g": 250, "protein": 26, "carbs": 0, "fat": 15, "iron": 2.6, "zinc": 4.8},
56
+ "pork": {"name": "豬肉", "calories_per_100g": 242, "protein": 27, "carbs": 0, "fat": 14, "thiamine": 0.7},
57
+ "fish": {"name": "魚肉", "calories_per_100g": 206, "protein": 22, "carbs": 0, "fat": 12, "omega_3": "豐富"},
58
+
59
+ # 蔬菜類
60
+ "broccoli": {"name": "花椰菜", "calories_per_100g": 34, "protein": 2.8, "carbs": 7, "fat": 0.4, "fiber": 2.6, "vitamin_c": 89.2},
61
+ "carrot": {"name": "胡蘿蔔", "calories_per_100g": 41, "protein": 0.9, "carbs": 10, "fat": 0.2, "fiber": 2.8, "vitamin_a": 835},
62
+ "tomato": {"name": "番茄", "calories_per_100g": 18, "protein": 0.9, "carbs": 3.9, "fat": 0.2, "fiber": 1.2, "vitamin_c": 13.7},
63
+ "lettuce": {"name": "萵苣", "calories_per_100g": 15, "protein": 1.4, "carbs": 2.9, "fat": 0.2, "fiber": 1.3, "folate": 38},
64
+
65
+ # 飲品類
66
+ "coffee": {"name": "咖啡", "calories_per_100g": 2, "protein": 0.3, "carbs": 0, "fat": 0, "caffeine": 95},
67
+ "tea": {"name": "茶", "calories_per_100g": 1, "protein": 0, "carbs": 0.3, "fat": 0, "antioxidants": "豐富"},
68
+ "milk": {"name": "牛奶", "calories_per_100g": 42, "protein": 3.4, "carbs": 5, "fat": 1, "calcium": 113},
69
+ "juice": {"name": "果汁", "calories_per_100g": 45, "protein": 0.7, "carbs": 11, "fat": 0.2, "vitamin_c": "因果汁種類而異"},
70
+
71
+ # 甜點類
72
+ "cake": {"name": "蛋糕", "calories_per_100g": 257, "protein": 4, "carbs": 46, "fat": 6, "sugar": 35},
73
+ "cookie": {"name": "餅乾", "calories_per_100g": 502, "protein": 5.9, "carbs": 64, "fat": 25, "sugar": 39},
74
+ "ice_cream": {"name": "冰淇淋", "calories_per_100g": 207, "protein": 3.5, "carbs": 24, "fat": 11, "sugar": 21},
75
+ "chocolate": {"name": "巧克力", "calories_per_100g": 546, "protein": 4.9, "carbs": 61, "fat": 31, "sugar": 48},
76
+
77
+ # 其他常見食物
78
+ "egg": {"name": "雞蛋", "calories_per_100g": 155, "protein": 13, "carbs": 1.1, "fat": 11, "choline": 294},
79
+ "cheese": {"name": "起司", "calories_per_100g": 113, "protein": 7, "carbs": 1, "fat": 9, "calcium": 200},
80
+ "yogurt": {"name": "優格", "calories_per_100g": 59, "protein": 10, "carbs": 3.6, "fat": 0.4, "probiotics": "豐富"},
81
+ "nuts": {"name": "堅果", "calories_per_100g": 607, "protein": 15, "carbs": 7, "fat": 54, "vitamin_e": 26},
82
+ "salad": {"name": "沙拉", "calories_per_100g": 20, "protein": 1.5, "carbs": 4, "fat": 0.2, "fiber": 2, "vitamins": "多種維生素"}
83
+ }
84
+
85
+ # 回應模型
86
+ class FoodAnalysisResponse(BaseModel):
87
+ success: bool
88
+ food_name: str
89
+ confidence: float
90
+ nutrition_info: Dict[str, Any]
91
+ ai_suggestions: list
92
+ message: str
93
+
94
+ class HealthResponse(BaseModel):
95
+ status: str
96
+ message: str
97
+
98
+ def get_nutrition_info(food_name: str) -> Dict[str, Any]:
99
+ """根據食物名稱獲取營養資訊"""
100
+ # 將食物名稱轉為小寫並清理
101
+ food_key = food_name.lower().strip()
102
+
103
+ # 移除常見的修飾詞和格式化字符
104
+ food_key = food_key.replace("_", " ").replace("-", " ")
105
+
106
+ # 直接匹配
107
+ if food_key in NUTRITION_DATABASE:
108
+ return NUTRITION_DATABASE[food_key]
109
+
110
+ # 模糊匹配 - 檢查是否包含關鍵字
111
+ for key, value in NUTRITION_DATABASE.items():
112
+ if key in food_key or food_key in key:
113
+ return value
114
+ # 也檢查中文名稱
115
+ if value["name"] in food_name:
116
+ return value
117
+
118
+ # 更智能的匹配 - 處理複合詞
119
+ food_words = food_key.split()
120
+ for word in food_words:
121
+ for key, value in NUTRITION_DATABASE.items():
122
+ if word == key or word in key:
123
+ return value
124
+
125
+ # 特殊情況處理
126
+ special_mappings = {
127
+ "french fries": "potato",
128
+ "hamburger": "beef",
129
+ "sandwich": "bread",
130
+ "soda": "juice",
131
+ "water": {"name": "水", "calories_per_100g": 0, "protein": 0, "carbs": 0, "fat": 0},
132
+ "soup": {"name": "湯", "calories_per_100g": 50, "protein": 2, "carbs": 8, "fat": 1, "sodium": 400}
133
+ }
134
+
135
+ for special_key, mapping in special_mappings.items():
136
+ if special_key in food_key:
137
+ if isinstance(mapping, str):
138
+ return NUTRITION_DATABASE.get(mapping, {"name": food_name, "message": "營養資料不完整"})
139
+ else:
140
+ return mapping
141
+
142
+ # 如果沒有找到,返回預設值
143
+ return {
144
+ "name": food_name,
145
+ "calories_per_100g": "未知",
146
+ "protein": "未知",
147
+ "carbs": "未知",
148
+ "fat": "未知",
149
+ "message": f"抱歉,暫時沒有「{food_name}」的詳細營養資料,建議查詢專業營養資料庫"
150
+ }
151
+
152
+ def generate_ai_suggestions(food_name: str, nutrition_info: Dict) -> list:
153
+ """根據食物和營養資訊生成AI建議"""
154
+ suggestions = []
155
+ food_name_lower = food_name.lower()
156
+
157
+ # 檢查是否有完整的營養資訊
158
+ if isinstance(nutrition_info.get("calories_per_100g"), (int, float)):
159
+ calories = nutrition_info["calories_per_100g"]
160
+
161
+ # 熱量相關建議
162
+ if calories > 400:
163
+ suggestions.append("⚠️ 這是高熱量食物,建議控制份量,搭配運動")
164
+ elif calories > 200:
165
+ suggestions.append("🍽️ 中等熱量食物,適量食用,建議搭配蔬菜")
166
+ elif calories < 50:
167
+ suggestions.append("✅ 低熱量食物,適合減重期間食用")
168
+
169
+ # 營養素相關建議
170
+ protein = nutrition_info.get("protein", 0)
171
+ if isinstance(protein, (int, float)) and protein > 20:
172
+ suggestions.append("💪 高蛋白食物,有助於肌肉發展和修復")
173
+
174
+ fiber = nutrition_info.get("fiber", 0)
175
+ if isinstance(fiber, (int, float)) and fiber > 3:
176
+ suggestions.append("🌿 富含纖維,有助於消化健康和增加飽足感")
177
+
178
+ sugar = nutrition_info.get("sugar", 0)
179
+ if isinstance(sugar, (int, float)) and sugar > 20:
180
+ suggestions.append("🍯 含糖量較高,建議適量食用,避免血糖快速上升")
181
+
182
+ # 特殊營養素
183
+ if nutrition_info.get("vitamin_c", 0) > 30:
184
+ suggestions.append("🍊 富含維生素C,有助於增強免疫力和抗氧化")
185
+
186
+ if nutrition_info.get("calcium", 0) > 100:
187
+ suggestions.append("🦴 富含鈣質,有助於骨骼和牙齒健康")
188
+
189
+ if nutrition_info.get("omega_3"):
190
+ suggestions.append("🐟 含有Omega-3脂肪酸,對心血管健康有益")
191
+
192
+ # 根據食物類型給出特定建議
193
+ if any(fruit in food_name_lower for fruit in ["apple", "banana", "orange", "strawberry", "grape"]):
194
+ suggestions.append("🍎 建議在餐前或運動前食用,提供天然糖分和維生素")
195
+
196
+ elif any(meat in food_name_lower for meat in ["chicken", "beef", "pork", "fish"]):
197
+ suggestions.append("🥩 建議搭配蔬菜食用,選擇健康的烹調方式(烤、蒸、煮)")
198
+
199
+ elif any(sweet in food_name_lower for sweet in ["cake", "cookie", "ice_cream", "chocolate"]):
200
+ suggestions.append("🍰 甜點建議偶爾享用,可在運動後適量食用")
201
+ suggestions.append("💡 可以考慮與朋友分享,減少單次攝取量")
202
+
203
+ elif any(drink in food_name_lower for drink in ["coffee", "tea"]):
204
+ suggestions.append("☕ 建議控制咖啡因攝取量,避免影響睡眠")
205
+
206
+ elif "salad" in food_name_lower:
207
+ suggestions.append("🥗 很棒的選擇!可以添加堅果或橄欖油增加健康脂肪")
208
+
209
+ # 通用健康建議
210
+ if not suggestions:
211
+ suggestions.extend([
212
+ "🍽️ 建議均衡飲食,搭配多樣化的食物",
213
+ "💧 記得多喝水,保持身體水分充足",
214
+ "🏃‍♂️ 搭配適量運動,維持健康生活型態"
215
+ ])
216
+ else:
217
+ # 添加一些通用的健康提醒
218
+ suggestions.append("💧 記得多喝水,幫助營養吸收")
219
+ if len(suggestions) < 4:
220
+ suggestions.append("⚖️ 注意食物份量,適量攝取是健康飲食的關鍵")
221
+
222
+ return suggestions[:5] # 限制建議數量,避免過多
223
+
224
+ @app.get("/", response_model=HealthResponse)
225
+ async def root():
226
+ """API根路径"""
227
+ return HealthResponse(
228
+ status="success",
229
+ message="Health Assistant AI - Food Recognition API is running!"
230
+ )
231
+
232
+ @app.get("/health", response_model=HealthResponse)
233
+ async def health_check():
234
+ """健康檢查端點"""
235
+ model_status = "正常" if food_classifier else "模型載入失敗"
236
+ return HealthResponse(
237
+ status="success",
238
+ message=f"API運行正常,模型狀態: {model_status}"
239
+ )
240
+
241
+ @app.post("/analyze-food", response_model=FoodAnalysisResponse)
242
+ async def analyze_food(file: UploadFile = File(...)):
243
+ """分析上傳的食物圖片"""
244
+ try:
245
+ # 檢查模型是否載入成功
246
+ if not food_classifier:
247
+ raise HTTPException(status_code=500, detail="AI模型尚未載入,請稍後再試")
248
+
249
+ # 檢查文件類型
250
+ if not file.content_type.startswith("image/"):
251
+ raise HTTPException(status_code=400, detail="請上傳圖片文件")
252
+
253
+ # 讀取圖片
254
+ image_data = await file.read()
255
+ image = Image.open(io.BytesIO(image_data))
256
+
257
+ # 確保圖片是RGB格式
258
+ if image.mode != "RGB":
259
+ image = image.convert("RGB")
260
+
261
+ # 使用AI模型進行食物辨識
262
+ results = food_classifier(image)
263
+
264
+ # 獲取最高信心度的結果
265
+ top_result = results[0]
266
+ food_name = top_result["label"]
267
+ confidence = top_result["score"]
268
+
269
+ # 獲取營養資訊
270
+ nutrition_info = get_nutrition_info(food_name)
271
+
272
+ # 生成AI建議
273
+ ai_suggestions = generate_ai_suggestions(food_name, nutrition_info)
274
+
275
+ return FoodAnalysisResponse(
276
+ success=True,
277
+ food_name=food_name,
278
+ confidence=round(confidence * 100, 2),
279
+ nutrition_info=nutrition_info,
280
+ ai_suggestions=ai_suggestions,
281
+ message="食物分析完成"
282
+ )
283
+
284
+ except Exception as e:
285
+ raise HTTPException(status_code=500, detail=f"分析失敗: {str(e)}")
286
+
287
+ @app.post("/analyze-food-base64", response_model=FoodAnalysisResponse)
288
+ async def analyze_food_base64(image_data: dict):
289
+ """分析base64編碼的食物圖片"""
290
+ try:
291
+ # 檢查模型是否載入成功
292
+ if not food_classifier:
293
+ raise HTTPException(status_code=500, detail="AI模型尚未載入,請稍後再試")
294
+
295
+ # 解碼base64圖片
296
+ base64_string = image_data.get("image", "")
297
+ if not base64_string:
298
+ raise HTTPException(status_code=400, detail="缺少圖片資料")
299
+
300
+ # 移除base64前綴(如果有的話)
301
+ if "," in base64_string:
302
+ base64_string = base64_string.split(",")[1]
303
+
304
+ # 解碼圖片
305
+ image_bytes = base64.b64decode(base64_string)
306
+ image = Image.open(io.BytesIO(image_bytes))
307
+
308
+ # 確保圖片是RGB格式
309
+ if image.mode != "RGB":
310
+ image = image.convert("RGB")
311
+
312
+ # 使用AI模型進行食物辨識
313
+ results = food_classifier(image)
314
+
315
+ # 獲取最高信心度的結果
316
+ top_result = results[0]
317
+ food_name = top_result["label"]
318
+ confidence = top_result["score"]
319
+
320
+ # 獲取營養資訊
321
+ nutrition_info = get_nutrition_info(food_name)
322
+
323
+ # 生成AI建議
324
+ ai_suggestions = generate_ai_suggestions(food_name, nutrition_info)
325
+
326
+ return FoodAnalysisResponse(
327
+ success=True,
328
+ food_name=food_name,
329
+ confidence=round(confidence * 100, 2),
330
+ nutrition_info=nutrition_info,
331
+ ai_suggestions=ai_suggestions,
332
+ message="食物分析完成"
333
+ )
334
+
335
+ except Exception as e:
336
+ raise HTTPException(status_code=500, detail=f"分析失敗: {str(e)}")
337
+
338
+ if __name__ == "__main__":
339
+ uvicorn.run(app, host="0.0.0.0", port=8000)
health_assistant.db ADDED
Binary file (32.8 kB). View file
 
init_db.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .database import Base, engine, SessionLocal
2
+ from .models.meal_log import MealLog
3
+ from .models.nutrition import Nutrition
4
+ import json
5
+
6
+ def init_db():
7
+ """初始化資料庫並填入初始營養數據"""
8
+ print("Creating database tables...")
9
+ # 根據模型建立所有表格
10
+ Base.metadata.create_all(bind=engine)
11
+ print("Database tables created successfully!")
12
+
13
+ # 檢查是否已有資料,避免重複新增
14
+ db = SessionLocal()
15
+ if db.query(Nutrition).count() == 0:
16
+ print("Populating nutrition table with initial data...")
17
+
18
+ # 從 main.py 移植過來的模擬資料
19
+ mock_nutrition_data = [
20
+ {
21
+ "food_name": "hamburger", "chinese_name": "漢堡", "calories": 540, "protein": 25, "fat": 31, "carbs": 40,
22
+ "fiber": 3, "sugar": 6, "sodium": 1040, "health_score": 45,
23
+ "recommendations": ["脂肪和鈉含量過高,建議減少食用頻率。"],
24
+ "warnings": ["高熱量", "高脂肪", "高鈉"]
25
+ },
26
+ {
27
+ "food_name": "pizza", "chinese_name": "披薩", "calories": 266, "protein": 11, "fat": 10, "carbs": 33,
28
+ "fiber": 2, "sugar": 4, "sodium": 598, "health_score": 65,
29
+ "recommendations": ["可搭配沙拉以增加纖維攝取。"],
30
+ "warnings": ["高鈉"]
31
+ },
32
+ {
33
+ "food_name": "sushi", "chinese_name": "壽司", "calories": 200, "protein": 12, "fat": 8, "carbs": 20,
34
+ "fiber": 1, "sugar": 2, "sodium": 380, "health_score": 85,
35
+ "recommendations": ["優質的蛋白質和碳水化合物來源。"],
36
+ "warnings": []
37
+ },
38
+ {
39
+ "food_name": "fried rice", "chinese_name": "炒飯", "calories": 238, "protein": 8, "fat": 12, "carbs": 26,
40
+ "fiber": 2, "sugar": 3, "sodium": 680, "health_score": 60,
41
+ "recommendations": ["注意油脂和鈉含量。"],
42
+ "warnings": ["高鈉"]
43
+ },
44
+ {
45
+ "food_name": "chicken wings", "chinese_name": "雞翅", "calories": 203, "protein": 18, "fat": 14, "carbs": 0,
46
+ "fiber": 0, "sugar": 0, "sodium": 380, "health_score": 70,
47
+ "recommendations": ["蛋白質的良好來源。"],
48
+ "warnings": []
49
+ },
50
+ {
51
+ "food_name": "salad", "chinese_name": "沙拉", "calories": 33, "protein": 3, "fat": 0.2, "carbs": 6,
52
+ "fiber": 3, "sugar": 3, "sodium": 65, "health_score": 95,
53
+ "recommendations": ["低熱量高纖維,是健康的選擇。"],
54
+ "warnings": []
55
+ }
56
+ ]
57
+
58
+ for food_data in mock_nutrition_data:
59
+ db_item = Nutrition(**food_data)
60
+ db.add(db_item)
61
+
62
+ db.commit()
63
+ print(f"{len(mock_nutrition_data)} items populated.")
64
+ else:
65
+ print("Nutrition table already contains data. Skipping population.")
66
+
67
+ db.close()
68
+
69
+ if __name__ == "__main__":
70
+ init_db()
main.cpython-313.pyc ADDED
Binary file (9.03 kB). View file
 
main.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Depends
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
+ from typing import Dict, Any, List, Optional
5
+ import uvicorn
6
+ import base64
7
+ from io import BytesIO
8
+ from PIL import Image
9
+ import json
10
+ import os
11
+ from dotenv import load_dotenv
12
+
13
+ # Database and services
14
+ from .app.database import get_db
15
+ from sqlalchemy.orm import Session
16
+ from .app.models.nutrition import Nutrition
17
+ from .app.services import nutrition_api_service
18
+
19
+ # Routers
20
+ from .app.routers import ai_router, meal_router
21
+
22
+ app = FastAPI(title="Health Assistant API")
23
+ app.include_router(ai_router.router)
24
+ app.include_router(meal_router.router)
25
+
26
+ # Load environment variables
27
+ load_dotenv()
28
+
29
+ # CORS middleware to allow frontend access
30
+ app.add_middleware(
31
+ CORSMiddleware,
32
+ allow_origins=["*"],
33
+ allow_credentials=True,
34
+ allow_methods=["*"],
35
+ allow_headers=["*"],
36
+ )
37
+
38
+ class FoodRecognitionResponse(BaseModel):
39
+ food_name: str
40
+ chinese_name: str
41
+ confidence: float
42
+ success: bool
43
+
44
+ class NutritionAnalysisResponse(BaseModel):
45
+ success: bool
46
+ food_name: str
47
+ chinese_name: Optional[str] = None
48
+ nutrition: Dict[str, Any]
49
+ analysis: Dict[str, Any]
50
+
51
+ class ErrorResponse(BaseModel):
52
+ detail: str
53
+
54
+ @app.get("/")
55
+ async def root():
56
+ return {"message": "Health Assistant API is running"}
57
+
58
+ @app.get("/api/analyze-nutrition/{food_name}", response_model=NutritionAnalysisResponse, responses={404: {"model": ErrorResponse}})
59
+ async def analyze_nutrition(food_name: str, db: Session = Depends(get_db)):
60
+ """
61
+ Analyze nutrition for a recognized food item by querying the database.
62
+ If not found locally, it queries an external API and saves the new data.
63
+ """
64
+ try:
65
+ # Query the database for the food item (case-insensitive search)
66
+ food_item = db.query(Nutrition).filter(Nutrition.food_name.ilike(f"%{food_name.strip()}%")).first()
67
+
68
+ if not food_item:
69
+ # If not found, query the external API
70
+ print(f"Food '{food_name}' not in local DB. Querying external API...")
71
+ api_data = nutrition_api_service.fetch_nutrition_data(food_name)
72
+
73
+ if not api_data:
74
+ # If external API also doesn't find it, raise 404
75
+ raise HTTPException(status_code=404, detail=f"Food '{food_name}' not found in local database or external API.")
76
+
77
+ # Create a new Nutrition object from the API data
78
+ recommendations = generate_recommendations(api_data)
79
+ warnings = generate_warnings(api_data)
80
+ health_score = calculate_health_score(api_data)
81
+
82
+ new_food_item = Nutrition(
83
+ food_name=api_data.get('food_name', food_name),
84
+ chinese_name=api_data.get('chinese_name'),
85
+ calories=api_data.get('calories', 0),
86
+ protein=api_data.get('protein', 0),
87
+ fat=api_data.get('fat', 0),
88
+ carbs=api_data.get('carbs', 0),
89
+ fiber=api_data.get('fiber', 0),
90
+ sugar=api_data.get('sugar', 0),
91
+ sodium=api_data.get('sodium', 0),
92
+ health_score=health_score,
93
+ recommendations=recommendations,
94
+ warnings=warnings,
95
+ details={} # API doesn't provide extra details in our format
96
+ )
97
+
98
+ # Add to DB and commit
99
+ db.add(new_food_item)
100
+ db.commit()
101
+ db.refresh(new_food_item)
102
+ print(f"Saved new food '{new_food_item.food_name}' to the database.")
103
+ food_item = new_food_item # Use the new item for the response
104
+
105
+ if not food_item:
106
+ raise HTTPException(status_code=404, detail=f"Food '{food_name}' not found in the database.")
107
+
108
+ # Structure the response
109
+ nutrition_details = {
110
+ "calories": food_item.calories,
111
+ "protein": food_item.protein,
112
+ "fat": food_item.fat,
113
+ "carbs": food_item.carbs,
114
+ "fiber": food_item.fiber,
115
+ "sugar": food_item.sugar,
116
+ "sodium": food_item.sodium,
117
+ }
118
+
119
+ # Safely add details if they exist and are a dictionary
120
+ if isinstance(food_item.details, dict):
121
+ nutrition_details.update(food_item.details)
122
+
123
+ analysis_details = {
124
+ "healthScore": food_item.health_score,
125
+ "recommendations": food_item.recommendations,
126
+ "warnings": food_item.warnings
127
+ }
128
+
129
+ return {
130
+ "success": True,
131
+ "food_name": food_item.food_name,
132
+ "chinese_name": food_item.chinese_name,
133
+ "nutrition": nutrition_details,
134
+ "analysis": analysis_details
135
+ }
136
+
137
+ except HTTPException:
138
+ raise
139
+ except Exception as e:
140
+ # Log the error for debugging
141
+ print(f"Error in analyze_nutrition: {str(e)}")
142
+ raise HTTPException(status_code=500, detail=f"Internal server error while processing '{food_name}'.")
143
+
144
+ def calculate_health_score(nutrition: Dict[str, Any]) -> int:
145
+ """Calculate a health score based on nutritional values"""
146
+ score = 100
147
+
148
+ # 熱量評分
149
+ if nutrition.get("calories", 0) > 400:
150
+ score -= 20
151
+ elif nutrition.get("calories", 0) > 300:
152
+ score -= 10
153
+
154
+ # 脂肪評分
155
+ if nutrition.get("fat", 0) > 20:
156
+ score -= 15
157
+ elif nutrition.get("fat", 0) > 15:
158
+ score -= 8
159
+
160
+ # 蛋白質評分
161
+ if nutrition.get("protein", 0) > 15:
162
+ score += 10
163
+ elif nutrition.get("protein", 0) < 5:
164
+ score -= 10
165
+
166
+ # 鈉含量評分
167
+ if nutrition.get("sodium", 0) > 800:
168
+ score -= 15
169
+ elif nutrition.get("sodium", 0) > 600:
170
+ score -= 8
171
+
172
+ return max(0, min(100, score))
173
+
174
+ def generate_recommendations(nutrition: Dict[str, Any]) -> List[str]:
175
+ """Generate dietary recommendations based on nutrition data"""
176
+ recommendations = []
177
+
178
+ if nutrition.get("protein", 0) < 10:
179
+ recommendations.append("建議增加蛋白質攝取,可搭配雞蛋或豆腐")
180
+
181
+ if nutrition.get("fat", 0) > 20:
182
+ recommendations.append("脂肪含量較高,建議適量食用")
183
+
184
+ if nutrition.get("fiber", 0) < 3:
185
+ recommendations.append("纖維含量不足,建議搭配蔬菜沙拉")
186
+
187
+ if nutrition.get("sodium", 0) > 600:
188
+ recommendations.append("鈉含量偏高,建議多喝水並減少其他鹽分攝取")
189
+
190
+ return recommendations
191
+
192
+ def generate_warnings(nutrition: Dict[str, Any]) -> List[str]:
193
+ """Generate dietary warnings based on nutrition data"""
194
+ warnings = []
195
+
196
+ if nutrition.get("calories", 0) > 500:
197
+ warnings.append("高熱量食物")
198
+
199
+ if nutrition.get("fat", 0) > 25:
200
+ warnings.append("高脂肪食物")
201
+
202
+ if nutrition.get("sodium", 0) > 1000:
203
+ warnings.append("高鈉食物")
204
+
205
+ return warnings
206
+
207
+ if __name__ == "__main__":
208
+ # It's better to run with `uvicorn backend.main:app --reload` from the project root.
209
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
models/__pycache__/meal_log.cpython-313.pyc ADDED
Binary file (1.13 kB). View file
 
models/__pycache__/nutrition.cpython-313.pyc ADDED
Binary file (1.17 kB). View file
 
models/meal_log.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, Float, DateTime, JSON
2
+ from ..database import Base
3
+
4
+ class MealLog(Base):
5
+ __tablename__ = "meal_logs"
6
+
7
+ id = Column(Integer, primary_key=True, index=True)
8
+ food_name = Column(String, index=True)
9
+ meal_type = Column(String) # breakfast, lunch, dinner, snack
10
+ portion_size = Column(String) # small, medium, large
11
+ calories = Column(Float)
12
+ protein = Column(Float)
13
+ carbs = Column(Float)
14
+ fat = Column(Float)
15
+ fiber = Column(Float)
16
+ meal_date = Column(DateTime, index=True)
17
+ image_url = Column(String)
18
+ ai_analysis = Column(JSON) # 儲存完整的 AI 分析結果
19
+ created_at = Column(DateTime)
models/nutrition.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # backend/app/models/nutrition.py
2
+ from sqlalchemy import Column, Integer, String, Float, JSON
3
+ from ..database import Base
4
+
5
+ class Nutrition(Base):
6
+ __tablename__ = "nutrition"
7
+
8
+ id = Column(Integer, primary_key=True, index=True)
9
+ food_name = Column(String, unique=True, index=True, nullable=False)
10
+ chinese_name = Column(String)
11
+ calories = Column(Float)
12
+ protein = Column(Float)
13
+ fat = Column(Float)
14
+ carbs = Column(Float)
15
+ fiber = Column(Float)
16
+ sugar = Column(Float)
17
+ sodium = Column(Float)
18
+ # For more complex data like vitamins, minerals, etc.
19
+ details = Column(JSON)
20
+ health_score = Column(Integer)
21
+ recommendations = Column(JSON)
22
+ warnings = Column(JSON)
routers/__pycache__/ai_router.cpython-313.pyc ADDED
Binary file (1.52 kB). View file
 
routers/__pycache__/meal_router.cpython-313.pyc ADDED
Binary file (4.72 kB). View file
 
routers/ai_router.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 檔案路徑: backend/app/routers/ai_router.py
2
+
3
+ from fastapi import APIRouter, File, UploadFile, HTTPException
4
+ from ..services.ai_service import classify_food_image # 直接引入分類函式
5
+ from ..services.nutrition_api_service import fetch_nutrition_data # 匯入營養查詢函式
6
+
7
+ router = APIRouter(
8
+ prefix="/ai",
9
+ tags=["AI"],
10
+ )
11
+
12
+ @router.post("/analyze-food-image/")
13
+ async def analyze_food_image_endpoint(file: UploadFile = File(...)):
14
+ """
15
+ 這個端點接收使用者上傳的食物圖片,使用 AI 模型進行辨識,
16
+ 並返回辨識出的食物名稱。
17
+ """
18
+ # 檢查上傳的檔案是否為圖片格式
19
+ if not file.content_type or not file.content_type.startswith("image/"):
20
+ raise HTTPException(status_code=400, detail="上傳的檔案不是圖片格式。")
21
+
22
+ # 讀取圖片的二進位制內容
23
+ image_bytes = await file.read()
24
+
25
+ # 呼叫 AI 服務中的分類函式
26
+ food_name = classify_food_image(image_bytes)
27
+
28
+ # 查詢營養資訊
29
+ nutrition_info = fetch_nutrition_data(food_name)
30
+ if nutrition_info is None:
31
+ raise HTTPException(status_code=404, detail=f"找不到 {food_name} 的營養資訊。")
32
+
33
+ # TODO: 在下一階段,我們會在這裡加入從資料庫查詢營養資訊的邏輯
34
+ # 目前,我們先直接回傳辨識出的食物名稱
35
+
36
+ return {"food_name": food_name, "nutrition_info": nutrition_info}
routers/meal_router.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from sqlalchemy.orm import Session
3
+ from typing import List, Dict, Any, Optional
4
+ from datetime import datetime
5
+ from ..services.meal_service import MealService
6
+ from ..database import get_db
7
+ from pydantic import BaseModel
8
+
9
+ router = APIRouter(prefix="/api/meals", tags=["Meals"])
10
+
11
+ class MealCreate(BaseModel):
12
+ food_name: str
13
+ meal_type: str
14
+ portion_size: str
15
+ meal_date: datetime
16
+ nutrition: Dict[str, float]
17
+ image_url: Optional[str] = None
18
+ ai_analysis: Optional[Dict[str, Any]] = None
19
+
20
+ class DateRange(BaseModel):
21
+ start_date: datetime
22
+ end_date: datetime
23
+ meal_type: Optional[str] = None
24
+
25
+ @router.post("/log")
26
+ async def create_meal_log(
27
+ meal: MealCreate,
28
+ db: Session = Depends(get_db)
29
+ ) -> Dict[str, Any]:
30
+ """創建新的用餐記錄"""
31
+ meal_service = MealService(db)
32
+ try:
33
+ meal_log = meal_service.create_meal_log(
34
+ food_name=meal.food_name,
35
+ meal_type=meal.meal_type,
36
+ portion_size=meal.portion_size,
37
+ nutrition=meal.nutrition,
38
+ meal_date=meal.meal_date,
39
+ image_url=meal.image_url,
40
+ ai_analysis=meal.ai_analysis
41
+ )
42
+ return {
43
+ "success": True,
44
+ "message": "用餐記錄已創建",
45
+ "data": {
46
+ "id": meal_log.id,
47
+ "food_name": meal_log.food_name,
48
+ "meal_type": meal_log.meal_type,
49
+ "meal_date": meal_log.meal_date
50
+ }
51
+ }
52
+ except Exception as e:
53
+ raise HTTPException(status_code=500, detail=str(e))
54
+
55
+ @router.post("/list")
56
+ async def get_meal_logs(
57
+ date_range: DateRange,
58
+ db: Session = Depends(get_db)
59
+ ) -> Dict[str, Any]:
60
+ """獲取用餐記錄列表"""
61
+ meal_service = MealService(db)
62
+ try:
63
+ logs = meal_service.get_meal_logs(
64
+ start_date=date_range.start_date,
65
+ end_date=date_range.end_date,
66
+ meal_type=date_range.meal_type
67
+ )
68
+ return {
69
+ "success": True,
70
+ "data": [{
71
+ "id": log.id,
72
+ "food_name": log.food_name,
73
+ "meal_type": log.meal_type,
74
+ "portion_size": log.portion_size,
75
+ "calories": log.calories,
76
+ "protein": log.protein,
77
+ "carbs": log.carbs,
78
+ "fat": log.fat,
79
+ "meal_date": log.meal_date,
80
+ "image_url": log.image_url
81
+ } for log in logs]
82
+ }
83
+ except Exception as e:
84
+ raise HTTPException(status_code=500, detail=str(e))
85
+
86
+ @router.post("/nutrition-summary")
87
+ async def get_nutrition_summary(
88
+ date_range: DateRange,
89
+ db: Session = Depends(get_db)
90
+ ) -> Dict[str, Any]:
91
+ """獲取營養攝入總結"""
92
+ meal_service = MealService(db)
93
+ try:
94
+ summary = meal_service.get_nutrition_summary(
95
+ start_date=date_range.start_date,
96
+ end_date=date_range.end_date
97
+ )
98
+ return {
99
+ "success": True,
100
+ "data": summary
101
+ }
102
+ except Exception as e:
103
+ raise HTTPException(status_code=500, detail=str(e))
services/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # This file makes the services directory a Python package
2
+ # Import the HybridFoodAnalyzer class to make it available when importing from app.services
3
+ from .food_analyzer_service import HybridFoodAnalyzer
4
+
5
+ __all__ = ['HybridFoodAnalyzer']
services/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (273 Bytes). View file
 
services/__pycache__/ai_service.cpython-313.pyc ADDED
Binary file (3.44 kB). View file
 
services/__pycache__/food_analyzer_service.cpython-313.pyc ADDED
Binary file (10.3 kB). View file
 
services/__pycache__/meal_service.cpython-313.pyc ADDED
Binary file (4.13 kB). View file
 
services/__pycache__/nutrition_api_service.cpython-313.pyc ADDED
Binary file (4.14 kB). View file
 
services/ai_service.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 檔案路徑: backend/app/services/ai_service.py
2
+
3
+ from transformers.pipelines import pipeline
4
+ from PIL import Image
5
+ import io
6
+ import logging
7
+
8
+ # 設置日誌
9
+ logging.basicConfig(level=logging.INFO)
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # 全局變量
13
+ image_classifier = None
14
+
15
+ def load_model():
16
+ """載入模型的函數"""
17
+ global image_classifier
18
+
19
+ try:
20
+ logger.info("正在載入食物辨識模型...")
21
+ # 載入模型 - 移除不支持的參數
22
+ image_classifier = pipeline(
23
+ "image-classification",
24
+ model="juliensimon/autotrain-food101-1471154053",
25
+ device=-1 # 使用CPU
26
+ )
27
+ logger.info("模型載入成功!")
28
+ return True
29
+ except Exception as e:
30
+ logger.error(f"模型載入失敗: {str(e)}")
31
+ image_classifier = None
32
+ return False
33
+
34
+ def classify_food_image(image_bytes: bytes) -> str:
35
+ """
36
+ 接收圖片的二進位制數據,進行分類並返回可能性最高的食物名稱。
37
+ """
38
+ global image_classifier
39
+
40
+ # 如果模型未載入,嘗試重新載入
41
+ if image_classifier is None:
42
+ logger.warning("模型未載入,嘗試重新載入...")
43
+ if not load_model():
44
+ return "Error: Model not loaded"
45
+
46
+ if image_classifier is None:
47
+ return "Error: Model could not be loaded"
48
+
49
+ try:
50
+ # 驗證圖片數據
51
+ if not image_bytes:
52
+ return "Error: Empty image data"
53
+
54
+ # 從記憶體中的 bytes 打開圖片
55
+ image = Image.open(io.BytesIO(image_bytes))
56
+
57
+ # 確保圖片是RGB格式
58
+ if image.mode != 'RGB':
59
+ image = image.convert('RGB')
60
+
61
+ logger.info(f"處理圖片,尺寸: {image.size}")
62
+
63
+ # 使用模型管線進行分類
64
+ pipeline_output = image_classifier(image)
65
+
66
+ logger.info(f"模型輸出: {pipeline_output}")
67
+
68
+ # 處理輸出結果
69
+ if not pipeline_output:
70
+ return "Unknown"
71
+
72
+ # pipeline_output 通常是一個列表
73
+ if isinstance(pipeline_output, list) and len(pipeline_output) > 0:
74
+ result = pipeline_output[0]
75
+ if isinstance(result, dict) and 'label' in result:
76
+ label = result['label']
77
+ confidence = result.get('score', 0)
78
+
79
+ logger.info(f"辨識結果: {label}, 信心度: {confidence:.2f}")
80
+
81
+ # 標籤可能包含底線,我們將其替換為空格,並讓首字母大寫
82
+ formatted_label = str(label).replace('_', ' ').title()
83
+ return formatted_label
84
+
85
+ return "Unknown"
86
+
87
+ except Exception as e:
88
+ logger.error(f"圖片分類過程中發生錯誤: {str(e)}")
89
+ return f"Error: {str(e)}"
90
+
91
+ # 在模塊載入時嘗試載入模型
92
+ logger.info("初始化 AI 服務...")
93
+ load_model()
94
+
95
+ __all__ = ["classify_food_image"]
services/food_analyzer_service.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import json
3
+ from typing import Dict, Any, Optional
4
+ from PIL import Image
5
+ from io import BytesIO
6
+ import base64
7
+ import os
8
+ from dotenv import load_dotenv
9
+
10
+ # Load environment variables
11
+ load_dotenv()
12
+
13
+ class HybridFoodAnalyzer:
14
+ def __init__(self, claude_api_key: Optional[str] = None):
15
+ """
16
+ Initialize the HybridFoodAnalyzer with HuggingFace model and Claude API.
17
+
18
+ Args:
19
+ claude_api_key: Optional API key for Claude. If not provided, will try to get from environment variable CLAUDE_API_KEY.
20
+ """
21
+ # Initialize HuggingFace model
22
+ from transformers import AutoImageProcessor, AutoModelForImageClassification
23
+
24
+ print("Loading HuggingFace food recognition model...")
25
+ self.processor = AutoImageProcessor.from_pretrained("nateraw/food")
26
+ self.model = AutoModelForImageClassification.from_pretrained("nateraw/food")
27
+ self.model.eval() # Set model to evaluation mode
28
+
29
+ # Initialize Claude API
30
+ print("Initializing Claude API...")
31
+ import anthropic
32
+ self.claude_api_key = claude_api_key or os.getenv('CLAUDE_API_KEY')
33
+ if not self.claude_api_key:
34
+ raise ValueError("Claude API key is required. Please set CLAUDE_API_KEY environment variable or pass it as an argument.")
35
+
36
+ self.claude_client = anthropic.Anthropic(api_key=self.claude_api_key)
37
+
38
+ def recognize_food(self, image: Image.Image) -> Dict[str, Any]:
39
+ """
40
+ Recognize food from an image using HuggingFace model.
41
+
42
+ Args:
43
+ image: PIL Image object containing the food image
44
+
45
+ Returns:
46
+ Dictionary containing food name and confidence score
47
+ """
48
+ try:
49
+ print("Processing image for food recognition...")
50
+ inputs = self.processor(images=image, return_tensors="pt")
51
+
52
+ with torch.no_grad():
53
+ outputs = self.model(**inputs)
54
+ predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
55
+
56
+ predicted_class_id = int(predictions.argmax().item())
57
+ confidence = predictions[0][predicted_class_id].item()
58
+ food_name = self.model.config.id2label[predicted_class_id]
59
+
60
+ # Map common food names to Chinese
61
+ food_name_mapping = {
62
+ "hamburger": "漢堡",
63
+ "pizza": "披薩",
64
+ "sushi": "壽司",
65
+ "fried rice": "炒飯",
66
+ "chicken wings": "雞翅",
67
+ "salad": "沙拉",
68
+ "apple": "蘋果",
69
+ "banana": "香蕉",
70
+ "orange": "橙子",
71
+ "noodles": "麵條"
72
+ }
73
+
74
+ chinese_name = food_name_mapping.get(food_name.lower(), food_name)
75
+
76
+ return {
77
+ "food_name": food_name,
78
+ "chinese_name": chinese_name,
79
+ "confidence": confidence,
80
+ "success": True
81
+ }
82
+
83
+ except Exception as e:
84
+ print(f"Error in food recognition: {str(e)}")
85
+ return {
86
+ "success": False,
87
+ "error": str(e)
88
+ }
89
+
90
+ def analyze_nutrition(self, food_name: str) -> Dict[str, Any]:
91
+ """
92
+ Analyze nutrition information for a given food using Claude API.
93
+
94
+ Args:
95
+ food_name: Name of the food to analyze
96
+
97
+ Returns:
98
+ Dictionary containing nutrition information
99
+ """
100
+ try:
101
+ print(f"Analyzing nutrition for {food_name}...")
102
+ prompt = f"""
103
+ 請分析 {food_name} 的營養成分(每100g),並以JSON格式回覆:
104
+ {{
105
+ "calories": 數值,
106
+ "protein": 數值,
107
+ "fat": 數值,
108
+ "carbs": 數值,
109
+ "fiber": 數值,
110
+ "sugar": 數值,
111
+ "sodium": 數值
112
+ }}
113
+ """
114
+
115
+ message = self.claude_client.messages.create(
116
+ model="claude-3-sonnet-20240229",
117
+ max_tokens=500,
118
+ messages=[{"role": "user", "content": prompt}]
119
+ )
120
+
121
+ # Extract and parse the JSON response
122
+ response_text = message.content[0].get("text", "") if isinstance(message.content[0], dict) else str(message.content[0])
123
+ try:
124
+ nutrition_data = json.loads(response_text.strip())
125
+ return {
126
+ "success": True,
127
+ "nutrition": nutrition_data
128
+ }
129
+ except json.JSONDecodeError as e:
130
+ print(f"Error parsing Claude response: {e}")
131
+ return {
132
+ "success": False,
133
+ "error": f"Failed to parse nutrition data: {e}",
134
+ "raw_response": response_text
135
+ }
136
+
137
+ except Exception as e:
138
+ print(f"Error in nutrition analysis: {str(e)}")
139
+ return {
140
+ "success": False,
141
+ "error": str(e)
142
+ }
143
+
144
+ def process_image(self, image_data: bytes) -> Dict[str, Any]:
145
+ """
146
+ Process an image to recognize food and analyze its nutrition.
147
+
148
+ Args:
149
+ image_data: Binary image data
150
+
151
+ Returns:
152
+ Dictionary containing recognition and analysis results
153
+ """
154
+ try:
155
+ # Convert bytes to PIL Image
156
+ image = Image.open(BytesIO(image_data))
157
+
158
+ # Step 1: Recognize food
159
+ recognition_result = self.recognize_food(image)
160
+ if not recognition_result.get("success"):
161
+ return recognition_result
162
+
163
+ # Step 2: Analyze nutrition
164
+ nutrition_result = self.analyze_nutrition(recognition_result["food_name"])
165
+ if not nutrition_result.get("success"):
166
+ return nutrition_result
167
+
168
+ # Calculate health score
169
+ nutrition = nutrition_result["nutrition"]
170
+ health_score = self.calculate_health_score(nutrition)
171
+
172
+ # Generate recommendations and warnings
173
+ recommendations = self.generate_recommendations(nutrition)
174
+ warnings = self.generate_warnings(nutrition)
175
+
176
+ return {
177
+ "success": True,
178
+ "food_name": recognition_result["food_name"],
179
+ "chinese_name": recognition_result["chinese_name"],
180
+ "confidence": recognition_result["confidence"],
181
+ "nutrition": nutrition,
182
+ "analysis": {
183
+ "healthScore": health_score,
184
+ "recommendations": recommendations,
185
+ "warnings": warnings
186
+ }
187
+ }
188
+
189
+ except Exception as e:
190
+ return {
191
+ "success": False,
192
+ "error": f"Failed to process image: {str(e)}"
193
+ }
194
+
195
+ def calculate_health_score(self, nutrition: Dict[str, float]) -> int:
196
+ """Calculate a health score based on nutritional values"""
197
+ score = 100
198
+
199
+ # 熱量評分
200
+ if nutrition["calories"] > 400:
201
+ score -= 20
202
+ elif nutrition["calories"] > 300:
203
+ score -= 10
204
+
205
+ # 脂肪評分
206
+ if nutrition["fat"] > 20:
207
+ score -= 15
208
+ elif nutrition["fat"] > 15:
209
+ score -= 8
210
+
211
+ # 蛋白質評分
212
+ if nutrition["protein"] > 15:
213
+ score += 10
214
+ elif nutrition["protein"] < 5:
215
+ score -= 10
216
+
217
+ # 鈉含量評分
218
+ if "sodium" in nutrition and nutrition["sodium"] > 800:
219
+ score -= 15
220
+ elif "sodium" in nutrition and nutrition["sodium"] > 600:
221
+ score -= 8
222
+
223
+ return max(0, min(100, score))
224
+
225
+ def generate_recommendations(self, nutrition: Dict[str, float]) -> list:
226
+ """Generate dietary recommendations based on nutrition data"""
227
+ recommendations = []
228
+
229
+ if nutrition["protein"] < 10:
230
+ recommendations.append("建議增加蛋白質攝取,可搭配雞蛋或豆腐")
231
+
232
+ if nutrition["fat"] > 20:
233
+ recommendations.append("脂肪含量較高,建議適量食用")
234
+
235
+ if "fiber" in nutrition and nutrition["fiber"] < 3:
236
+ recommendations.append("纖維含量不足,建議搭配蔬菜沙拉")
237
+
238
+ if "sodium" in nutrition and nutrition["sodium"] > 600:
239
+ recommendations.append("鈉含量偏高,建議多喝水並減少其他鹽分攝取")
240
+
241
+ return recommendations
242
+
243
+ def generate_warnings(self, nutrition: Dict[str, float]) -> list:
244
+ """Generate dietary warnings based on nutrition data"""
245
+ warnings = []
246
+
247
+ if nutrition["calories"] > 500:
248
+ warnings.append("高熱量食物")
249
+
250
+ if nutrition["fat"] > 25:
251
+ warnings.append("高脂肪食物")
252
+
253
+ if "sodium" in nutrition and nutrition["sodium"] > 1000:
254
+ warnings.append("高鈉食物")
255
+
256
+ return warnings
services/meal_service.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from typing import List, Dict, Any, Optional
3
+ from sqlalchemy.orm import Session
4
+ from ..models.meal_log import MealLog
5
+
6
+ class MealService:
7
+ def __init__(self, db: Session):
8
+ self.db = db
9
+
10
+ def create_meal_log(
11
+ self,
12
+ food_name: str,
13
+ meal_type: str,
14
+ portion_size: str,
15
+ nutrition: Dict[str, float],
16
+ meal_date: datetime,
17
+ image_url: Optional[str] = None,
18
+ ai_analysis: Optional[Dict[str, Any]] = None
19
+ ) -> MealLog:
20
+ """創建新的用餐記錄"""
21
+ meal_log = MealLog(
22
+ food_name=food_name,
23
+ meal_type=meal_type,
24
+ portion_size=portion_size,
25
+ calories=nutrition.get('calories', 0),
26
+ protein=nutrition.get('protein', 0),
27
+ carbs=nutrition.get('carbs', 0),
28
+ fat=nutrition.get('fat', 0),
29
+ fiber=nutrition.get('fiber', 0),
30
+ meal_date=meal_date,
31
+ image_url=image_url,
32
+ ai_analysis=ai_analysis,
33
+ created_at=datetime.utcnow()
34
+ )
35
+
36
+ self.db.add(meal_log)
37
+ self.db.commit()
38
+ self.db.refresh(meal_log)
39
+ return meal_log
40
+
41
+ def get_meal_logs(
42
+ self,
43
+ start_date: Optional[datetime] = None,
44
+ end_date: Optional[datetime] = None,
45
+ meal_type: Optional[str] = None
46
+ ) -> List[MealLog]:
47
+ """獲取用餐記錄"""
48
+ query = self.db.query(MealLog)
49
+
50
+ if start_date:
51
+ query = query.filter(MealLog.meal_date >= start_date)
52
+ if end_date:
53
+ query = query.filter(MealLog.meal_date <= end_date)
54
+ if meal_type:
55
+ query = query.filter(MealLog.meal_type == meal_type)
56
+
57
+ return query.order_by(MealLog.meal_date.desc()).all()
58
+
59
+ def get_nutrition_summary(
60
+ self,
61
+ start_date: datetime,
62
+ end_date: datetime
63
+ ) -> Dict[str, float]:
64
+ """獲取指定時間範圍內的營養攝入總結"""
65
+ meals = self.get_meal_logs(start_date, end_date)
66
+
67
+ summary = {
68
+ 'total_calories': 0,
69
+ 'total_protein': 0,
70
+ 'total_carbs': 0,
71
+ 'total_fat': 0,
72
+ 'total_fiber': 0
73
+ }
74
+
75
+ for meal in meals:
76
+ # 根據份量大小調整營養值
77
+ multiplier = {
78
+ 'small': 0.7,
79
+ 'medium': 1.0,
80
+ 'large': 1.3
81
+ }.get(meal.portion_size, 1.0)
82
+
83
+ summary['total_calories'] += meal.calories * multiplier
84
+ summary['total_protein'] += meal.protein * multiplier
85
+ summary['total_carbs'] += meal.carbs * multiplier
86
+ summary['total_fat'] += meal.fat * multiplier
87
+ summary['total_fiber'] += meal.fiber * multiplier
88
+
89
+ return summary
services/nutrition_api_service.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # backend/app/services/nutrition_api_service.py
2
+ import os
3
+ import requests
4
+ from dotenv import load_dotenv
5
+ import logging
6
+
7
+ # 設置日誌
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # 載入環境變數
12
+ load_dotenv()
13
+
14
+ # 從環境變數中獲取 API 金鑰
15
+ USDA_API_KEY = os.getenv("USDA_API_KEY", "DEMO_KEY")
16
+ USDA_API_URL = "https://api.nal.usda.gov/fdc/v1/foods/search"
17
+
18
+ # 我們關心的主要營養素及其在 USDA API 中的名稱或編號
19
+ # 我們可以透過 nutrient.nutrientNumber 或 nutrient.name 來匹配
20
+ NUTRIENT_MAP = {
21
+ 'calories': 'Energy',
22
+ 'protein': 'Protein',
23
+ 'fat': 'Total lipid (fat)',
24
+ 'carbs': 'Carbohydrate, by difference',
25
+ 'fiber': 'Fiber, total dietary',
26
+ 'sugar': 'Total sugars',
27
+ 'sodium': 'Sodium, Na'
28
+ }
29
+
30
+ def fetch_nutrition_data(food_name: str):
31
+ """
32
+ 從 USDA FoodData Central API 獲取食物的營養資訊。
33
+
34
+ :param food_name: 要查詢的食物名稱 (例如 "Donuts")。
35
+ :return: 包含營養資訊的字典,如果找不到則返回 None。
36
+ """
37
+ if not USDA_API_KEY:
38
+ logger.error("USDA_API_KEY 未設定,無法查詢營養資訊。")
39
+ return None
40
+
41
+ params = {
42
+ 'query': food_name,
43
+ 'api_key': USDA_API_KEY,
44
+ 'dataType': 'Branded', # 優先搜尋品牌食品,結果通常更符合預期
45
+ 'pageSize': 1 # 我們只需要最相關的一筆結果
46
+ }
47
+
48
+ try:
49
+ logger.info(f"正在向 USDA API 查詢食物:{food_name}")
50
+ response = requests.get(USDA_API_URL, params=params)
51
+ response.raise_for_status() # 如果請求失敗 (例如 4xx 或 5xx),則會拋出異常
52
+
53
+ data = response.json()
54
+
55
+ # 檢查是否有找到食物
56
+ if data.get('foods') and len(data['foods']) > 0:
57
+ food_data = data['foods'][0] # 取第一個最相關的結果
58
+ logger.info(f"從 API 成功獲取到食物 '{food_data.get('description')}' 的資料")
59
+
60
+ nutrition_info = {
61
+ "food_name": food_data.get('description', food_name).capitalize(),
62
+ "chinese_name": None, # USDA API 不提供中文名
63
+ }
64
+
65
+ # 遍歷我們需要的營養素
66
+ extracted_nutrients = {key: 0.0 for key in NUTRIENT_MAP.keys()} # 初始化
67
+
68
+ for nutrient in food_data.get('foodNutrients', []):
69
+ for key, name in NUTRIENT_MAP.items():
70
+ if nutrient.get('nutrientName').strip().lower() == name.strip().lower():
71
+ # 將值存入我們的格式
72
+ extracted_nutrients[key] = float(nutrient.get('value', 0.0))
73
+ break # 找到後就跳出內層迴圈
74
+
75
+ nutrition_info.update(extracted_nutrients)
76
+
77
+ # 由於 USDA 不直接提供健康建議,我們先回傳原始數據
78
+ # 後續可以在 main.py 中根據這些數據生成我們自己的建議
79
+ return nutrition_info
80
+
81
+ else:
82
+ logger.warning(f"在 USDA API 中找不到食物:{food_name}")
83
+ return None
84
+
85
+ except requests.exceptions.RequestException as e:
86
+ logger.error(f"請求 USDA API 時發生錯誤: {e}")
87
+ return None
88
+ except Exception as e:
89
+ logger.error(f"處理 API 回應時發生未知錯誤: {e}")
90
+ return None
91
+
92
+ if __name__ == '__main__':
93
+ # 測試此模組的功能
94
+ test_food = "donuts"
95
+ nutrition = fetch_nutrition_data(test_food)
96
+ if nutrition:
97
+ import json
98
+ print(f"成功獲取 '{test_food}' 的營養資訊:")
99
+ print(json.dumps(nutrition, indent=2))
100
+ else:
101
+ print(f"無法獲取 '{test_food}' 的營養資訊。")
setup.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="health_assistant",
5
+ version="0.1.0",
6
+ packages=find_packages(where="backend"),
7
+ package_dir={"": "backend"},
8
+ install_requires=[
9
+ "fastapi>=0.68.0",
10
+ "uvicorn>=0.15.0",
11
+ "python-multipart>=0.0.5",
12
+ "Pillow>=8.3.1",
13
+ "transformers>=4.11.3",
14
+ "torch>=1.9.0",
15
+ "python-dotenv>=0.19.0",
16
+ "httpx>=0.19.0",
17
+ "pydantic>=1.8.0",
18
+ ],
19
+ extras_require={
20
+ "dev": [
21
+ "pytest>=6.2.5",
22
+ "pytest-cov>=2.12.1",
23
+ "pytest-asyncio>=0.15.1",
24
+ "black>=21.7b0",
25
+ "isort>=5.9.3",
26
+ "mypy>=0.910",
27
+ ],
28
+ },
29
+ python_requires=">=3.8",
30
+ )
test_pytest.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ def test_example():
2
+ assert 1 + 1 == 2