# ============================================================================= # ๐Ÿฅ— NutriVision - app.py # Vision Models: nateraw/food | prithivMLmods/Indian-Western-Food-34 | Custom 80-class # Text AI: OpenRouter API # ============================================================================= from flask import Flask, render_template, request, jsonify from transformers import pipeline, AutoImageProcessor, AutoModelForImageClassification from PIL import Image import torch import functools import os import re import requests import json from werkzeug.utils import secure_filename app = Flask(__name__) app.config["UPLOAD_FOLDER"] = "static/uploads" app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 app.config["ALLOWED_EXTENSIONS"] = {'png', 'jpg', 'jpeg', 'webp'} os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True) # ============================================================ # ๐Ÿ”‘ OPENROUTER CONFIG # ============================================================ OPENROUTER_API_KEY = "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" OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions" CANDIDATE_MODELS = [ "openai/gpt-4o-mini", "mistralai/mistral-7b-instruct:free", "google/gemma-2-9b-it:free", ] # ================================ # ๐Ÿ”น UTILITIES # ================================ def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config["ALLOWED_EXTENSIONS"] def calculate_bmi(height, weight): h = height / 100 return round(weight / (h ** 2), 1) def get_bmi_category(bmi): if bmi < 18.5: return "Underweight" elif bmi < 25.0: return "Normal weight" elif bmi < 30.0: return "Overweight" else: return "Obese" def call_openrouter(prompt, max_tokens=1000): headers = { "Authorization": f"Bearer {OPENROUTER_API_KEY}", "Content-Type": "application/json", "HTTP-Referer": "https://nutrivision.ai", "X-Title": "NutriVision", } for model in CANDIDATE_MODELS: print(f" ๐Ÿ”ท Trying model: {model}") try: payload = { "model": model, "messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens, "temperature": 0.4, } resp = requests.post(OPENROUTER_URL, headers=headers, json=payload, timeout=45) print(f" HTTP {resp.status_code}") if resp.status_code != 200: print(f" โŒ Error: {resp.text[:300]}") continue content = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "").strip() if not content: print(f" โŒ Empty content from {model}") continue print(f" โœ… Got {len(content)} chars from {model}") return content, model except requests.exceptions.Timeout: print(f" โŒ Timeout on {model}") except Exception as e: print(f" โŒ Exception on {model}: {e}") print(" โŒ All models failed") return None, None # ================================ # ๐Ÿ”น MODEL 1: nateraw/food # ================================ @functools.lru_cache(maxsize=1) def load_food101_classifier(): print("๐Ÿ”„ [Model 1] Loading nateraw/food โ€ฆ") return pipeline("image-classification", model="nateraw/food", device=0 if torch.cuda.is_available() else -1) # ================================ # ๐Ÿ”น MODEL 2: Indian-Western-Food-34 # ================================ @functools.lru_cache(maxsize=1) def load_indian_western_classifier(): print("๐Ÿ”„ [Model 2] Loading prithivMLmods/Indian-Western-Food-34 โ€ฆ") return pipeline("image-classification", model="prithivMLmods/Indian-Western-Food-34", device=0 if torch.cuda.is_available() else -1) # ================================ # ๐Ÿ”น MODEL 3: Custom Fine-Tuned # ================================ @functools.lru_cache(maxsize=1) def load_custom_model(): MODEL_PATH = "final_model" print("๐Ÿ”„ [Model 3] Loading custom fine-tuned model โ€ฆ") try: proc = AutoImageProcessor.from_pretrained(MODEL_PATH) mdl = AutoModelForImageClassification.from_pretrained( MODEL_PATH, torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32 ) mdl.eval() if torch.cuda.is_available(): mdl = mdl.cuda() print("โœ… [Model 3] Loaded!") return proc, mdl except Exception as e: print(f"โš ๏ธ [Model 3] Failed: {e}") return None, None # ================================ # ๐Ÿ”น 3-MODEL ENSEMBLE # ================================ def detect_food(image_path): image = Image.open(image_path).convert('RGB') candidates = [] try: preds = load_food101_classifier()(image, top_k=3) b = preds[0] candidates.append({"food": b['label'].replace('_',' ').title(), "confidence": b['score'], "source": "Food-101"}) print(f" โ–ธ Model 1 {b['label']} {b['score']*100:.1f}%") except Exception as e: print(f" โ–ธ Model 1 error: {e}") try: preds = load_indian_western_classifier()(image, top_k=3) b = preds[0] candidates.append({"food": b['label'].replace('_',' ').title(), "confidence": b['score'], "source": "Indian-Western-34"}) print(f" โ–ธ Model 2 {b['label']} {b['score']*100:.1f}%") except Exception as e: print(f" โ–ธ Model 2 error: {e}") try: proc, mdl = load_custom_model() if proc and mdl: inputs = proc(images=image, return_tensors="pt") if torch.cuda.is_available(): inputs = {k: v.cuda() for k, v in inputs.items()} with torch.no_grad(): logits = mdl(**inputs).logits pid = logits.argmax(-1).item() conf = torch.softmax(logits, dim=-1)[0][pid].item() lbl = mdl.config.id2label[pid] candidates.append({"food": lbl.replace('_',' ').title(), "confidence": conf, "source": "Custom-80"}) print(f" โ–ธ Model 3 {lbl} {conf*100:.1f}%") except Exception as e: print(f" โ–ธ Model 3 error: {e}") if not candidates: return "Unknown Food", 0.0, "No model available" winner = max(candidates, key=lambda x: x["confidence"]) print(f"โœ… Winner โ†’ {winner['food']} {winner['confidence']*100:.1f}% [{winner['source']}]") return winner["food"], winner["confidence"], winner["source"] # ================================ # ๐Ÿ”น LLM: FULL NUTRITION REPORT # ================================ def generate_full_report(food_name, age, gender, height, weight, bmi, bmi_category, condition, diet_pref): cond_str = condition if condition and condition.lower() != "none" else "None" print(f"\n๐Ÿ”ถ generate_full_report() โ†’ food={food_name}, condition={cond_str}, bmi={bmi_category}") 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 }}. You are analyzing: {food_name} User details: - Age: {age}, Gender: {gender} - Height: {height}cm, Weight: {weight}kg - BMI: {bmi} which is {bmi_category} - Diet: {diet_pref} - Health condition: {cond_str} Fill this JSON with REAL, SPECIFIC data for {food_name}. Every field must be specific to {food_name} โ€” never give generic values. {{ "nutrition": {{ "serving_size": "", "calories": "", "protein": "", "carbohydrates": "", "fat": "", "fiber": "", "sugar": "", "sodium": "" }}, "health_benefits": [ "", "", "" ], "portion_advice": "", "health_context": "", "alternatives": [ {{"name": "", "reason": ""}}, {{"name": "", "reason": ""}}, {{"name": "", "reason": ""}} ] }}""" raw, model_used = call_openrouter(prompt, max_tokens=1000) if not raw: print("โš ๏ธ All LLM calls failed โ†’ using fallback") return None print(f" Model used: {model_used}") print(f" Raw (first 400 chars): {raw[:400]}") try: clean = raw.strip() clean = re.sub(r"^```[a-zA-Z]*\n?", "", clean) clean = re.sub(r"\n?```$", "", clean.strip()) m = re.search(r'\{.*\}', clean, re.DOTALL) if m: clean = m.group(0) parsed = json.loads(clean) print(f"โœ… JSON parsed OK โ€” calories={parsed.get('nutrition',{}).get('calories','?')}") return parsed except Exception as e: print(f"โš ๏ธ JSON parse error: {e}") print(f" Raw response: {raw[:600]}") return None # ================================ # ๐Ÿ”น SHOPPING + DELIVERY URLS # ================================ def get_shopping_urls(food_item): """ Returns search links for grocery delivery + food delivery platforms. Uses each platform's native search URL format. """ raw = food_item.strip() q_pct = raw.lower().replace(' ', '%20') # URL percent-encoded q_plus = raw.lower().replace(' ', '+') # + encoded (Google style) q_dash = raw.lower().replace(' ', '-') # dash-separated (Swiggy) q_zomato = raw.lower().replace(' ', '%20') # Zomato uses %20 return [ # โ”€โ”€ Grocery / delivery platforms โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ { "platform": "BigBasket", "url": f"https://www.bigbasket.com/ps/?q={q_pct}", "emoji": "๐Ÿ›’", "category": "grocery" }, { "platform": "Blinkit", "url": f"https://blinkit.com/s/?q={q_pct}", "emoji": "โšก", "category": "grocery" }, { "platform": "Amazon", "url": f"https://www.amazon.in/s?k={q_plus}+food", "emoji": "๐Ÿ“ฆ", "category": "grocery" }, { "platform": "Flipkart", "url": f"https://www.flipkart.com/search?q={q_pct}", "emoji": "๐Ÿ›๏ธ", "category": "grocery" }, # โ”€โ”€ Food delivery platforms โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ { "platform": "Swiggy", "url": f"https://www.swiggy.com/search?query={q_pct}", "emoji": "๐ŸŠ", "category": "delivery" }, { "platform": "Zomato", "url": f"https://www.zomato.com/search?q={q_zomato}", "emoji": "๐Ÿ”ด", "category": "delivery" }, ] # ================================ # ๐Ÿ”น FALLBACK REPORT # ================================ def fallback_report(food_name="this food"): return { "nutrition": { "serving_size": "1 standard serving (~150g)", "calories": "~250 kcal", "protein": "~8g", "carbohydrates": "~35g", "fat": "~10g", "fiber": "~3g", "sugar": "~5g", "sodium": "~200mg" }, "health_benefits": [ f"{food_name} provides essential macronutrients for daily energy.", "Contains dietary fiber supporting digestive health.", "Source of micronutrients important for body functions." ], "portion_advice": f"Consume 1 standard serving of {food_name} as part of a balanced diet.", "health_context": f"Consult a nutritionist for personalised advice about {food_name} and your health goals.", "alternatives": [ {"name": "Steamed Vegetables", "reason": "Low calories, high fiber and nutrients"}, {"name": "Grilled Chicken", "reason": "Lean protein, low in saturated fat"}, {"name": "Fresh Fruit Bowl", "reason": "Natural sugars with vitamins and antioxidants"} ] } # ================================ # ๐Ÿ”น ROUTES # ================================ @app.route("/") def home(): return render_template("home.html") @app.route("/analyzer") def analyzer(): return render_template("index.html") @app.route("/about") def about(): return render_template("about.html") @app.route("/analyze", methods=["POST"]) def analyze(): try: if 'image' not in request.files: return jsonify({"error": "No image uploaded"}), 400 image_file = request.files['image'] if not image_file.filename or not allowed_file(image_file.filename): return jsonify({"error": "Invalid file type. Use PNG, JPG, JPEG or WebP."}), 400 age = request.form.get("age", "25") gender = request.form.get("gender", "Male") height = float(request.form.get("height", "170")) weight = float(request.form.get("weight", "70")) diet_pref = request.form.get("preference", "Vegetarian") condition = request.form.get("condition", "None") bmi = calculate_bmi(height, weight) bmi_category = get_bmi_category(bmi) filename = secure_filename(image_file.filename) img_path = os.path.join(app.config["UPLOAD_FOLDER"], filename) image_file.save(img_path) print("\n" + "="*55) print(f"๐Ÿ“ฅ REQUEST: {age}y {gender}, h={height} w={weight}, BMI={bmi} ({bmi_category})") print(f" condition={condition}, diet={diet_pref}") print("\nโ”โ”โ” 3-MODEL ENSEMBLE โ”โ”โ”") food_name, confidence, detection_source = detect_food(img_path) print("\nโ”โ”โ” LLM NUTRITION REPORT โ”โ”โ”") report = generate_full_report( food_name, age, gender, height, weight, bmi, bmi_category, condition, diet_pref ) if report is None: print("โš ๏ธ Using FALLBACK") report = fallback_report(food_name) alternatives = [ {"name": a["name"], "reason": a["reason"], "urls": get_shopping_urls(a["name"])} for a in report.get("alternatives", []) ] return jsonify({ "food": food_name, "confidence": f"{confidence * 100:.1f}%", "detection_source": detection_source, "bmi": bmi, "bmi_category": bmi_category, "nutrition": report.get("nutrition", {}), "health_benefits": report.get("health_benefits", []), "portion_advice": report.get("portion_advice", "1 standard serving"), "health_context": report.get("health_context", ""), "alternatives": alternatives, }) except Exception as e: import traceback; traceback.print_exc() return jsonify({"error": f"Analysis failed: {str(e)}"}), 500 if __name__ == "__main__": print("๐Ÿš€ NutriVision startingโ€ฆ") print(f"๐ŸŽฎ GPU: {torch.cuda.is_available()}") print(f"๐Ÿ”‘ OpenRouter key: {OPENROUTER_API_KEY[:18]}...") print(f"๐Ÿค– Model priority: {CANDIDATE_MODELS}") app.run(host="0.0.0.0", port=7860)