Korapati commited on
Commit
f70ed8e
ยท
verified ยท
1 Parent(s): d2882d0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +407 -407
app.py CHANGED
@@ -1,408 +1,408 @@
1
- # =============================================================================
2
- # ๐Ÿฅ— NutriVision - app.py
3
- # Vision Models: nateraw/food | prithivMLmods/Indian-Western-Food-34 | Custom 80-class
4
- # Text AI: OpenRouter API
5
- # =============================================================================
6
-
7
- from flask import Flask, render_template, request, jsonify
8
- from transformers import pipeline, AutoImageProcessor, AutoModelForImageClassification
9
- from PIL import Image
10
- import torch
11
- import functools
12
- import os
13
- import re
14
- import requests
15
- import json
16
- from werkzeug.utils import secure_filename
17
-
18
- app = Flask(__name__)
19
- app.config["UPLOAD_FOLDER"] = "static/uploads"
20
- app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024
21
- app.config["ALLOWED_EXTENSIONS"] = {'png', 'jpg', 'jpeg', 'webp'}
22
- os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True)
23
-
24
- # ============================================================
25
- # ๐Ÿ”‘ OPENROUTER CONFIG
26
- # ============================================================
27
- OPENROUTER_API_KEY = "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"
28
- OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
29
-
30
- CANDIDATE_MODELS = [
31
- "openai/gpt-4o-mini",
32
- "mistralai/mistral-7b-instruct:free",
33
- "google/gemma-2-9b-it:free",
34
- ]
35
-
36
- # ================================
37
- # ๐Ÿ”น UTILITIES
38
- # ================================
39
- def allowed_file(filename):
40
- return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config["ALLOWED_EXTENSIONS"]
41
-
42
- def calculate_bmi(height, weight):
43
- h = height / 100
44
- return round(weight / (h ** 2), 1)
45
-
46
- def get_bmi_category(bmi):
47
- if bmi < 18.5: return "Underweight"
48
- elif bmi < 25.0: return "Normal weight"
49
- elif bmi < 30.0: return "Overweight"
50
- else: return "Obese"
51
-
52
- def call_openrouter(prompt, max_tokens=1000):
53
- headers = {
54
- "Authorization": f"Bearer {OPENROUTER_API_KEY}",
55
- "Content-Type": "application/json",
56
- "HTTP-Referer": "https://nutrivision.ai",
57
- "X-Title": "NutriVision",
58
- }
59
- for model in CANDIDATE_MODELS:
60
- print(f" ๐Ÿ”ท Trying model: {model}")
61
- try:
62
- payload = {
63
- "model": model,
64
- "messages": [{"role": "user", "content": prompt}],
65
- "max_tokens": max_tokens,
66
- "temperature": 0.4,
67
- }
68
- resp = requests.post(OPENROUTER_URL, headers=headers, json=payload, timeout=45)
69
- print(f" HTTP {resp.status_code}")
70
- if resp.status_code != 200:
71
- print(f" โŒ Error: {resp.text[:300]}")
72
- continue
73
- content = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "").strip()
74
- if not content:
75
- print(f" โŒ Empty content from {model}")
76
- continue
77
- print(f" โœ… Got {len(content)} chars from {model}")
78
- return content, model
79
- except requests.exceptions.Timeout:
80
- print(f" โŒ Timeout on {model}")
81
- except Exception as e:
82
- print(f" โŒ Exception on {model}: {e}")
83
- print(" โŒ All models failed")
84
- return None, None
85
-
86
- # ================================
87
- # ๐Ÿ”น MODEL 1: nateraw/food
88
- # ================================
89
- @functools.lru_cache(maxsize=1)
90
- def load_food101_classifier():
91
- print("๐Ÿ”„ [Model 1] Loading nateraw/food โ€ฆ")
92
- return pipeline("image-classification", model="nateraw/food",
93
- device=0 if torch.cuda.is_available() else -1)
94
-
95
- # ================================
96
- # ๐Ÿ”น MODEL 2: Indian-Western-Food-34
97
- # ================================
98
- @functools.lru_cache(maxsize=1)
99
- def load_indian_western_classifier():
100
- print("๐Ÿ”„ [Model 2] Loading prithivMLmods/Indian-Western-Food-34 โ€ฆ")
101
- return pipeline("image-classification",
102
- model="prithivMLmods/Indian-Western-Food-34",
103
- device=0 if torch.cuda.is_available() else -1)
104
-
105
- # ================================
106
- # ๐Ÿ”น MODEL 3: Custom Fine-Tuned
107
- # ================================
108
- @functools.lru_cache(maxsize=1)
109
- def load_custom_model():
110
- MODEL_PATH = "C:/Users/nages/Documents/nutrivision-ai/final_model"
111
- print("๐Ÿ”„ [Model 3] Loading custom fine-tuned model โ€ฆ")
112
- try:
113
- proc = AutoImageProcessor.from_pretrained(MODEL_PATH)
114
- mdl = AutoModelForImageClassification.from_pretrained(
115
- MODEL_PATH,
116
- torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32
117
- )
118
- mdl.eval()
119
- if torch.cuda.is_available():
120
- mdl = mdl.cuda()
121
- print("โœ… [Model 3] Loaded!")
122
- return proc, mdl
123
- except Exception as e:
124
- print(f"โš ๏ธ [Model 3] Failed: {e}")
125
- return None, None
126
-
127
- # ================================
128
- # ๐Ÿ”น 3-MODEL ENSEMBLE
129
- # ================================
130
- def detect_food(image_path):
131
- image = Image.open(image_path).convert('RGB')
132
- candidates = []
133
-
134
- try:
135
- preds = load_food101_classifier()(image, top_k=3)
136
- b = preds[0]
137
- candidates.append({"food": b['label'].replace('_',' ').title(),
138
- "confidence": b['score'], "source": "Food-101"})
139
- print(f" โ–ธ Model 1 {b['label']} {b['score']*100:.1f}%")
140
- except Exception as e:
141
- print(f" โ–ธ Model 1 error: {e}")
142
-
143
- try:
144
- preds = load_indian_western_classifier()(image, top_k=3)
145
- b = preds[0]
146
- candidates.append({"food": b['label'].replace('_',' ').title(),
147
- "confidence": b['score'], "source": "Indian-Western-34"})
148
- print(f" โ–ธ Model 2 {b['label']} {b['score']*100:.1f}%")
149
- except Exception as e:
150
- print(f" โ–ธ Model 2 error: {e}")
151
-
152
- try:
153
- proc, mdl = load_custom_model()
154
- if proc and mdl:
155
- inputs = proc(images=image, return_tensors="pt")
156
- if torch.cuda.is_available():
157
- inputs = {k: v.cuda() for k, v in inputs.items()}
158
- with torch.no_grad():
159
- logits = mdl(**inputs).logits
160
- pid = logits.argmax(-1).item()
161
- conf = torch.softmax(logits, dim=-1)[0][pid].item()
162
- lbl = mdl.config.id2label[pid]
163
- candidates.append({"food": lbl.replace('_',' ').title(),
164
- "confidence": conf, "source": "Custom-80"})
165
- print(f" โ–ธ Model 3 {lbl} {conf*100:.1f}%")
166
- except Exception as e:
167
- print(f" โ–ธ Model 3 error: {e}")
168
-
169
- if not candidates:
170
- return "Unknown Food", 0.0, "No model available"
171
-
172
- winner = max(candidates, key=lambda x: x["confidence"])
173
- print(f"โœ… Winner โ†’ {winner['food']} {winner['confidence']*100:.1f}% [{winner['source']}]")
174
- return winner["food"], winner["confidence"], winner["source"]
175
-
176
- # ================================
177
- # ๐Ÿ”น LLM: FULL NUTRITION REPORT
178
- # ================================
179
- def generate_full_report(food_name, age, gender, height, weight,
180
- bmi, bmi_category, condition, diet_pref):
181
- cond_str = condition if condition and condition.lower() != "none" else "None"
182
- print(f"\n๐Ÿ”ถ generate_full_report() โ†’ food={food_name}, condition={cond_str}, bmi={bmi_category}")
183
-
184
- prompt = f"""You are a certified nutritionist AI. Return ONLY a raw JSON object โ€” no markdown, no code fences, no explanation, no extra text whatsoever. Start your response with {{ and end with }}.
185
-
186
- You are analyzing: {food_name}
187
-
188
- User details:
189
- - Age: {age}, Gender: {gender}
190
- - Height: {height}cm, Weight: {weight}kg
191
- - BMI: {bmi} which is {bmi_category}
192
- - Diet: {diet_pref}
193
- - Health condition: {cond_str}
194
-
195
- Fill this JSON with REAL, SPECIFIC data for {food_name}. Every field must be specific to {food_name} โ€” never give generic values.
196
-
197
- {{
198
- "nutrition": {{
199
- "serving_size": "<typical serving size of {food_name}>",
200
- "calories": "<real calories of {food_name} per serving>",
201
- "protein": "<real protein of {food_name}>",
202
- "carbohydrates": "<real carbs of {food_name}>",
203
- "fat": "<real fat of {food_name}>",
204
- "fiber": "<real fiber of {food_name}>",
205
- "sugar": "<real sugar of {food_name}>",
206
- "sodium": "<real sodium of {food_name}>"
207
- }},
208
- "health_benefits": [
209
- "<benefit 1 specific to {food_name}>",
210
- "<benefit 2 specific to {food_name}>",
211
- "<benefit 3 specific to {food_name}>"
212
- ],
213
- "portion_advice": "<how much {food_name} should a {age}-year-old {gender} with {bmi_category} BMI and {cond_str} eat>",
214
- "health_context": "<specific explanation of how {food_name} affects {cond_str} โ€” mention key nutrients and why they matter for {cond_str}>",
215
- "alternatives": [
216
- {{"name": "<healthier alternative to {food_name}>", "reason": "<why better for {cond_str} and {bmi_category}>"}},
217
- {{"name": "<healthier alternative to {food_name}>", "reason": "<why better for {cond_str} and {bmi_category}>"}},
218
- {{"name": "<healthier alternative to {food_name}>", "reason": "<why better for {cond_str} and {bmi_category}>"}}
219
- ]
220
- }}"""
221
-
222
- raw, model_used = call_openrouter(prompt, max_tokens=1000)
223
- if not raw:
224
- print("โš ๏ธ All LLM calls failed โ†’ using fallback")
225
- return None
226
-
227
- print(f" Model used: {model_used}")
228
- print(f" Raw (first 400 chars): {raw[:400]}")
229
-
230
- try:
231
- clean = raw.strip()
232
- clean = re.sub(r"^```[a-zA-Z]*\n?", "", clean)
233
- clean = re.sub(r"\n?```$", "", clean.strip())
234
- m = re.search(r'\{.*\}', clean, re.DOTALL)
235
- if m:
236
- clean = m.group(0)
237
- parsed = json.loads(clean)
238
- print(f"โœ… JSON parsed OK โ€” calories={parsed.get('nutrition',{}).get('calories','?')}")
239
- return parsed
240
- except Exception as e:
241
- print(f"โš ๏ธ JSON parse error: {e}")
242
- print(f" Raw response: {raw[:600]}")
243
- return None
244
-
245
- # ================================
246
- # ๐Ÿ”น SHOPPING + DELIVERY URLS
247
- # ================================
248
- def get_shopping_urls(food_item):
249
- """
250
- Returns search links for grocery delivery + food delivery platforms.
251
- Uses each platform's native search URL format.
252
- """
253
- raw = food_item.strip()
254
- q_pct = raw.lower().replace(' ', '%20') # URL percent-encoded
255
- q_plus = raw.lower().replace(' ', '+') # + encoded (Google style)
256
- q_dash = raw.lower().replace(' ', '-') # dash-separated (Swiggy)
257
- q_zomato = raw.lower().replace(' ', '%20') # Zomato uses %20
258
-
259
- return [
260
- # โ”€โ”€ Grocery / delivery platforms โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
261
- {
262
- "platform": "BigBasket",
263
- "url": f"https://www.bigbasket.com/ps/?q={q_pct}",
264
- "emoji": "๐Ÿ›’",
265
- "category": "grocery"
266
- },
267
- {
268
- "platform": "Blinkit",
269
- "url": f"https://blinkit.com/s/?q={q_pct}",
270
- "emoji": "โšก",
271
- "category": "grocery"
272
- },
273
- {
274
- "platform": "Amazon",
275
- "url": f"https://www.amazon.in/s?k={q_plus}+food",
276
- "emoji": "๐Ÿ“ฆ",
277
- "category": "grocery"
278
- },
279
- {
280
- "platform": "Flipkart",
281
- "url": f"https://www.flipkart.com/search?q={q_pct}",
282
- "emoji": "๐Ÿ›๏ธ",
283
- "category": "grocery"
284
- },
285
- # โ”€โ”€ Food delivery platforms โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
286
- {
287
- "platform": "Swiggy",
288
- "url": f"https://www.swiggy.com/search?query={q_pct}",
289
- "emoji": "๐ŸŠ",
290
- "category": "delivery"
291
- },
292
- {
293
- "platform": "Zomato",
294
- "url": f"https://www.zomato.com/search?q={q_zomato}",
295
- "emoji": "๐Ÿ”ด",
296
- "category": "delivery"
297
- },
298
- ]
299
-
300
- # ================================
301
- # ๐Ÿ”น FALLBACK REPORT
302
- # ================================
303
- def fallback_report(food_name="this food"):
304
- return {
305
- "nutrition": {
306
- "serving_size": "1 standard serving (~150g)",
307
- "calories": "~250 kcal", "protein": "~8g",
308
- "carbohydrates": "~35g", "fat": "~10g",
309
- "fiber": "~3g", "sugar": "~5g", "sodium": "~200mg"
310
- },
311
- "health_benefits": [
312
- f"{food_name} provides essential macronutrients for daily energy.",
313
- "Contains dietary fiber supporting digestive health.",
314
- "Source of micronutrients important for body functions."
315
- ],
316
- "portion_advice": f"Consume 1 standard serving of {food_name} as part of a balanced diet.",
317
- "health_context": f"Consult a nutritionist for personalised advice about {food_name} and your health goals.",
318
- "alternatives": [
319
- {"name": "Steamed Vegetables", "reason": "Low calories, high fiber and nutrients"},
320
- {"name": "Grilled Chicken", "reason": "Lean protein, low in saturated fat"},
321
- {"name": "Fresh Fruit Bowl", "reason": "Natural sugars with vitamins and antioxidants"}
322
- ]
323
- }
324
-
325
- # ================================
326
- # ๐Ÿ”น ROUTES
327
- # ================================
328
- @app.route("/")
329
- def home():
330
- return render_template("home.html")
331
-
332
- @app.route("/analyzer")
333
- def analyzer():
334
- return render_template("index.html")
335
-
336
- @app.route("/about")
337
- def about():
338
- return render_template("about.html")
339
-
340
- @app.route("/analyze", methods=["POST"])
341
- def analyze():
342
- try:
343
- if 'image' not in request.files:
344
- return jsonify({"error": "No image uploaded"}), 400
345
- image_file = request.files['image']
346
- if not image_file.filename or not allowed_file(image_file.filename):
347
- return jsonify({"error": "Invalid file type. Use PNG, JPG, JPEG or WebP."}), 400
348
-
349
- age = request.form.get("age", "25")
350
- gender = request.form.get("gender", "Male")
351
- height = float(request.form.get("height", "170"))
352
- weight = float(request.form.get("weight", "70"))
353
- diet_pref = request.form.get("preference", "Vegetarian")
354
- condition = request.form.get("condition", "None")
355
-
356
- bmi = calculate_bmi(height, weight)
357
- bmi_category = get_bmi_category(bmi)
358
-
359
- filename = secure_filename(image_file.filename)
360
- img_path = os.path.join(app.config["UPLOAD_FOLDER"], filename)
361
- image_file.save(img_path)
362
-
363
- print("\n" + "="*55)
364
- print(f"๐Ÿ“ฅ REQUEST: {age}y {gender}, h={height} w={weight}, BMI={bmi} ({bmi_category})")
365
- print(f" condition={condition}, diet={diet_pref}")
366
-
367
- print("\nโ”โ”โ” 3-MODEL ENSEMBLE โ”โ”โ”")
368
- food_name, confidence, detection_source = detect_food(img_path)
369
-
370
- print("\nโ”โ”โ” LLM NUTRITION REPORT โ”โ”โ”")
371
- report = generate_full_report(
372
- food_name, age, gender, height, weight,
373
- bmi, bmi_category, condition, diet_pref
374
- )
375
-
376
- if report is None:
377
- print("โš ๏ธ Using FALLBACK")
378
- report = fallback_report(food_name)
379
-
380
- alternatives = [
381
- {"name": a["name"], "reason": a["reason"],
382
- "urls": get_shopping_urls(a["name"])}
383
- for a in report.get("alternatives", [])
384
- ]
385
-
386
- return jsonify({
387
- "food": food_name,
388
- "confidence": f"{confidence * 100:.1f}%",
389
- "detection_source": detection_source,
390
- "bmi": bmi,
391
- "bmi_category": bmi_category,
392
- "nutrition": report.get("nutrition", {}),
393
- "health_benefits": report.get("health_benefits", []),
394
- "portion_advice": report.get("portion_advice", "1 standard serving"),
395
- "health_context": report.get("health_context", ""),
396
- "alternatives": alternatives,
397
- })
398
-
399
- except Exception as e:
400
- import traceback; traceback.print_exc()
401
- return jsonify({"error": f"Analysis failed: {str(e)}"}), 500
402
-
403
- if __name__ == "__main__":
404
- print("๐Ÿš€ NutriVision startingโ€ฆ")
405
- print(f"๐ŸŽฎ GPU: {torch.cuda.is_available()}")
406
- print(f"๐Ÿ”‘ OpenRouter key: {OPENROUTER_API_KEY[:18]}...")
407
- print(f"๐Ÿค– Model priority: {CANDIDATE_MODELS}")
408
  app.run(host="0.0.0.0", port=7860)
 
1
+ # =============================================================================
2
+ # ๐Ÿฅ— NutriVision - app.py
3
+ # Vision Models: nateraw/food | prithivMLmods/Indian-Western-Food-34 | Custom 80-class
4
+ # Text AI: OpenRouter API
5
+ # =============================================================================
6
+
7
+ from flask import Flask, render_template, request, jsonify
8
+ from transformers import pipeline, AutoImageProcessor, AutoModelForImageClassification
9
+ from PIL import Image
10
+ import torch
11
+ import functools
12
+ import os
13
+ import re
14
+ import requests
15
+ import json
16
+ from werkzeug.utils import secure_filename
17
+
18
+ app = Flask(__name__)
19
+ app.config["UPLOAD_FOLDER"] = "static/uploads"
20
+ app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024
21
+ app.config["ALLOWED_EXTENSIONS"] = {'png', 'jpg', 'jpeg', 'webp'}
22
+ os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True)
23
+
24
+ # ============================================================
25
+ # ๐Ÿ”‘ OPENROUTER CONFIG
26
+ # ============================================================
27
+ OPENROUTER_API_KEY = "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"
28
+ OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
29
+
30
+ CANDIDATE_MODELS = [
31
+ "openai/gpt-4o-mini",
32
+ "mistralai/mistral-7b-instruct:free",
33
+ "google/gemma-2-9b-it:free",
34
+ ]
35
+
36
+ # ================================
37
+ # ๐Ÿ”น UTILITIES
38
+ # ================================
39
+ def allowed_file(filename):
40
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config["ALLOWED_EXTENSIONS"]
41
+
42
+ def calculate_bmi(height, weight):
43
+ h = height / 100
44
+ return round(weight / (h ** 2), 1)
45
+
46
+ def get_bmi_category(bmi):
47
+ if bmi < 18.5: return "Underweight"
48
+ elif bmi < 25.0: return "Normal weight"
49
+ elif bmi < 30.0: return "Overweight"
50
+ else: return "Obese"
51
+
52
+ def call_openrouter(prompt, max_tokens=1000):
53
+ headers = {
54
+ "Authorization": f"Bearer {OPENROUTER_API_KEY}",
55
+ "Content-Type": "application/json",
56
+ "HTTP-Referer": "https://nutrivision.ai",
57
+ "X-Title": "NutriVision",
58
+ }
59
+ for model in CANDIDATE_MODELS:
60
+ print(f" ๐Ÿ”ท Trying model: {model}")
61
+ try:
62
+ payload = {
63
+ "model": model,
64
+ "messages": [{"role": "user", "content": prompt}],
65
+ "max_tokens": max_tokens,
66
+ "temperature": 0.4,
67
+ }
68
+ resp = requests.post(OPENROUTER_URL, headers=headers, json=payload, timeout=45)
69
+ print(f" HTTP {resp.status_code}")
70
+ if resp.status_code != 200:
71
+ print(f" โŒ Error: {resp.text[:300]}")
72
+ continue
73
+ content = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "").strip()
74
+ if not content:
75
+ print(f" โŒ Empty content from {model}")
76
+ continue
77
+ print(f" โœ… Got {len(content)} chars from {model}")
78
+ return content, model
79
+ except requests.exceptions.Timeout:
80
+ print(f" โŒ Timeout on {model}")
81
+ except Exception as e:
82
+ print(f" โŒ Exception on {model}: {e}")
83
+ print(" โŒ All models failed")
84
+ return None, None
85
+
86
+ # ================================
87
+ # ๐Ÿ”น MODEL 1: nateraw/food
88
+ # ================================
89
+ @functools.lru_cache(maxsize=1)
90
+ def load_food101_classifier():
91
+ print("๐Ÿ”„ [Model 1] Loading nateraw/food โ€ฆ")
92
+ return pipeline("image-classification", model="nateraw/food",
93
+ device=0 if torch.cuda.is_available() else -1)
94
+
95
+ # ================================
96
+ # ๐Ÿ”น MODEL 2: Indian-Western-Food-34
97
+ # ================================
98
+ @functools.lru_cache(maxsize=1)
99
+ def load_indian_western_classifier():
100
+ print("๐Ÿ”„ [Model 2] Loading prithivMLmods/Indian-Western-Food-34 โ€ฆ")
101
+ return pipeline("image-classification",
102
+ model="prithivMLmods/Indian-Western-Food-34",
103
+ device=0 if torch.cuda.is_available() else -1)
104
+
105
+ # ================================
106
+ # ๐Ÿ”น MODEL 3: Custom Fine-Tuned
107
+ # ================================
108
+ @functools.lru_cache(maxsize=1)
109
+ def load_custom_model():
110
+ MODEL_PATH = "final_model"
111
+ print("๐Ÿ”„ [Model 3] Loading custom fine-tuned model โ€ฆ")
112
+ try:
113
+ proc = AutoImageProcessor.from_pretrained(MODEL_PATH)
114
+ mdl = AutoModelForImageClassification.from_pretrained(
115
+ MODEL_PATH,
116
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32
117
+ )
118
+ mdl.eval()
119
+ if torch.cuda.is_available():
120
+ mdl = mdl.cuda()
121
+ print("โœ… [Model 3] Loaded!")
122
+ return proc, mdl
123
+ except Exception as e:
124
+ print(f"โš ๏ธ [Model 3] Failed: {e}")
125
+ return None, None
126
+
127
+ # ================================
128
+ # ๐Ÿ”น 3-MODEL ENSEMBLE
129
+ # ================================
130
+ def detect_food(image_path):
131
+ image = Image.open(image_path).convert('RGB')
132
+ candidates = []
133
+
134
+ try:
135
+ preds = load_food101_classifier()(image, top_k=3)
136
+ b = preds[0]
137
+ candidates.append({"food": b['label'].replace('_',' ').title(),
138
+ "confidence": b['score'], "source": "Food-101"})
139
+ print(f" โ–ธ Model 1 {b['label']} {b['score']*100:.1f}%")
140
+ except Exception as e:
141
+ print(f" โ–ธ Model 1 error: {e}")
142
+
143
+ try:
144
+ preds = load_indian_western_classifier()(image, top_k=3)
145
+ b = preds[0]
146
+ candidates.append({"food": b['label'].replace('_',' ').title(),
147
+ "confidence": b['score'], "source": "Indian-Western-34"})
148
+ print(f" โ–ธ Model 2 {b['label']} {b['score']*100:.1f}%")
149
+ except Exception as e:
150
+ print(f" โ–ธ Model 2 error: {e}")
151
+
152
+ try:
153
+ proc, mdl = load_custom_model()
154
+ if proc and mdl:
155
+ inputs = proc(images=image, return_tensors="pt")
156
+ if torch.cuda.is_available():
157
+ inputs = {k: v.cuda() for k, v in inputs.items()}
158
+ with torch.no_grad():
159
+ logits = mdl(**inputs).logits
160
+ pid = logits.argmax(-1).item()
161
+ conf = torch.softmax(logits, dim=-1)[0][pid].item()
162
+ lbl = mdl.config.id2label[pid]
163
+ candidates.append({"food": lbl.replace('_',' ').title(),
164
+ "confidence": conf, "source": "Custom-80"})
165
+ print(f" โ–ธ Model 3 {lbl} {conf*100:.1f}%")
166
+ except Exception as e:
167
+ print(f" โ–ธ Model 3 error: {e}")
168
+
169
+ if not candidates:
170
+ return "Unknown Food", 0.0, "No model available"
171
+
172
+ winner = max(candidates, key=lambda x: x["confidence"])
173
+ print(f"โœ… Winner โ†’ {winner['food']} {winner['confidence']*100:.1f}% [{winner['source']}]")
174
+ return winner["food"], winner["confidence"], winner["source"]
175
+
176
+ # ================================
177
+ # ๐Ÿ”น LLM: FULL NUTRITION REPORT
178
+ # ================================
179
+ def generate_full_report(food_name, age, gender, height, weight,
180
+ bmi, bmi_category, condition, diet_pref):
181
+ cond_str = condition if condition and condition.lower() != "none" else "None"
182
+ print(f"\n๐Ÿ”ถ generate_full_report() โ†’ food={food_name}, condition={cond_str}, bmi={bmi_category}")
183
+
184
+ prompt = f"""You are a certified nutritionist AI. Return ONLY a raw JSON object โ€” no markdown, no code fences, no explanation, no extra text whatsoever. Start your response with {{ and end with }}.
185
+
186
+ You are analyzing: {food_name}
187
+
188
+ User details:
189
+ - Age: {age}, Gender: {gender}
190
+ - Height: {height}cm, Weight: {weight}kg
191
+ - BMI: {bmi} which is {bmi_category}
192
+ - Diet: {diet_pref}
193
+ - Health condition: {cond_str}
194
+
195
+ Fill this JSON with REAL, SPECIFIC data for {food_name}. Every field must be specific to {food_name} โ€” never give generic values.
196
+
197
+ {{
198
+ "nutrition": {{
199
+ "serving_size": "<typical serving size of {food_name}>",
200
+ "calories": "<real calories of {food_name} per serving>",
201
+ "protein": "<real protein of {food_name}>",
202
+ "carbohydrates": "<real carbs of {food_name}>",
203
+ "fat": "<real fat of {food_name}>",
204
+ "fiber": "<real fiber of {food_name}>",
205
+ "sugar": "<real sugar of {food_name}>",
206
+ "sodium": "<real sodium of {food_name}>"
207
+ }},
208
+ "health_benefits": [
209
+ "<benefit 1 specific to {food_name}>",
210
+ "<benefit 2 specific to {food_name}>",
211
+ "<benefit 3 specific to {food_name}>"
212
+ ],
213
+ "portion_advice": "<how much {food_name} should a {age}-year-old {gender} with {bmi_category} BMI and {cond_str} eat>",
214
+ "health_context": "<specific explanation of how {food_name} affects {cond_str} โ€” mention key nutrients and why they matter for {cond_str}>",
215
+ "alternatives": [
216
+ {{"name": "<healthier alternative to {food_name}>", "reason": "<why better for {cond_str} and {bmi_category}>"}},
217
+ {{"name": "<healthier alternative to {food_name}>", "reason": "<why better for {cond_str} and {bmi_category}>"}},
218
+ {{"name": "<healthier alternative to {food_name}>", "reason": "<why better for {cond_str} and {bmi_category}>"}}
219
+ ]
220
+ }}"""
221
+
222
+ raw, model_used = call_openrouter(prompt, max_tokens=1000)
223
+ if not raw:
224
+ print("โš ๏ธ All LLM calls failed โ†’ using fallback")
225
+ return None
226
+
227
+ print(f" Model used: {model_used}")
228
+ print(f" Raw (first 400 chars): {raw[:400]}")
229
+
230
+ try:
231
+ clean = raw.strip()
232
+ clean = re.sub(r"^```[a-zA-Z]*\n?", "", clean)
233
+ clean = re.sub(r"\n?```$", "", clean.strip())
234
+ m = re.search(r'\{.*\}', clean, re.DOTALL)
235
+ if m:
236
+ clean = m.group(0)
237
+ parsed = json.loads(clean)
238
+ print(f"โœ… JSON parsed OK โ€” calories={parsed.get('nutrition',{}).get('calories','?')}")
239
+ return parsed
240
+ except Exception as e:
241
+ print(f"โš ๏ธ JSON parse error: {e}")
242
+ print(f" Raw response: {raw[:600]}")
243
+ return None
244
+
245
+ # ================================
246
+ # ๐Ÿ”น SHOPPING + DELIVERY URLS
247
+ # ================================
248
+ def get_shopping_urls(food_item):
249
+ """
250
+ Returns search links for grocery delivery + food delivery platforms.
251
+ Uses each platform's native search URL format.
252
+ """
253
+ raw = food_item.strip()
254
+ q_pct = raw.lower().replace(' ', '%20') # URL percent-encoded
255
+ q_plus = raw.lower().replace(' ', '+') # + encoded (Google style)
256
+ q_dash = raw.lower().replace(' ', '-') # dash-separated (Swiggy)
257
+ q_zomato = raw.lower().replace(' ', '%20') # Zomato uses %20
258
+
259
+ return [
260
+ # โ”€โ”€ Grocery / delivery platforms โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
261
+ {
262
+ "platform": "BigBasket",
263
+ "url": f"https://www.bigbasket.com/ps/?q={q_pct}",
264
+ "emoji": "๐Ÿ›’",
265
+ "category": "grocery"
266
+ },
267
+ {
268
+ "platform": "Blinkit",
269
+ "url": f"https://blinkit.com/s/?q={q_pct}",
270
+ "emoji": "โšก",
271
+ "category": "grocery"
272
+ },
273
+ {
274
+ "platform": "Amazon",
275
+ "url": f"https://www.amazon.in/s?k={q_plus}+food",
276
+ "emoji": "๐Ÿ“ฆ",
277
+ "category": "grocery"
278
+ },
279
+ {
280
+ "platform": "Flipkart",
281
+ "url": f"https://www.flipkart.com/search?q={q_pct}",
282
+ "emoji": "๐Ÿ›๏ธ",
283
+ "category": "grocery"
284
+ },
285
+ # โ”€โ”€ Food delivery platforms โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
286
+ {
287
+ "platform": "Swiggy",
288
+ "url": f"https://www.swiggy.com/search?query={q_pct}",
289
+ "emoji": "๐ŸŠ",
290
+ "category": "delivery"
291
+ },
292
+ {
293
+ "platform": "Zomato",
294
+ "url": f"https://www.zomato.com/search?q={q_zomato}",
295
+ "emoji": "๐Ÿ”ด",
296
+ "category": "delivery"
297
+ },
298
+ ]
299
+
300
+ # ================================
301
+ # ๐Ÿ”น FALLBACK REPORT
302
+ # ================================
303
+ def fallback_report(food_name="this food"):
304
+ return {
305
+ "nutrition": {
306
+ "serving_size": "1 standard serving (~150g)",
307
+ "calories": "~250 kcal", "protein": "~8g",
308
+ "carbohydrates": "~35g", "fat": "~10g",
309
+ "fiber": "~3g", "sugar": "~5g", "sodium": "~200mg"
310
+ },
311
+ "health_benefits": [
312
+ f"{food_name} provides essential macronutrients for daily energy.",
313
+ "Contains dietary fiber supporting digestive health.",
314
+ "Source of micronutrients important for body functions."
315
+ ],
316
+ "portion_advice": f"Consume 1 standard serving of {food_name} as part of a balanced diet.",
317
+ "health_context": f"Consult a nutritionist for personalised advice about {food_name} and your health goals.",
318
+ "alternatives": [
319
+ {"name": "Steamed Vegetables", "reason": "Low calories, high fiber and nutrients"},
320
+ {"name": "Grilled Chicken", "reason": "Lean protein, low in saturated fat"},
321
+ {"name": "Fresh Fruit Bowl", "reason": "Natural sugars with vitamins and antioxidants"}
322
+ ]
323
+ }
324
+
325
+ # ================================
326
+ # ๐Ÿ”น ROUTES
327
+ # ================================
328
+ @app.route("/")
329
+ def home():
330
+ return render_template("home.html")
331
+
332
+ @app.route("/analyzer")
333
+ def analyzer():
334
+ return render_template("index.html")
335
+
336
+ @app.route("/about")
337
+ def about():
338
+ return render_template("about.html")
339
+
340
+ @app.route("/analyze", methods=["POST"])
341
+ def analyze():
342
+ try:
343
+ if 'image' not in request.files:
344
+ return jsonify({"error": "No image uploaded"}), 400
345
+ image_file = request.files['image']
346
+ if not image_file.filename or not allowed_file(image_file.filename):
347
+ return jsonify({"error": "Invalid file type. Use PNG, JPG, JPEG or WebP."}), 400
348
+
349
+ age = request.form.get("age", "25")
350
+ gender = request.form.get("gender", "Male")
351
+ height = float(request.form.get("height", "170"))
352
+ weight = float(request.form.get("weight", "70"))
353
+ diet_pref = request.form.get("preference", "Vegetarian")
354
+ condition = request.form.get("condition", "None")
355
+
356
+ bmi = calculate_bmi(height, weight)
357
+ bmi_category = get_bmi_category(bmi)
358
+
359
+ filename = secure_filename(image_file.filename)
360
+ img_path = os.path.join(app.config["UPLOAD_FOLDER"], filename)
361
+ image_file.save(img_path)
362
+
363
+ print("\n" + "="*55)
364
+ print(f"๐Ÿ“ฅ REQUEST: {age}y {gender}, h={height} w={weight}, BMI={bmi} ({bmi_category})")
365
+ print(f" condition={condition}, diet={diet_pref}")
366
+
367
+ print("\nโ”โ”โ” 3-MODEL ENSEMBLE โ”โ”โ”")
368
+ food_name, confidence, detection_source = detect_food(img_path)
369
+
370
+ print("\nโ”โ”โ” LLM NUTRITION REPORT โ”โ”โ”")
371
+ report = generate_full_report(
372
+ food_name, age, gender, height, weight,
373
+ bmi, bmi_category, condition, diet_pref
374
+ )
375
+
376
+ if report is None:
377
+ print("โš ๏ธ Using FALLBACK")
378
+ report = fallback_report(food_name)
379
+
380
+ alternatives = [
381
+ {"name": a["name"], "reason": a["reason"],
382
+ "urls": get_shopping_urls(a["name"])}
383
+ for a in report.get("alternatives", [])
384
+ ]
385
+
386
+ return jsonify({
387
+ "food": food_name,
388
+ "confidence": f"{confidence * 100:.1f}%",
389
+ "detection_source": detection_source,
390
+ "bmi": bmi,
391
+ "bmi_category": bmi_category,
392
+ "nutrition": report.get("nutrition", {}),
393
+ "health_benefits": report.get("health_benefits", []),
394
+ "portion_advice": report.get("portion_advice", "1 standard serving"),
395
+ "health_context": report.get("health_context", ""),
396
+ "alternatives": alternatives,
397
+ })
398
+
399
+ except Exception as e:
400
+ import traceback; traceback.print_exc()
401
+ return jsonify({"error": f"Analysis failed: {str(e)}"}), 500
402
+
403
+ if __name__ == "__main__":
404
+ print("๐Ÿš€ NutriVision startingโ€ฆ")
405
+ print(f"๐ŸŽฎ GPU: {torch.cuda.is_available()}")
406
+ print(f"๐Ÿ”‘ OpenRouter key: {OPENROUTER_API_KEY[:18]}...")
407
+ print(f"๐Ÿค– Model priority: {CANDIDATE_MODELS}")
408
  app.run(host="0.0.0.0", port=7860)