Korapati commited on
Commit
7910ffc
ยท
verified ยท
1 Parent(s): e2d065b

Upload 3 files

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