Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import os | |
| import json | |
| import requests | |
| from PIL import Image | |
| from difflib import get_close_matches | |
| from functools import lru_cache | |
| from collections import Counter | |
| # ===================== | |
| # ENV | |
| # ===================== | |
| ROBOFLOW_API_KEY = os.getenv("ROBOFLOW_API_KEY") | |
| # ===================== | |
| # LOAD DB | |
| # ===================== | |
| with open("nutrition_db.json", "r") as f: | |
| NUTRITION_DB = json.load(f) | |
| print("β Loaded DB:", len(NUTRITION_DB)) | |
| # ===================== | |
| # COUNTABLE FOODS (NEW) | |
| # ===================== | |
| COUNTABLE_FOODS = { | |
| "roti": {"calories": 120, "protein": 3, "carbs": 20, "fat": 2}, | |
| "bread": {"calories": 80, "protein": 3, "carbs": 15, "fat": 1}, | |
| "samosa": {"calories": 260, "protein": 5, "carbs": 30, "fat": 14}, | |
| "gulab jamun": {"calories": 150, "protein": 2, "carbs": 30, "fat": 5}, | |
| "laddu": {"calories": 180, "protein": 3, "carbs": 25, "fat": 8}, | |
| "idli": {"calories": 60, "protein": 2, "carbs": 12, "fat": 1}, | |
| "vada": {"calories": 150, "protein": 4, "carbs": 20, "fat": 8} | |
| } | |
| # ===================== | |
| # NORMALIZATION (FIXED) | |
| # ===================== | |
| def normalize_food_name(name): | |
| name = name.lower() | |
| mapping = { | |
| "chapati": "roti", | |
| "omellete": "omelette", | |
| "fried rice": "rice", | |
| "plain rice": "rice" | |
| } | |
| return mapping.get(name, name) | |
| # ===================== | |
| # FIND MATCH | |
| # ===================== | |
| def find_food(name): | |
| if name in NUTRITION_DB: | |
| return name | |
| matches = get_close_matches(name, NUTRITION_DB.keys(), n=1, cutoff=0.6) | |
| return matches[0] if matches else None | |
| # ===================== | |
| # FALLBACK | |
| # ===================== | |
| def estimate_unknown_food(grams): | |
| return { | |
| "calories": round(1.5 * grams, 2), | |
| "protein": round(0.05 * grams, 2), | |
| "carbs": round(0.2 * grams, 2), | |
| "fat": round(0.05 * grams, 2), | |
| } | |
| # ===================== | |
| # CACHE | |
| # ===================== | |
| def compute_nutrition_cached(food_key, grams): | |
| base = NUTRITION_DB[food_key] | |
| factor = grams / 100 | |
| return { | |
| "calories": round(base["calories"] * factor, 2), | |
| "protein": round(base["protein"] * factor, 2), | |
| "carbs": round(base["carbs"] * factor, 2), | |
| "fat": round(base["fat"] * factor, 2), | |
| } | |
| # ===================== | |
| # GET NUTRITION | |
| # ===================== | |
| def get_nutrition(dish, grams): | |
| dish = normalize_food_name(dish) | |
| food_key = find_food(dish) | |
| if not food_key: | |
| return estimate_unknown_food(grams) | |
| return compute_nutrition_cached(food_key, grams) | |
| # ===================== | |
| # QUANTITY ESTIMATION | |
| # ===================== | |
| def estimate_quantity(pred): | |
| width = pred.get("width", 0) | |
| height = pred.get("height", 0) | |
| area = width * height | |
| ratio = area / (640 * 640) | |
| grams = 150 + (ratio * 300) | |
| return round(grams, 1) | |
| # ===================== | |
| # ROBOFLOW DETECTION | |
| # ===================== | |
| def detect(image_path): | |
| api_key = os.getenv("ROBOFLOW_API_KEY") | |
| if not api_key: | |
| return "β API KEY NOT FOUND" | |
| url = "https://detect.roboflow.com/almost-final/2" | |
| try: | |
| with open(image_path, "rb") as f: | |
| response = requests.post( | |
| url, | |
| files={"file": f}, | |
| params={"api_key": api_key}, | |
| timeout=15 | |
| ) | |
| if response.status_code != 200: | |
| return f"β Roboflow Error: {response.text}" | |
| data = response.json() | |
| return data.get("predictions", []) | |
| except Exception as e: | |
| return f"β Request Failed: {str(e)}" | |
| # ===================== | |
| # MAIN FUNCTION (HYBRID) | |
| # ===================== | |
| def analyze_image(image): | |
| if image is None: | |
| return "Please upload an image" | |
| path = "temp.jpg" | |
| image.save(path) | |
| preds = detect(path) | |
| if isinstance(preds, str): | |
| return preds | |
| if len(preds) == 0: | |
| return "β No food detected" | |
| # GROUP ITEMS | |
| dish_counts = Counter([ | |
| normalize_food_name(p.get("class", "unknown")) for p in preds | |
| ]) | |
| output = "" | |
| total = {"calories": 0, "protein": 0, "carbs": 0, "fat": 0} | |
| for dish, count in dish_counts.items(): | |
| if dish == "unknown": | |
| continue | |
| # ========================= | |
| # COUNTABLE FOODS | |
| # ========================= | |
| if dish in COUNTABLE_FOODS: | |
| base = COUNTABLE_FOODS[dish] | |
| nutrition = { | |
| "calories": base["calories"] * count, | |
| "protein": base["protein"] * count, | |
| "carbs": base["carbs"] * count, | |
| "fat": base["fat"] * count, | |
| } | |
| output += f"π½οΈ {dish} ({count} pcs)\n" | |
| # ========================= | |
| # WEIGHT-BASED FOODS | |
| # ========================= | |
| else: | |
| relevant_preds = [ | |
| p for p in preds if normalize_food_name(p.get("class")) == dish | |
| ] | |
| grams_list = [estimate_quantity(p) for p in relevant_preds] | |
| grams = sum(grams_list) | |
| nutrition = get_nutrition(dish, grams) | |
| output += f"π½οΈ {dish}\n" | |
| output += f"π {grams:.1f} g\n" | |
| # ========================= | |
| # OUTPUT | |
| # ========================= | |
| output += f"π₯ {nutrition['calories']} kcal\n" | |
| output += f"πͺ Protein: {nutrition['protein']} g\n" | |
| output += f"π Carbs: {nutrition['carbs']} g\n" | |
| output += f"π§ Fat: {nutrition['fat']} g\n" | |
| output += "-"*30 + "\n" | |
| for k in total: | |
| total[k] += nutrition[k] | |
| # TOTAL | |
| output += "\nπ§Ύ TOTAL:\n" | |
| output += f"π₯ Calories: {round(total['calories'],2)}\n" | |
| output += f"πͺ Protein: {round(total['protein'],2)} g\n" | |
| output += f"π Carbs: {round(total['carbs'],2)} g\n" | |
| output += f"π§ Fat: {round(total['fat'],2)} g\n" | |
| return output | |
| # ===================== | |
| # UI | |
| # ===================== | |
| demo = gr.Interface( | |
| fn=analyze_image, | |
| inputs=gr.Image(type="pil"), | |
| outputs="text", | |
| title="π½οΈ AI Nutritionist", | |
| description="Upload food image to get calories & macros" | |
| ) | |
| # ===================== | |
| # RUN | |
| # ===================== | |
| if __name__ == "__main__": | |
| demo.launch(ssr_mode=False) |