RajatMalviya commited on
Commit
42ecad8
·
verified ·
1 Parent(s): fe3f5f5

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +275 -0
app.py ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uvicorn
3
+ from fastapi import FastAPI, File, UploadFile
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ import google.generativeai as genai
6
+ from ultralytics import YOLO
7
+ from PIL import Image
8
+ import io
9
+ import json
10
+ import re
11
+ import pickle
12
+ import time
13
+ from datetime import datetime
14
+
15
+ # Initialize FastAPI app
16
+ app = FastAPI(title="Food Nutrition API",
17
+ description="API for detecting food items and retrieving nutritional information")
18
+
19
+ # Add CORS middleware
20
+ app.add_middleware(
21
+ CORSMiddleware,
22
+ allow_origins=["*"], # Allows all origins, modify for production
23
+ allow_credentials=True,
24
+ allow_methods=["*"], # Allows all methods
25
+ allow_headers=["*"], # Allows all headers
26
+ )
27
+
28
+ # Load YOLOv8 model
29
+ model = YOLO("yolov8n.pt")
30
+
31
+ # Set up Google Gemini API - Get from environment variable
32
+ GOOGLE_API_KEY = "AIzaSyAlvHXHO7xFFjfEyaNOmyZvn9FFPPJdIx4"
33
+ genai.configure(api_key=GOOGLE_API_KEY)
34
+ gemini_model = genai.GenerativeModel("gemini-pro")
35
+
36
+ # # Cache configuration
37
+ # CACHE_DIR = os.getenv("CACHE_DIR", ".") # Get cache directory from environment or use current directory
38
+ # CACHE_FILE = os.path.join(CACHE_DIR, "food_nutrition_cache.pkl")
39
+ # CACHE_EXPIRY_DAYS = int(os.getenv("CACHE_EXPIRY_DAYS", "30")) # Cache entries expire after 30 days by default
40
+
41
+ CACHE_DIR = os.getenv("CACHE_DIR", "") # Use /tmp for temporary storage
42
+ CACHE_FILE = os.path.join(CACHE_DIR, "food_nutrition_cache.pkl")
43
+ CACHE_EXPIRY_DAYS = int(os.getenv("CACHE_EXPIRY_DAYS", "30")) # Default to 30 days
44
+
45
+ def load_cache():
46
+ """Load the nutrition cache from disk."""
47
+ if os.path.exists(CACHE_FILE):
48
+ try:
49
+ with open(CACHE_FILE, 'rb') as f:
50
+ return pickle.load(f)
51
+ except (pickle.PickleError, EOFError):
52
+ print("Cache file corrupted, creating new cache")
53
+ return {}
54
+
55
+
56
+ def save_cache(cache):
57
+ """Save the nutrition cache to disk."""
58
+ try:
59
+ # Ensure cache directory exists
60
+ os.makedirs(os.path.dirname(CACHE_FILE), exist_ok=True)
61
+ with open(CACHE_FILE, 'wb') as f:
62
+ pickle.dump(cache, f)
63
+ except Exception as e:
64
+ print(f"Error saving cache: {e}")
65
+
66
+
67
+ # Initialize the cache
68
+ nutrition_cache = load_cache()
69
+
70
+
71
+ def clean_expired_cache_entries():
72
+ """Remove expired entries from the cache."""
73
+ current_time = time.time()
74
+ expiry_seconds = CACHE_EXPIRY_DAYS * 24 * 60 * 60
75
+ expired_keys = []
76
+
77
+ for food_item, entry in nutrition_cache.items():
78
+ if current_time - entry['timestamp'] > expiry_seconds:
79
+ expired_keys.append(food_item)
80
+
81
+ for key in expired_keys:
82
+ del nutrition_cache[key]
83
+
84
+ if expired_keys:
85
+ save_cache(nutrition_cache)
86
+ print(f"Removed {len(expired_keys)} expired cache entries")
87
+
88
+
89
+ def get_food_info(food_item):
90
+ """Fetch nutritional information with caching."""
91
+ # Check if the food item is in cache and not expired
92
+ if food_item in nutrition_cache:
93
+ print(f"Cache hit for {food_item}")
94
+ return nutrition_cache[food_item]['data']
95
+
96
+ print(f"Cache miss for {food_item}, fetching from API")
97
+ # If not in cache, fetch from Gemini API
98
+ prompt = f"""
99
+ Provide the nutritional information for "{food_item}" in JSON format with the following structure:
100
+
101
+ {{
102
+ "food_item": "{food_item}",
103
+ "nutritional_info": {{
104
+ "calories_kcal": value,
105
+ "protein_g": value,
106
+ "fiber_g": value,
107
+ "vitamins": {{
108
+ "Vitamin A_mcg": value,
109
+ "Vitamin C_mg": value,
110
+ "Vitamin D_mcg": value,
111
+ "Vitamin E_mg": value,
112
+ "Vitamin K_mcg": value,
113
+ "Vitamin B1_mg": value,
114
+ "Vitamin B2_mg": value,
115
+ "Vitamin B3_mg": value,
116
+ "Vitamin B6_mg": value,
117
+ "Vitamin B12_mcg": value,
118
+ "Folate_mcg": value
119
+ }}
120
+ }},
121
+ "health_benefits": [
122
+ "Brief description of health benefit 1",
123
+ "Brief description of health benefit 2"
124
+ ]
125
+ }}
126
+
127
+ Ensure the response is **valid JSON** inside triple backticks (```json ... ```).
128
+ """
129
+
130
+ try:
131
+ response = gemini_model.generate_content(prompt)
132
+ result = response.text if response else "No data found"
133
+
134
+ # Store in cache with timestamp
135
+ nutrition_cache[food_item] = {
136
+ 'data': result,
137
+ 'timestamp': time.time()
138
+ }
139
+
140
+ # Save updated cache
141
+ save_cache(nutrition_cache)
142
+
143
+ return result
144
+ except Exception as e:
145
+ print(f"Error fetching from Gemini API: {e}")
146
+ return json.dumps({"error": f"Failed to retrieve data: {str(e)}"})
147
+
148
+
149
+ def parse_nutrition_response(response_dict):
150
+ """Parses the raw response containing JSON-like data embedded in a string format."""
151
+ parsed_data = {}
152
+
153
+ for food, raw_json in response_dict.items():
154
+ # Extract JSON part from the response using regex
155
+ json_match = re.search(r"```json\n(.*?)\n```", raw_json, re.DOTALL)
156
+
157
+ if json_match:
158
+ json_data = json_match.group(1) # Extract JSON content
159
+ try:
160
+ parsed_data[food] = json.loads(json_data) # Convert JSON string to dictionary
161
+ except json.JSONDecodeError:
162
+ parsed_data[food] = {"error": "Invalid JSON format"}
163
+ else:
164
+ parsed_data[food] = {"error": "JSON not found in response"}
165
+
166
+ return parsed_data
167
+
168
+
169
+ def detect_food(image: Image.Image):
170
+ """Detect food items in an image using YOLOv8."""
171
+ results = model(image)
172
+ detected_foods = {model.names[int(box.cls)] for result in results for box in result.boxes}
173
+ return list(detected_foods)
174
+
175
+ @app.get("/")
176
+ def read_root():
177
+ return {"message": "FastAPI is running on Render!"}
178
+
179
+ @app.head("/")
180
+ async def root_head():
181
+ return {} # Empty response for HEAD requests
182
+
183
+ @app.post("/analyze")
184
+ async def analyze_image(file: UploadFile = File(...)):
185
+ """API endpoint to analyze food from an image and return nutritional info."""
186
+ # Periodically clean expired cache entries
187
+ clean_expired_cache_entries()
188
+
189
+ try:
190
+ # Process the image
191
+ contents = await file.read()
192
+ image = Image.open(io.BytesIO(contents))
193
+ detected_foods = detect_food(image)
194
+
195
+ if not detected_foods:
196
+ return {"message": "No food detected"}
197
+
198
+ # Get nutrition info (from cache if available)
199
+ raw_nutrition_info = {food: get_food_info(food) for food in detected_foods}
200
+ parsed_nutrition_info = parse_nutrition_response(raw_nutrition_info)
201
+
202
+ # Add cache statistics to the response
203
+ cache_stats = {
204
+ "cache_size": len(nutrition_cache),
205
+ "cache_hits": sum(1 for food in detected_foods if food in nutrition_cache),
206
+ "cache_misses": sum(1 for food in detected_foods if food not in nutrition_cache),
207
+ "last_cache_update": datetime.fromtimestamp(
208
+ max([entry['timestamp'] for entry in nutrition_cache.values()])
209
+ if nutrition_cache else time.time()
210
+ ).isoformat()
211
+ }
212
+
213
+ return {
214
+ "detected_foods": detected_foods,
215
+ "nutrition_info": parsed_nutrition_info,
216
+ "cache_stats": cache_stats
217
+ }
218
+ except Exception as e:
219
+ return {"error": f"An error occurred: {str(e)}"}
220
+
221
+
222
+ # Add endpoints to manage the cache
223
+ @app.get("/cache/stats")
224
+ async def get_cache_stats():
225
+ """Get statistics about the nutrition cache."""
226
+ if not nutrition_cache:
227
+ return {"message": "Cache is empty"}
228
+
229
+ return {
230
+ "cache_size": len(nutrition_cache),
231
+ "foods_cached": list(nutrition_cache.keys()),
232
+ "oldest_entry": datetime.fromtimestamp(
233
+ min([entry['timestamp'] for entry in nutrition_cache.values()])
234
+ ).isoformat(),
235
+ "newest_entry": datetime.fromtimestamp(
236
+ max([entry['timestamp'] for entry in nutrition_cache.values()])
237
+ ).isoformat()
238
+ }
239
+
240
+
241
+ @app.delete("/cache/clear")
242
+ async def clear_cache():
243
+ """Clear the nutrition cache."""
244
+ global nutrition_cache
245
+ nutrition_cache = {}
246
+ save_cache(nutrition_cache)
247
+ return {"message": "Cache cleared successfully"}
248
+
249
+
250
+ @app.delete("/cache/item/{food_item}")
251
+ async def delete_cache_item(food_item: str):
252
+ """Delete a specific food item from the cache."""
253
+ if food_item in nutrition_cache:
254
+ del nutrition_cache[food_item]
255
+ save_cache(nutrition_cache)
256
+ return {"message": f"Removed {food_item} from cache"}
257
+ return {"message": f"{food_item} not found in cache"}
258
+
259
+
260
+ # Add health check endpoint for production deployments
261
+ @app.get("/health")
262
+ async def health_check():
263
+ """Health check endpoint for monitoring."""
264
+ return {
265
+ "status": "healthy",
266
+ "cache_size": len(nutrition_cache),
267
+ "yolo_model_loaded": model is not None,
268
+ "gemini_api_configured": GOOGLE_API_KEY != ""
269
+ }
270
+
271
+ import os
272
+
273
+ if __name__ == "__main__":
274
+ port = int(os.getenv("PORT", 8000)) # Render assigns PORT dynamically
275
+ uvicorn.run(app, host="0.0.0.0", port=port)