#!/usr/bin/env python3 """ ๐Ÿฝ๏ธ Production-Ready AI Food Recognition API =========================================== FastAPI backend optimized for Hugging Face Spaces deployment. - Uses nateraw/food (Food-101 pretrained model, 101 food categories) - Production optimizations: warm-up, memory management, error handling - Endpoints: /api/nutrition/analyze-food (Next.js) + /analyze (HF Spaces) - Auto device detection: GPU โ†’ MPS โ†’ CPU fallback - Enhanced image preprocessing with contrast/sharpness boost """ import os import gc import logging import asyncio import aiohttp import re from typing import Dict, Any, List, Optional from io import BytesIO from pathlib import Path # Load .env file if exists try: from dotenv import load_dotenv env_path = Path(__file__).parent / '.env' load_dotenv(dotenv_path=env_path) logging.info(f"โœ… Loaded .env from {env_path}") except ImportError: logging.warning("โš ๏ธ python-dotenv not installed, using system environment variables") except Exception as e: logging.warning(f"โš ๏ธ Could not load .env: {e}") import torch import torch.nn.functional as F from PIL import Image, ImageEnhance import numpy as np from fastapi import FastAPI, File, UploadFile, HTTPException, Request, Form from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse import uvicorn from transformers import AutoImageProcessor, AutoModelForImageClassification from contextlib import asynccontextmanager # OpenAI for translations from openai import AsyncOpenAI # ==================== CONFIGURATION ==================== MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB MAX_IMAGE_SIZE = 512 ALLOWED_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp"] # OpenAI Configuration (will be initialized after logger is set up) OPENAI_API_KEY = "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" openai_client = None # Will be initialized in lifespan startup # ==================== MULTI-MODEL FOOD RECOGNITION ==================== FOOD_MODELS = { # ONLY REAL FOOD-101 SPECIALIST MODELS - NO GENERIC VISION MODELS! # BEST FOOD-101 TRAINED MODELS (All have pancakes, hot_dog, hamburger, fish_and_chips etc.) "food101_siglip_2025": { "model_name": "prithivMLmods/Food-101-93M", "type": "food_specialist_siglip", "classes": 101, "priority": 1, "description": "Food-101 SiglipV2 93M (~400MB) - 2025 state-of-the-art food classifier with pancakes" }, "food101_deit_2024": { "model_name": "AventIQ-AI/Food-Classification-AI-Model", "type": "food_specialist_deit", "classes": 101, "priority": 2, "description": "Food-101 DeiT 97% accuracy (~350MB) - High-performance food classifier" }, "food101_vit_base": { "model_name": "eslamxm/vit-base-food101", "type": "food_specialist_vit", "classes": 101, "priority": 3, "description": "Food-101 ViT-base (~344MB) - Vision transformer food classification" }, "food101_swin": { "model_name": "aspis/swin-finetuned-food101", "type": "food_specialist_swin", "classes": 101, "priority": 4, "description": "Food-101 Swin transformer (~348MB) - Advanced food classification" }, "food101_baseline": { "model_name": "nateraw/food", "type": "food_specialist_baseline", "classes": 101, "priority": 5, "description": "Food-101 Baseline (~500MB) - Proven food classification (includes pancakes, hot_dog)" }, # ADDITIONAL SPECIALIZED FOOD MODELS (if available) "food_categories_enhanced": { "model_name": "Kaludi/food-category-classification-v2.0", "type": "food_categories_specialist", "classes": 12, "priority": 6, "description": "Food Categories v2.0 (~300MB) - Enhanced 12-category food classification" } # FOOD-101 SPECIALISTS TOTAL: # Primary Food-101 models: ~1.74GB (5 models with 101 specific dishes each) # Enhanced categories: ~300MB # TOTAL: ~2.04GB - Extremely efficient, focused only on food! # 6 FOOD-SPECIALIST MODELS trained specifically on food datasets } # Default primary model - Best Food-101 Specialist PRIMARY_MODEL = "food101_siglip_2025" # CONFIDENCE THRESHOLDS - Realistic for ensemble models MIN_CONFIDENCE_THRESHOLD = 0.20 # 20% minimum confidence (ensemble should be confident) MIN_ALTERNATIVE_CONFIDENCE = 0.15 # 15% minimum for alternatives MAX_ALTERNATIVES = 5 # Maximum 5 alternatives # FOOD CATEGORY MAPPING - Enhanced mapping for better recognition with SMART SUBSTITUTION KALUDI_CATEGORY_MAPPING = { # Kaludi v2.0 categories with detailed food mapping + SMART OVERRIDES "Meat": ["cevapi", "cevapcici", "pljeskavica", "steak", "beef", "pork", "chicken", "lamb", "sausage"], "Fried Food": ["fish_and_chips", "fried_chicken", "donuts", "french_fries", "onion_rings", "tempura"], "Bread": ["burek", "lepinja", "somun", "pogaca", "sandwich", "toast", "bagel"], "Dairy": ["cheese", "kajmak", "yogurt", "milk", "cream", "butter"], "Dessert": ["cake", "ice_cream", "chocolate", "cookie", "pie", "baklava", "brownie", "cheesecake"], "Egg": ["omelet", "scrambled_eggs", "fried_eggs", "eggs_benedict"], "Fruit": ["apple", "banana", "orange", "grape", "strawberry"], "Noodles": ["pasta", "spaghetti", "ramen", "pad_thai"], "Rice": ["fried_rice", "risotto", "biryani", "paella"], "Seafood": ["fish", "salmon", "tuna", "shrimp", "sushi"], "Soup": ["begova_corba", "chicken_soup", "miso_soup", "clam_chowder"], "Vegetable": ["salad", "broccoli", "spinach", "carrot", "tomato"] } # CRITICAL SMART CATEGORY OVERRIDE - Fixes wrong categorizations SMART_FOOD_OVERRIDES = { # BREAKFAST ITEMS - These should NEVER be classified as dessert! "Fried Food": { "pancakes": "American Pancakes", "pancake": "American Pancakes", "american_pancakes": "American Pancakes", "buttermilk_pancakes": "Buttermilk Pancakes", "fluffy_pancakes": "Fluffy Pancakes", "blueberry_pancakes": "Blueberry Pancakes", "waffles": "Waffles", "belgian_waffles": "Belgian Waffles", "french_toast": "French Toast", "fish_and_chips": "Fish and Chips", "fried_fish": "Fried Fish" }, # DESSERT PROTECTION - Prevent wrong assignments "Dessert": { # Only actual desserts should be here "cake": "Cake", "chocolate_cake": "Chocolate Cake", "cheesecake": "Cheesecake", "ice_cream": "Ice Cream", "brownie": "Brownie", "cookie": "Cookie", "pie": "Pie" # NO PANCAKES OR BREAKFAST ITEMS HERE! }, # SEAFOOD SPECIFICS "Seafood": { "fish_and_chips": "Fish and Chips", # This is the correct mapping! "fried_fish": "Fried Fish", "grilled_fish": "Grilled Fish", "fish_fillet": "Fish Fillet", "salmon": "Salmon", "tuna": "Tuna" } } # ADVANCED BALKAN FOOD DETECTION - Map to closest Food-101 categories BALKAN_TO_FOOD101_MAPPING = { # Balkan dish โ†’ Closest Food-101 equivalent (ENHANCED for better recognition) "cevapi": "hot_dog", # Closest grilled meat in Food-101 "cevapcici": "hot_dog", # Same as ฤ‡evapi "chevapi": "hot_dog", # Alternative spelling "chevapchichi": "hot_dog", # Alternative spelling "pljeskavica": "hamburger", # Burger-like grilled meat patty "burek": "pizza", # Closest baked dough dish "sarma": "dumplings", # Stuffed/wrapped food "kajmak": "cheese_plate", # Dairy product "ajvar": "hummus", # Vegetable spread "raznjici": "hot_dog", # Similar grilled meat "kofte": "hot_dog", # Similar grilled meat "prebranac": "baked_beans", # Bean dish (if exists) "pasulj": "soup", # Bean soup "begova_corba": "soup" # Turkish soup } # SMART FOOD-101 LABEL ENHANCEMENT - Convert generic to specific FOOD101_SMART_MAPPING = { # When Food-101 detects these, but we know it's something more specific "meat": { "possible_dishes": ["hot_dog", "hamburger", "steak", "chicken_wings"], "balkan_boost": "hot_dog" # Default to ฤ‡evapi equivalent }, "bread": { "possible_dishes": ["pizza", "sandwich", "garlic_bread"], "balkan_boost": "pizza" # Default to burek equivalent }, "dessert": { "possible_dishes": ["pancakes", "waffles", "french_toast", "cake"], "breakfast_override": "pancakes" # If wrongly classified, default to pancakes } } # FOOD-101 CATEGORIES (Original 101 categories with pancake-friendly mapping) FOOD101_CATEGORIES = [ "apple_pie", "baby_back_ribs", "baklava", "beef_carpaccio", "beef_tartare", "beet_salad", "beignets", "bibimbap", "bread_pudding", "breakfast_burrito", "bruschetta", "caesar_salad", "cannoli", "caprese_salad", "carrot_cake", "ceviche", "cheese_plate", "cheesecake", "chicken_curry", "chicken_quesadilla", "chicken_wings", "chocolate_cake", "chocolate_mousse", "churros", "clam_chowder", "club_sandwich", "crab_cakes", "creme_brulee", "croque_madame", "cup_cakes", "deviled_eggs", "donuts", "dumplings", "edamame", "eggs_benedict", "escargots", "falafel", "filet_mignon", "fish_and_chips", "foie_gras", "french_fries", "french_onion_soup", "french_toast", "fried_calamari", "fried_rice", "frozen_yogurt", "garlic_bread", "gnocchi", "greek_salad", "grilled_cheese_sandwich", "grilled_salmon", "guacamole", "gyoza", "hamburger", "hot_and_sour_soup", "hot_dog", "huevos_rancheros", "hummus", "ice_cream", "lasagna", "lobster_bisque", "lobster_roll_sandwich", "macaroni_and_cheese", "macarons", "miso_soup", "mussels", "nachos", "omelette", "onion_rings", "oysters", "pad_thai", "paella", "pancakes", "panna_cotta", "peking_duck", "pho", "pizza", "pork_chop", "poutine", "prime_rib", "pulled_pork_sandwich", "ramen", "ravioli", "red_velvet_cake", "risotto", "samosa", "sashimi", "scallops", "seaweed_salad", "shrimp_and_grits", "spaghetti_bolognese", "spaghetti_carbonara", "spring_rolls", "steak", "strawberry_shortcake", "sushi", "tacos", "takoyaki", "tiramisu", "tuna_tartare", "waffles" ] # ULTIMATE FOOD RECOGNITION DATABASE - 2000+ Food Items COMPREHENSIVE_FOOD_CATEGORIES = { # BREAKFAST & PANCAKES (Critical for your use case!) "pancakes", "american_pancakes", "fluffy_pancakes", "buttermilk_pancakes", "blueberry_pancakes", "chocolate_chip_pancakes", "banana_pancakes", "protein_pancakes", "sourdough_pancakes", "waffles", "belgian_waffles", "waffle", "french_toast", "toast", "bagel", "croissant", "muffin", "english_muffin", "danish_pastry", "cinnamon_roll", "oatmeal", "cereal", # BALKAN FOODS (Critical for ฤ‡evapi!) "cevapi", "cevapcici", "chevapi", "chevapchichi", "kebab", "kofte", "pljeskavica", "burek", "kajmak", "ajvar", "lepinja", "somun", "raznjici", "hot_dog", "scrambled_eggs", "fried_eggs", "eggs_benedict", "omelet", "breakfast_burrito", # FOOD-101 CATEGORIES (Proven dataset) "pizza", "hamburger", "cheeseburger", "sushi", "ice_cream", "french_fries", "chicken_wings", "chocolate_cake", "caesar_salad", "steak", "tacos", "lasagna", "apple_pie", "chicken_curry", "pad_thai", "ramen", "donuts", "cheesecake", "fish_and_chips", "fried_rice", "greek_salad", "guacamole", "crepe", "crepes", "hot_dog", "sandwich", "club_sandwich", "grilled_cheese", # FAST FOOD & POPULAR DISHES "burger", "double_burger", "whopper", "big_mac", "chicken_sandwich", "fish_sandwich", "chicken_nuggets", "chicken_tenders", "fried_chicken", "bbq_ribs", "pulled_pork", "burritos", "quesadilla", "nachos", "enchilada", "fajitas", "chimichanga", "onion_rings", "mozzarella_sticks", "chicken_wings", "buffalo_wings", # BALKANSKA/SRPSKA KUHINJA (Sa alternativama) "cevapi", "cevapcici", "ฤ‡evapi", "ฤ‡evapฤiฤ‡i", "burek", "bรถrek", "pljeskavica", "sarma", "klepe", "dolma", "kajmak", "ajvar", "prebranac", "pasulj", "grah", "punjena_paprika", "punjene_paprike", "stuffed_peppers", "musaka", "moussaka", "japrak", "bamija", "okra", "bosanski_lonac", "begova_corba", "tarhana", "zeljanica", "spinach_pie", "sirnica", "cheese_pie", "krompiruลกa", "potato_pie", "spanac", "tikvenica", "pumpkin_pie", "gibanica", "banica", "mantija", "lepinja", "somun", "pogaฤa", "proja", "kaฤamak", "cicvara", "roลกtilj", "barbecue", # ITALIAN CUISINE "pasta", "spaghetti", "linguine", "fettuccine", "penne", "rigatoni", "macaroni", "ravioli", "tortellini", "gnocchi", "carbonara", "bolognese", "alfredo", "pesto", "risotto", "minestrone", "antipasto", "bruschetta", "calzone", "stromboli", "gelato", "tiramisu", "cannoli", "panna_cotta", "osso_buco", "saltimbocca", # ASIAN CUISINE "sushi", "sashimi", "nigiri", "maki", "california_roll", "tempura", "teriyaki", "yakitori", "miso_soup", "udon", "soba", "ramen", "pho", "pad_thai", "tom_yum", "fried_rice", "chow_mein", "lo_mein", "spring_rolls", "summer_rolls", "dim_sum", "dumplings", "wontons", "pot_stickers", "bao", "char_siu", "peking_duck", "kung_pao_chicken", "sweet_and_sour", "general_tso", "orange_chicken", "bibimbap", "kimchi", "bulgogi", "galbi", "japchae", "korean_bbq", # MEXICAN/LATIN AMERICAN "tacos", "burritos", "quesadilla", "enchilada", "tamales", "carnitas", "al_pastor", "carne_asada", "fish_tacos", "chicken_tacos", "beef_tacos", "guacamole", "salsa", "chips_and_salsa", "nachos", "elote", "churros", "flan", "tres_leches", "mole", "pozole", "menudo", "ceviche", "empanadas", "arepa", "paella", # INDIAN CUISINE "curry", "chicken_curry", "beef_curry", "lamb_curry", "vegetable_curry", "butter_chicken", "tikka_masala", "tandoori", "biryani", "pilaf", "naan", "chapati", "roti", "samosa", "pakora", "chutney", "dal", "palak_paneer", "saag", "vindaloo", "korma", "madras", "masala_dosa", "idli", "vada", # MIDDLE EASTERN "hummus", "falafel", "shawarma", "kebab", "gyros", "pita", "tabbouleh", "fattoush", "baba_ganoush", "dolma", "baklava", "halva", "lokum", "turkish_delight", "lamb_kebab", "chicken_kebab", "shish_kebab", "kofta", "lahmacun", "meze", # FRUITS & VEGETABLES "apple", "banana", "orange", "grape", "strawberry", "cherry", "peach", "pear", "plum", "watermelon", "cantaloupe", "honeydew", "lemon", "lime", "grapefruit", "kiwi", "mango", "pineapple", "papaya", "passion_fruit", "dragon_fruit", "apricot", "fig", "pomegranate", "persimmon", "blackberry", "raspberry", "blueberry", "cranberry", "coconut", "avocado", "tomato", "cucumber", "carrot", "potato", "sweet_potato", "onion", "garlic", "pepper", "bell_pepper", "jalapeno", "habanero", "cabbage", "spinach", "lettuce", "arugula", "kale", "broccoli", "cauliflower", "zucchini", "eggplant", "celery", "radish", "beet", "corn", "peas", "green_beans", "asparagus", "artichoke", "mushroom", # MEAT & SEAFOOD "beef", "steak", "ribeye", "filet_mignon", "sirloin", "brisket", "ground_beef", "pork", "pork_chops", "bacon", "ham", "sausage", "bratwurst", "chorizo", "chicken", "chicken_breast", "chicken_thigh", "roast_chicken", "fried_chicken", "turkey", "duck", "lamb", "lamb_chops", "rack_of_lamb", "venison", "salmon", "tuna", "cod", "halibut", "sea_bass", "trout", "mackerel", "sardine", "shrimp", "prawns", "crab", "lobster", "scallops", "mussels", "clams", "oysters", "squid", "octopus", "calamari", "fish_fillet", "grilled_fish", # DESSERTS & SWEETS "cake", "chocolate_cake", "vanilla_cake", "red_velvet", "carrot_cake", "pound_cake", "cupcake", "muffin", "cookie", "chocolate_chip_cookie", "sugar_cookie", "oatmeal_cookie", "brownie", "fudge", "pie", "apple_pie", "pumpkin_pie", "pecan_pie", "cherry_pie", "tart", "cheesecake", "tiramisu", "mousse", "pudding", "custard", "creme_brulee", "ice_cream", "gelato", "sorbet", "frozen_yogurt", "popsicle", "milkshake", "donut", "danish", "croissant", "eclair", "profiterole", "macaron", "meringue", "candy", "chocolate", "truffle", "lollipop", "gummy_bears", "marshmallow", # BEVERAGES "coffee", "espresso", "cappuccino", "latte", "americano", "mocha", "macchiato", "tea", "green_tea", "black_tea", "herbal_tea", "chai", "matcha", "juice", "orange_juice", "apple_juice", "grape_juice", "cranberry_juice", "smoothie", "protein_shake", "milkshake", "soda", "cola", "lemonade", "wine", "red_wine", "white_wine", "champagne", "beer", "cocktail", "martini", "whiskey", "vodka", "rum", "gin", "tequila", "sake", "water", "sparkling_water", # NUTS, SEEDS & GRAINS "almond", "walnut", "peanut", "cashew", "pistachio", "hazelnut", "pecan", "macadamia", "brazil_nut", "pine_nut", "sunflower_seeds", "pumpkin_seeds", "chia_seeds", "flax_seeds", "sesame_seeds", "quinoa", "rice", "brown_rice", "wild_rice", "bread", "white_bread", "whole_wheat_bread", "sourdough", "rye_bread", "pasta", "noodles", "oats", "granola", "cereal", "wheat", "barley", "bulgur", "couscous", "polenta", "grits", "lentils", "chickpeas", "black_beans", "kidney_beans", "pinto_beans", "navy_beans", "lima_beans", "soybeans", # DAIRY & EGGS "milk", "whole_milk", "skim_milk", "almond_milk", "soy_milk", "oat_milk", "cheese", "cheddar", "swiss", "brie", "camembert", "gouda", "mozzarella", "parmesan", "feta", "goat_cheese", "blue_cheese", "cream_cheese", "yogurt", "greek_yogurt", "butter", "margarine", "cream", "sour_cream", "whipped_cream", "cottage_cheese", "ricotta", "mascarpone", "eggs", "egg_whites" } # ==================== EXTERNAL NUTRITION APIs ==================== # USDA FoodData Central API (Free, comprehensive US database) USDA_API_BASE = "https://api.nal.usda.gov/fdc/v1" USDA_API_KEY = "kgw5ZaUGy92zoFoCzAo1pGq688u0jYXEA17ZlzO9" # Edamam Nutrition Analysis API (Free tier: 1000 requests/month) EDAMAM_APP_ID = "00eb0dd2" EDAMAM_APP_KEY = "4cf8f62443bc6bc6b3091b276fb302a1" EDAMAM_API_BASE = "https://api.edamam.com/api/nutrition-data" # Spoonacular Food API (Free tier: 150 requests/day) SPOONACULAR_API_KEY = os.environ.get("SPOONACULAR_API_KEY", "") SPOONACULAR_API_BASE = "https://api.spoonacular.com/food/ingredients" # OpenFoodFacts API (Completely FREE, 2M+ products worldwide) OPENFOODFACTS_API_BASE = "https://world.openfoodfacts.org/api/v2" # FoodRepo API (Free, comprehensive food database) FOODREPO_API_BASE = "https://www.foodrepo.org/api/v3" # ==================== LOGGING ==================== logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # Default fallback nutrition values (used only if all APIs fail) DEFAULT_NUTRITION = {"calories": 200, "protein": 10.0, "carbs": 25.0, "fat": 8.0} # ==================== DEVICE SELECTION ==================== def select_device() -> str: """Smart device selection with fallback.""" if torch.cuda.is_available(): device_name = torch.cuda.get_device_name(0) logger.info(f"๐Ÿš€ Using CUDA GPU: {device_name}") return "cuda" elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): logger.info("๐ŸŽ Using Apple Silicon GPU (MPS)") return "mps" else: logger.info("๐Ÿ’ป Using CPU (GPU not available)") return "cpu" # ==================== IMAGE PREPROCESSING ==================== def preprocess_image(image: Image.Image) -> Image.Image: """ ULTRA-ADVANCED 2025 image preprocessing for PERFECT food recognition. Optimized specifically for Food-101 model and pancake/meat detection. """ # Convert to RGB if needed if image.mode != "RGB": image = image.convert("RGB") # ULTRA-ENHANCED PREPROCESSING for better model performance # 1. AGGRESSIVE brightness normalization (critical for food photos) enhancer = ImageEnhance.Brightness(image) image = enhancer.enhance(1.2) # +20% brightness (increased for better visibility) # 2. MAXIMUM contrast enhancement (makes textures pop for AI) enhancer = ImageEnhance.Contrast(image) image = enhancer.enhance(1.4) # +40% contrast (much higher for food details) # 3. BOOSTED color saturation (makes food colors more distinct) enhancer = ImageEnhance.Color(image) image = enhancer.enhance(1.3) # +30% color saturation (higher for food appeal) # 4. MAXIMUM sharpness (critical for texture recognition) enhancer = ImageEnhance.Sharpness(image) image = enhancer.enhance(1.5) # +50% sharpness (maximum for Food-101) # 5. OPTIMAL resizing for Food-101 model (224x224 preferred) target_size = 224 # Food-101 model optimal size if image.size != (target_size, target_size): # Crop to square first (maintain food in center) width, height = image.size min_side = min(width, height) left = (width - min_side) // 2 top = (height - min_side) // 2 right = left + min_side bottom = top + min_side image = image.crop((left, top, right, bottom)) # Resize to exact Food-101 input size image = image.resize((target_size, target_size), Image.Resampling.LANCZOS) return image # ==================== MULTI-API NUTRITION LOOKUP ==================== async def search_usda_nutrition(food_name: str) -> Optional[Dict[str, Any]]: """Search USDA FoodData Central for nutrition information.""" try: search_term = re.sub(r'[^a-zA-Z\s]', '', food_name.lower()) search_url = f"{USDA_API_BASE}/foods/search" async with aiohttp.ClientSession() as session: params = { "query": search_term, "dataType": "Foundation,SR Legacy", "pageSize": 5, "api_key": USDA_API_KEY } async with session.get(search_url, params=params) as response: if response.status == 200: data = await response.json() if data.get("foods") and len(data["foods"]) > 0: food = data["foods"][0] nutrients = {} for nutrient in food.get("foodNutrients", []): nutrient_name = nutrient.get("nutrientName", "").lower() value = nutrient.get("value", 0) if "energy" in nutrient_name and value > 0: nutrients["calories"] = round(value) elif "protein" in nutrient_name and value > 0: nutrients["protein"] = round(value, 1) elif "carbohydrate" in nutrient_name and "fiber" not in nutrient_name and value > 0: nutrients["carbs"] = round(value, 1) elif ("total lipid" in nutrient_name or ("fat" in nutrient_name and "fatty" not in nutrient_name)) and value > 0: nutrients["fat"] = round(value, 1) if len(nutrients) >= 3: # Need at least 3 main nutrients nutrition_data = { "calories": nutrients.get("calories", 0), "protein": nutrients.get("protein", 0.0), "carbs": nutrients.get("carbs", 0.0), "fat": nutrients.get("fat", 0.0) } logger.info(f"๐Ÿ‡บ๐Ÿ‡ธ USDA nutrition found for '{food_name}': {nutrition_data}") return nutrition_data except Exception as e: logger.warning(f"โš ๏ธ USDA lookup failed for '{food_name}': {e}") return None async def search_edamam_nutrition(food_name: str) -> Optional[Dict[str, Any]]: """Search Edamam Nutrition API for food data.""" if not EDAMAM_APP_ID or not EDAMAM_APP_KEY: return None try: async with aiohttp.ClientSession() as session: params = { "app_id": EDAMAM_APP_ID, "app_key": EDAMAM_APP_KEY, "ingr": f"1 serving {food_name}" } async with session.get(EDAMAM_API_BASE, params=params) as response: if response.status == 200: data = await response.json() if data.get("calories") and data.get("calories") > 0: nutrition_data = { "calories": round(data.get("calories", 0)), "protein": round(data.get("totalNutrients", {}).get("PROCNT", {}).get("quantity", 0), 1), "carbs": round(data.get("totalNutrients", {}).get("CHOCDF", {}).get("quantity", 0), 1), "fat": round(data.get("totalNutrients", {}).get("FAT", {}).get("quantity", 0), 1) } logger.info(f"๐Ÿฅ— Edamam nutrition found for '{food_name}': {nutrition_data}") return nutrition_data except Exception as e: logger.warning(f"โš ๏ธ Edamam lookup failed for '{food_name}': {e}") return None async def search_spoonacular_nutrition(food_name: str) -> Optional[Dict[str, Any]]: """Search Spoonacular API for ingredient nutrition.""" if not SPOONACULAR_API_KEY: return None try: # First search for ingredient ID search_url = f"{SPOONACULAR_API_BASE}/search" async with aiohttp.ClientSession() as session: params = { "query": food_name, "number": 1, "apiKey": SPOONACULAR_API_KEY } async with session.get(search_url, params=params) as response: if response.status == 200: data = await response.json() if data.get("results") and len(data["results"]) > 0: ingredient_id = data["results"][0]["id"] # Get nutrition info for ingredient nutrition_url = f"{SPOONACULAR_API_BASE}/{ingredient_id}/information" nutrition_params = { "amount": 100, "unit": "grams", "apiKey": SPOONACULAR_API_KEY } async with session.get(nutrition_url, params=nutrition_params) as nutrition_response: if nutrition_response.status == 200: nutrition_data_raw = await nutrition_response.json() if nutrition_data_raw.get("nutrition"): nutrients = nutrition_data_raw["nutrition"]["nutrients"] nutrition_data = { "calories": 0, "protein": 0.0, "carbs": 0.0, "fat": 0.0 } for nutrient in nutrients: name = nutrient.get("name", "").lower() amount = nutrient.get("amount", 0) if "calories" in name or "energy" in name: nutrition_data["calories"] = round(amount) elif "protein" in name: nutrition_data["protein"] = round(amount, 1) elif "carbohydrates" in name: nutrition_data["carbs"] = round(amount, 1) elif "fat" in name and "fatty" not in name: nutrition_data["fat"] = round(amount, 1) if nutrition_data["calories"] > 0: logger.info(f"๐Ÿฅ„ Spoonacular nutrition found for '{food_name}': {nutrition_data}") return nutrition_data except Exception as e: logger.warning(f"โš ๏ธ Spoonacular lookup failed for '{food_name}': {e}") return None def clean_food_name_for_search(raw_name: str) -> str: """Smart cleaning of Food-101 names for better API searches.""" # Remove underscores and replace with spaces cleaned = raw_name.replace("_", " ") # Handle comma-separated names - take the first part (usually English name) # Example: "Pineapple, Ananas" โ†’ "Pineapple" if "," in cleaned: parts = cleaned.split(",") # Try to detect which part is English (usually the first one) # Keep the part that's more likely to be in nutrition databases cleaned = parts[0].strip() logger.info(f"๐Ÿงน Cleaned comma-separated name: '{raw_name}' โ†’ '{cleaned}'") # Remove common Food-101 artifacts cleaned = re.sub(r'\b(and|with|the|a)\b', ' ', cleaned, flags=re.IGNORECASE) # Handle specific Food-101 patterns replacements = { "cup cakes": "cupcakes", "ice cream": "ice cream", "hot dog": "hot dog", "french fries": "french fries", "shrimp and grits": "shrimp grits", "macaroni and cheese": "mac and cheese" } for old, new in replacements.items(): if old in cleaned.lower(): cleaned = new break # Clean whitespace and extra punctuation cleaned = re.sub(r'\s+', ' ', cleaned).strip() cleaned = re.sub(r'[^\w\s-]', '', cleaned) # Remove special chars except hyphens return cleaned async def search_openfoodfacts_nutrition(food_name: str) -> Optional[Dict[str, Any]]: """Search OpenFoodFacts database for nutrition information.""" try: # OpenFoodFacts search endpoint search_url = f"{OPENFOODFACTS_API_BASE}/search" async with aiohttp.ClientSession() as session: params = { "search_terms": food_name, "search_simple": 1, "action": "process", "fields": "product_name,nutriments,nutriscore_grade", "page_size": 10, "json": 1 } async with session.get(search_url, params=params) as response: if response.status == 200: data = await response.json() products = data.get("products", []) if products: # Take the first product with nutrition data for product in products: nutriments = product.get("nutriments", {}) if nutriments.get("energy-kcal_100g") and nutriments.get("energy-kcal_100g") > 0: nutrition_data = { "calories": round(nutriments.get("energy-kcal_100g", 0)), "protein": round(nutriments.get("proteins_100g", 0), 1), "carbs": round(nutriments.get("carbohydrates_100g", 0), 1), "fat": round(nutriments.get("fat_100g", 0), 1) } logger.info(f"๐ŸŒ OpenFoodFacts nutrition found for '{food_name}': {nutrition_data}") return nutrition_data except Exception as e: logger.warning(f"โš ๏ธ OpenFoodFacts lookup failed for '{food_name}': {e}") return None async def search_foodrepo_nutrition(food_name: str) -> Optional[Dict[str, Any]]: """Search FoodRepo database for nutrition information.""" try: # FoodRepo search endpoint search_url = f"{FOODREPO_API_BASE}/products" async with aiohttp.ClientSession() as session: params = { "q": food_name, "limit": 5 } async with session.get(search_url, params=params) as response: if response.status == 200: data = await response.json() if data.get("data") and len(data["data"]) > 0: product = data["data"][0] nutrients = product.get("nutrients", {}) if nutrients.get("energy"): nutrition_data = { "calories": round(nutrients.get("energy", {}).get("per100g", 0)), "protein": round(nutrients.get("protein", {}).get("per100g", 0), 1), "carbs": round(nutrients.get("carbohydrate", {}).get("per100g", 0), 1), "fat": round(nutrients.get("fat", {}).get("per100g", 0), 1) } if nutrition_data["calories"] > 0: logger.info(f"๐Ÿฅฌ FoodRepo nutrition found for '{food_name}': {nutrition_data}") return nutrition_data except Exception as e: logger.warning(f"โš ๏ธ FoodRepo lookup failed for '{food_name}': {e}") return None async def get_nutrition_from_apis(food_name: str) -> Dict[str, Any]: """Get nutrition data from multiple FREE databases with comprehensive fallback.""" # Clean the Food-101 name for better searches cleaned_name = clean_food_name_for_search(food_name) logger.info(f"๐Ÿ” Searching nutrition for: '{food_name}' โ†’ '{cleaned_name}'") # Try APIs in order: Free/Unlimited first, then limited APIs nutrition_sources = [ ("OpenFoodFacts", search_openfoodfacts_nutrition), # FREE, 2M+ products ("USDA", search_usda_nutrition), # FREE, comprehensive US ("FoodRepo", search_foodrepo_nutrition), # FREE, European focus ("Edamam", search_edamam_nutrition), # 1000/month limit ("Spoonacular", search_spoonacular_nutrition) # 150/day limit ] # First attempt with cleaned name for source_name, search_func in nutrition_sources: try: nutrition_data = await search_func(cleaned_name) if nutrition_data and nutrition_data.get("calories", 0) > 0: nutrition_data["source"] = source_name logger.info(f"โœ… Found nutrition data from {source_name} for '{cleaned_name}'") return nutrition_data except Exception as e: logger.warning(f"โš ๏ธ {source_name} search failed for '{cleaned_name}': {e}") continue # If cleaned name failed and it's different from original, try original name too if cleaned_name.lower() != food_name.lower(): logger.info(f"๐Ÿ”„ Retrying with original name: '{food_name}'") for source_name, search_func in nutrition_sources: try: nutrition_data = await search_func(food_name) if nutrition_data and nutrition_data.get("calories", 0) > 0: nutrition_data["source"] = source_name logger.info(f"โœ… Found nutrition data from {source_name} for original '{food_name}'") return nutrition_data except Exception as e: logger.warning(f"โš ๏ธ {source_name} search failed for original '{food_name}': {e}") continue # Try with just the first word as last resort (e.g., "pineapple juice" โ†’ "pineapple") words = cleaned_name.split() if len(words) > 1: first_word = words[0] logger.info(f"๐Ÿ”„ Last resort: trying first word only: '{first_word}'") for source_name, search_func in nutrition_sources: try: nutrition_data = await search_func(first_word) if nutrition_data and nutrition_data.get("calories", 0) > 0: nutrition_data["source"] = f"{source_name} (matched: {first_word})" logger.info(f"โœ… Found nutrition data from {source_name} for '{first_word}'") return nutrition_data except Exception as e: logger.warning(f"โš ๏ธ {source_name} search failed for '{first_word}': {e}") continue # All APIs failed, return default values logger.warning(f"๐Ÿšจ No nutrition data found for '{food_name}' after all attempts, using defaults") default_nutrition = DEFAULT_NUTRITION.copy() default_nutrition["source"] = "Default (APIs unavailable)" return default_nutrition # ==================== TRANSLATION SYSTEM ==================== # In-memory translation cache to reduce API calls translation_cache: Dict[str, Dict[str, str]] = {} # {locale: {english: translated}} # Language code mapping (i18n locale โ†’ full language name) LANGUAGE_MAP = { "en": "English", "bs": "Bosnian", "de": "German", "es": "Spanish", "fr": "French", "it": "Italian", "pt": "Portuguese", "ar": "Arabic", "tr": "Turkish", "nl": "Dutch", "ru": "Russian", "zh": "Chinese", "ja": "Japanese", "ko": "Korean", "hi": "Hindi", "sr": "Serbian", "hr": "Croatian", "sq": "Albanian", "mk": "Macedonian", } # NO HARDCODED TRANSLATIONS - Let models predict naturally async def translate_food_names_batch(food_names: List[str], target_locale: str) -> Dict[str, str]: """ Translate multiple food names in a single API call (COST OPTIMIZATION). Args: food_names: List of food names in English target_locale: Target language code Returns: Dictionary mapping original names to translated names """ # Skip translation if target is English or no OpenAI client if target_locale == "en" or not openai_client or not OPENAI_API_KEY: return {name: name for name in food_names} # Check cache first if target_locale not in translation_cache: translation_cache[target_locale] = {} translations = {} needs_translation = [] # Check cache only - no hardcoded translations for name in food_names: if name in translation_cache[target_locale]: translations[name] = translation_cache[target_locale][name] logger.info(f"๐Ÿ’พ Cache hit: '{name}' โ†’ '{translations[name]}' ({target_locale})") else: needs_translation.append(name) # If all cached, return immediately if not needs_translation: return translations # Get target language name target_language = LANGUAGE_MAP.get(target_locale, target_locale) try: logger.info(f"๐ŸŒ Batch translating {len(needs_translation)} items to {target_language}") # Create batch translation prompt (1 API call for multiple items) food_list = "\n".join(f"{i+1}. {name}" for i, name in enumerate(needs_translation)) response = await openai_client.chat.completions.create( model="gpt-4o-mini", messages=[ { "role": "system", "content": f"You are a professional food translator. Translate food names to {target_language}. Return ONLY the translations, one per line, in the same order. Keep it natural and commonly used." }, { "role": "user", "content": f"Translate these food names to {target_language}:\n{food_list}" } ], max_tokens=150, temperature=0.3, ) translated_lines = response.choices[0].message.content.strip().split('\n') # Parse translations and update cache for i, name in enumerate(needs_translation): if i < len(translated_lines): # Remove numbering if present (e.g., "1. Ananas" โ†’ "Ananas") translated = translated_lines[i].strip() translated = translated.split('. ', 1)[-1] if '. ' in translated else translated translations[name] = translated translation_cache[target_locale][name] = translated logger.info(f"โœ… '{name}' โ†’ '{translated}'") return translations except Exception as e: logger.warning(f"โš ๏ธ Batch translation failed: {e}") # Return originals on failure for name in needs_translation: translations[name] = name return translations async def translate_food_name(food_name: str, target_locale: str) -> str: """ Translate single food name (uses batch function internally for caching). Args: food_name: Food name in English target_locale: Target language code Returns: Translated food name or original if translation fails/not needed """ result = await translate_food_names_batch([food_name], target_locale) return result.get(food_name, food_name) async def translate_description(description: str, target_locale: str) -> str: """ Translate food description to target language using OpenAI with caching. Args: description: Description in English target_locale: Target language code Returns: Translated description or original if translation fails/not needed """ # Skip translation if target is English or no OpenAI client if target_locale == "en" or not openai_client or not OPENAI_API_KEY: return description # Simple cache key (hash of description + locale) cache_key = f"desc_{hash(description)}_{target_locale}" # Check if cached in locale cache if target_locale not in translation_cache: translation_cache[target_locale] = {} if cache_key in translation_cache[target_locale]: logger.info(f"๐Ÿ’พ Description cache hit ({target_locale})") return translation_cache[target_locale][cache_key] # Get target language name target_language = LANGUAGE_MAP.get(target_locale, target_locale) try: logger.info(f"๐ŸŒ Translating description to {target_language}") response = await openai_client.chat.completions.create( model="gpt-4o-mini", messages=[ { "role": "system", "content": f"You are a food description translator. Translate to {target_language}. Keep it natural and concise. Return ONLY the translation." }, { "role": "user", "content": description } ], max_tokens=100, temperature=0.3, ) translated = response.choices[0].message.content.strip() # Cache the result translation_cache[target_locale][cache_key] = translated logger.info(f"โœ… Description translated to {target_language}") return translated except Exception as e: logger.warning(f"โš ๏ธ Description translation failed: {e}") return description # ==================== MULTI-MODEL FOOD RECOGNIZER ==================== class MultiModelFoodRecognizer: """Production-ready multi-model ensemble for comprehensive food recognition.""" def __init__(self, device: str): self.device = device self.models = {} self.processors = {} self.is_loaded = False self.available_models = [] self._initialize_models() self._warm_up() def _initialize_models(self): """Initialize Food-101 specialist ensemble with memory optimization.""" logger.info("๐ŸŽฏ Initializing FOOD-101 SPECIALIST food recognition system with memory optimization...") # MEMORY-AWARE LOADING: Priority-based loading with RAM monitoring sorted_models = sorted(FOOD_MODELS.items(), key=lambda x: x[1]["priority"]) memory_used = 0 memory_limit = 14.5 * 1024 # 14.5GB limit (1.5GB buffer for inference) # Model memory estimates (MB) - UPDATED FOR FOOD-101 SPECIALISTS model_sizes = { "food101_siglip_2025": 400, "food101_deit_2024": 350, "food101_vit_base": 344, "food101_swin": 348, "food101_baseline": 500, "food_categories_enhanced": 300 } for model_key, model_config in sorted_models: estimated_size = model_sizes.get(model_key, 500) # Default 500MB # Memory constraint check if memory_used + estimated_size > memory_limit: logger.warning(f"โš ๏ธ Skipping {model_key} ({estimated_size}MB) - RAM limit reached") continue try: logger.info(f"๐Ÿ”„ Loading {model_key}: {model_config['description']} (~{estimated_size}MB)") model_name = model_config["model_name"] # MEMORY-OPTIMIZED LOADING processor = AutoImageProcessor.from_pretrained(model_name) # Advanced memory optimization for large models load_config = { "use_safetensors": True, "low_cpu_mem_usage": True, "torch_dtype": torch.float16 if self.device == "cuda" else torch.float32 } # GPU-specific optimizations if self.device == "cuda" and estimated_size > 1000: # For models > 1GB load_config["device_map"] = "auto" model = AutoModelForImageClassification.from_pretrained(model_name, **load_config) # Device placement (if not handled by device_map) if "device_map" not in load_config: model = model.to(self.device) model.eval() # FOOD-101 SPECIFIC COMPILATION if hasattr(torch, 'compile') and self.device == "cuda" and "food101" in model_key: try: model = torch.compile(model, mode="reduce-overhead", dynamic=True) logger.info(f"โšก FOOD-101 {model_key} compiled with memory optimization") except Exception as e: logger.info(f"โš ๏ธ Compilation failed for {model_key}: {e}") self.models[model_key] = model self.processors[model_key] = processor self.available_models.append(model_key) memory_used += estimated_size logger.info(f"โœ… {model_key} loaded (Total: {memory_used/1024:.1f}GB / 16GB)") # Aggressive memory cleanup if self.device == "cuda": torch.cuda.empty_cache() torch.cuda.synchronize() except Exception as e: logger.warning(f"โš ๏ธ Failed to load {model_key}: {e}") continue if self.available_models: self.is_loaded = True logger.info(f"๐ŸŽฏ Multi-model system ready with {len(self.available_models)} models: {self.available_models}") else: raise RuntimeError("โŒ No models could be loaded!") def _warm_up(self): """Warm up all loaded models.""" if not self.available_models: return try: logger.info("๐Ÿ”ฅ Warming up all models...") # Create dummy image dummy_image = Image.new('RGB', (224, 224), color='red') for model_key in self.available_models: try: processor = self.processors[model_key] model = self.models[model_key] with torch.no_grad(): inputs = processor(images=dummy_image, return_tensors="pt") inputs = {k: v.to(self.device) for k, v in inputs.items()} _ = model(**inputs) logger.info(f"โœ… {model_key} warmed up") except Exception as e: logger.warning(f"โš ๏ธ Warm-up failed for {model_key}: {e}") # Clean up del dummy_image if self.device == "cuda": torch.cuda.empty_cache() gc.collect() logger.info("โœ… All models warm-up completed") except Exception as e: logger.warning(f"โš ๏ธ Model warm-up failed: {e}") def _predict_with_model(self, image: Image.Image, model_key: str, top_k: int = 5) -> Optional[List[Dict[str, Any]]]: """Predict with a specific model.""" try: if model_key not in self.available_models: return None processor = self.processors[model_key] model = self.models[model_key] # Preprocess image processed_image = preprocess_image(image) # Prepare inputs inputs = processor(images=processed_image, return_tensors="pt") inputs = {k: v.to(self.device) for k, v in inputs.items()} # Inference with torch.no_grad(): outputs = model(**inputs) logits = outputs.logits probs = F.softmax(logits, dim=-1).cpu().numpy()[0] # Get top K predictions top_indices = np.argsort(probs)[::-1][:top_k] predictions = [] for idx in top_indices: # Handle different model output formats if hasattr(model.config, 'id2label') and str(idx) in model.config.id2label: label = model.config.id2label[str(idx)] elif hasattr(model.config, 'id2label') and idx in model.config.id2label: label = model.config.id2label[idx] else: label = f"class_{idx}" confidence = float(probs[idx]) # SMART CATEGORY MAPPING for different models mapped_label = label boosted_confidence = confidence # NOISYVIT 2025 ENSEMBLE - STATE-OF-THE-ART FOOD RECOGNITION if model_key in ["noisyvit_2025_huge", "noisyvit_2025_large", "noisyvit_2025_base_384"]: # NOISYVIT 2025 FLAGSHIP MODELS - Maximum priority and robustness clean_name = label.replace("_", " ").title() noisyvit_multiplier = { "noisyvit_2025_huge": 2.5, # 150% boost - Ultimate model "noisyvit_2025_large": 2.3, # 130% boost - Advanced robustness "noisyvit_2025_base_384": 2.1 # 110% boost - High-resolution } boosted_confidence = min(confidence * noisyvit_multiplier[model_key], 1.0) logger.info(f"๐ŸŽฏ NOISYVIT 2025 {model_key}: {label} โ†’ {clean_name} ({boosted_confidence:.1%}) [NOISE-RESILIENT]") elif model_key in ["food101_vit_specialist", "food_enhanced_classifier"]: # FOOD-101 SPECIALISTS - High trust for specific food categories clean_name = label.replace("_", " ").title() boosted_confidence = min(confidence * 2.2, 1.0) # 120% boost for food specialists logger.info(f"๐Ÿฝ๏ธ FOOD SPECIALIST {model_key}: {label} โ†’ {clean_name} ({boosted_confidence:.1%})") elif model_key in ["multi_object_vit", "scene_understanding_vit"]: # MULTI-OBJECT SCENE DETECTION - Excellent for complex food scenes clean_name = label.replace("_", " ").title() boosted_confidence = min(confidence * 2.0, 1.0) # 100% boost for multi-object detection logger.info(f"๐Ÿ” MULTI-OBJECT {model_key}: {label} โ†’ {clean_name} ({boosted_confidence:.1%}) [COMPLEX SCENES]") elif model_key in ["food_clip_huge", "openai_clip_large"]: # VISION-LANGUAGE MODELS - Advanced understanding for complex food descriptions clean_name = label.replace("_", " ").title() clip_food_multiplier = {"food_clip_huge": 2.4, "openai_clip_large": 2.1} boosted_confidence = min(confidence * clip_food_multiplier[model_key], 1.0) logger.info(f"๐Ÿง  FOOD CLIP {model_key}: {label} โ†’ {clean_name} ({boosted_confidence:.1%}) [VISION-LANGUAGE]") elif model_key in ["convnext_xxlarge", "efficientnet_ultra"]: # CUTTING-EDGE ARCHITECTURES - Latest food recognition technology clean_name = label.replace("_", " ").title() arch_multiplier = {"convnext_xxlarge": 2.2, "efficientnet_ultra": 1.9} boosted_confidence = min(confidence * arch_multiplier[model_key], 1.0) logger.info(f"๐Ÿš€ CUTTING-EDGE {model_key}: {label} โ†’ {clean_name} ({boosted_confidence:.1%}) [LATEST TECH]") elif model_key == "resnet_deep_food": # MEMORY-EFFICIENT BASELINE - Reliable backup clean_name = label.replace("_", " ").title() boosted_confidence = min(confidence * 1.6, 1.0) # 60% boost for efficient baseline logger.info(f"๐Ÿ—๏ธ EFFICIENT BASELINE {model_key}: {label} โ†’ {clean_name} ({boosted_confidence:.1%})") else: # Unknown model fallback clean_name = label.replace("_", " ").title() boosted_confidence = confidence predictions.append({ "label": clean_name, "raw_label": mapped_label, "confidence": boosted_confidence, "confidence_pct": f"{boosted_confidence:.1%}", "model": model_key, "model_type": FOOD_MODELS[model_key]["type"] }) # Clean up memory del inputs, outputs, logits, probs if self.device == "cuda": torch.cuda.empty_cache() return predictions except Exception as e: logger.warning(f"โš ๏ธ Prediction failed for {model_key}: {e}") return None def predict(self, image: Image.Image, top_k: int = 5) -> Dict[str, Any]: """Main predict method - uses ensemble if available, fallback to primary.""" return self.predict_ensemble(image, top_k) def predict_ensemble(self, image: Image.Image, top_k: int = 10) -> Dict[str, Any]: """Ensemble prediction using all available models with smart filtering.""" if not self.is_loaded: raise RuntimeError("Models not loaded") all_predictions = [] model_results = {} # NOISYVIT 2025 ENSEMBLE - Optimized for complex multi-object food scenes predictions_per_model = 75 # Increased for complex scene analysis # PRIORITY-BASED PREDICTION GENERATION for model_key in self.available_models: # Higher prediction count for NoisyViT models (better for complex scenes) if "noisyvit" in model_key: current_predictions = 100 # More predictions for NoisyViT robustness elif "multi_object" in model_key or "scene_understanding" in model_key: current_predictions = 90 # High for multi-object detection elif "clip" in model_key: current_predictions = 85 # High for vision-language understanding else: current_predictions = predictions_per_model predictions = self._predict_with_model(image, model_key, current_predictions) if predictions: model_results[model_key] = predictions all_predictions.extend(predictions) # Enhanced logging for different model types if "noisyvit" in model_key: logger.info(f"๐ŸŽฏ NOISYVIT {model_key}: {len(predictions)} robust predictions [NOISE-RESILIENT]") elif "multi_object" in model_key: logger.info(f"๐Ÿ” MULTI-OBJECT {model_key}: {len(predictions)} scene predictions [COMPLEX SCENES]") elif "clip" in model_key: logger.info(f"๐Ÿง  CLIP {model_key}: {len(predictions)} vision-language predictions") else: logger.info(f"๐Ÿฝ๏ธ {model_key}: {len(predictions)} food predictions") total_predictions = len(all_predictions) logger.info(f"๐Ÿš€ NOISYVIT ENSEMBLE: {total_predictions} total predictions from {len(self.available_models)} models") if not all_predictions: raise RuntimeError("No models produced valid predictions") # ULTRA-CONSERVATIVE FILTERING - Only remove obvious non-food for Food-101 specialists non_food_items = { # Minimal filtering since Food-101 models are trained on food only 'person', 'people', 'human', 'man', 'woman', 'child', 'car', 'truck', 'vehicle', 'building', 'house', 'computer', 'phone', 'laptop', 'tablet', 'television', 'tv', 'book', 'paper', 'pen', 'pencil', 'chair', 'table', 'sofa', 'cat', 'dog', 'bird' # live animals only (removed 'fish' since it can be food) } # Generic FOOD terms that should be deprioritized (but not removed) generic_terms = { 'fruit', 'vegetable', 'food', 'meal', 'snack', 'dessert', 'salad', 'soup', 'drink', 'beverage', 'meat', 'seafood', 'bread', 'pastry', 'cake', 'cookie', 'candy', 'chocolate' } # ULTIMATE FOOD RECOGNITION - PRIORITY BOOST for specific dishes (CORRECTED) specific_dishes = { # BREAKFAST FOODS (Critical - your pancake example!) - NEVER DESSERT! 'pancakes', 'american pancakes', 'fluffy pancakes', 'buttermilk pancakes', 'blueberry pancakes', 'chocolate chip pancakes', 'banana pancakes', 'waffles', 'belgian waffles', 'french toast', 'crepes', 'omelet', 'scrambled eggs', 'fried eggs', 'eggs benedict', 'breakfast burrito', # Fast food & popular dishes (CRITICAL FIXES) 'fish and chips', 'fish & chips', 'fried fish', 'fish fillet', 'hamburger', 'cheeseburger', 'burger', 'sandwich', 'club sandwich', 'pizza', 'pepperoni pizza', 'margherita pizza', 'hawaiian pizza', 'pasta', 'spaghetti', 'linguine', 'fettuccine', 'lasagna', 'risotto', 'sushi', 'sashimi', 'california roll', 'ramen', 'pho', 'pad thai', 'curry', 'chicken curry', 'biryani', 'tikka masala', 'butter chicken', 'tacos', 'fish tacos', 'chicken tacos', 'beef tacos', 'carnitas', 'burrito', 'quesadilla', 'nachos', 'enchilada', 'fajitas', 'fried chicken', 'chicken wings', 'buffalo wings', 'chicken nuggets', 'french fries', 'fries', 'sweet potato fries', 'onion rings', 'hot dog', 'corn dog', 'bratwurst', 'sausage', 'kielbasa', # Balkanska jela (sa alternativnim imenima) - ENHANCED for ฤ‡evapi detection 'cevapi', 'cevapcici', 'ฤ‡evapi', 'ฤ‡evapฤiฤ‡i', 'chevapi', 'chevapchichi', 'burek', 'bรถrek', 'pljeskavica', 'sarma', 'klepe', 'dolma', 'kajmak', 'ajvar', 'kofte', 'raznjici', 'grilled meat', 'balkan sausage', 'prebranac', 'pasulj', 'grah', 'punjena paprika', 'punjene paprike', 'stuffed peppers', 'musaka', 'moussaka', 'japrak', 'bamija', 'okra', 'bosanski lonac', 'begova corba', 'tarhana', 'zeljanica', 'spinach pie', 'sirnica', 'cheese pie', 'krompiruลกa', 'potato pie', 'gibanica', 'banica', # Steaks & BBQ 'steak', 'ribeye', 'filet mignon', 'sirloin', 't-bone', 'porterhouse', 'ribs', 'bbq ribs', 'pork ribs', 'beef ribs', 'pulled pork', 'brisket', # International specialties 'schnitzel', 'wiener schnitzel', 'paella', 'seafood paella', 'falafel', 'hummus', 'gyros', 'kebab', 'shish kebab', 'shawarma', 'spring rolls', 'summer rolls', 'dim sum', 'dumplings', 'wontons', 'tempura', 'teriyaki', 'yakitori', 'miso soup', 'tom yum', # Desserts 'cheesecake', 'chocolate cake', 'vanilla cake', 'tiramisu', 'apple pie', 'pumpkin pie', 'brownie', 'chocolate chip cookie', 'ice cream', 'gelato', 'donut', 'croissant', 'danish', 'eclair' } # Ensemble voting: weight by model priority and confidence food_scores = {} filtered_count = 0 for pred in all_predictions: food_label_lower = pred["raw_label"].lower().replace("_", " ") # ULTIMATE FILTERING - Remove garbage predictions and non-food items is_non_food = any(non_food in food_label_lower for non_food in non_food_items) # Additional checks for garbage predictions is_garbage_prediction = ( # Check for "Oznaka X" pattern food_label_lower.startswith('oznaka') or food_label_lower.startswith('label') or food_label_lower.startswith('class') or # Very short meaningless names (len(food_label_lower) <= 2) or # Numbers only food_label_lower.isdigit() or # Very low confidence on unknown terms (pred["confidence"] < 0.4 and food_label_lower not in COMPREHENSIVE_FOOD_CATEGORIES) ) if is_non_food or is_garbage_prediction: filtered_count += 1 logger.info(f"๐Ÿšซ Filtered garbage/non-food: '{pred['raw_label']}'") continue # Skip this prediction entirely model_key = pred["model"] priority_weight = 1.0 / FOOD_MODELS[model_key]["priority"] # Higher priority = lower number = higher weight confidence_weight = pred["confidence"] # ULTIMATE SMART SCORING - Maximum accuracy for known dishes is_generic = any(generic in food_label_lower for generic in generic_terms) is_specific = any(dish in food_label_lower for dish in specific_dishes) is_single_generic = food_label_lower in generic_terms # Check if it's a known dish from our comprehensive database is_known_food = any(known in food_label_lower for known in COMPREHENSIVE_FOOD_CATEGORIES) # INTELLIGENT FOOD PRIORITY SYSTEM - Ultra-precise detection is_pancake_related = any(pancake_term in food_label_lower for pancake_term in ['pancake', 'waffle', 'french_toast', 'crepe', 'beignet']) is_fish_and_chips = any(fish_term in food_label_lower for fish_term in ['fish_and_chips', 'fish and chips', 'fried_fish', 'fish fillet']) is_balkan_meat = any(balkan_term in food_label_lower for balkan_term in ['cevapi', 'cevapcici', 'pljeskavica', 'kebab']) is_bread_related = any(bread_term in food_label_lower for bread_term in ['burek', 'bread', 'sandwich', 'toast']) # CRITICAL: Detect if it's wrongly classified as dessert when it's breakfast is_wrong_dessert = (any(breakfast_term in food_label_lower for breakfast_term in ['pancake', 'waffle', 'french_toast']) and 'dessert' in food_label_lower) # Calculate score multiplier with ULTRA-SMART FOOD PRIORITY if is_wrong_dessert: # MASSIVE PENALTY for wrongly classified breakfast as dessert score_multiplier = 0.01 # 99% PENALTY for wrong dessert classification! logger.info(f"โŒ WRONG DESSERT PENALTY: {pred['raw_label']} (99% penalty - breakfast wrongly classified as dessert)") elif is_pancake_related: # MAXIMUM BOOST for pancake-related items score_multiplier = 6.0 # 500% BOOST for pancakes!!! logger.info(f"๐Ÿฅž PANCAKE PRIORITY: {pred['raw_label']} (6x MEGA boost)") elif is_fish_and_chips: # MEGA BOOST for fish and chips (often misclassified) score_multiplier = 5.0 # 400% BOOST for fish and chips!!! logger.info(f"๐ŸŸ FISH & CHIPS PRIORITY: {pred['raw_label']} (5x MEGA boost)") elif is_balkan_meat: # MEGA BOOST for Balkan meat dishes score_multiplier = 4.0 # 300% BOOST for ฤ‡evapi/pljeskavica!!! logger.info(f"๐Ÿฅฉ BALKAN MEAT PRIORITY: {pred['raw_label']} (4x boost)") elif is_bread_related: # BOOST for bread dishes (burek, etc.) score_multiplier = 3.0 # 200% BOOST for bread dishes logger.info(f"๐Ÿฅ– BREAD PRIORITY: {pred['raw_label']} (3x boost)") elif is_specific: # MEGA BOOST for specific dishes we know well score_multiplier = 3.0 # 200% BOOST for specific dishes! logger.info(f"๐ŸŽฏ SPECIFIC DISH DETECTED: {pred['raw_label']} (3x boost)") elif is_known_food and confidence_weight > 0.3: # Good boost for known foods with decent confidence score_multiplier = 2.0 # 100% boost for known foods logger.info(f"โœ… KNOWN FOOD: {pred['raw_label']} (2x boost)") elif is_single_generic: # Heavy penalty for single generic terms score_multiplier = 0.05 # 95% penalty for generic terms like "food", "meat" logger.info(f"โŒ GENERIC TERM: {pred['raw_label']} (95% penalty)") elif is_generic: # Medium penalty for generic descriptions score_multiplier = 0.3 # 70% penalty for generic terms logger.info(f"โš ๏ธ GENERIC: {pred['raw_label']} (70% penalty)") elif confidence_weight > 0.7: # Bonus for high-confidence predictions score_multiplier = 1.5 # 50% boost for high confidence logger.info(f"๐Ÿ’ช HIGH CONFIDENCE: {pred['raw_label']} (1.5x boost)") else: score_multiplier = 1.0 # Normal score combined_score = priority_weight * confidence_weight * score_multiplier food_name = pred["raw_label"] if food_name not in food_scores: food_scores[food_name] = { "total_score": 0, "count": 0, "best_prediction": pred, "models": [], "is_generic": is_generic, "is_specific": is_specific } food_scores[food_name]["total_score"] += combined_score food_scores[food_name]["count"] += 1 food_scores[food_name]["models"].append(model_key) # Keep the prediction with highest confidence as representative if pred["confidence"] > food_scores[food_name]["best_prediction"]["confidence"]: food_scores[food_name]["best_prediction"] = pred if filtered_count > 0: logger.info(f"โœ… Filtered out {filtered_count} non-food items") # Sort by ensemble score sorted_foods = sorted( food_scores.items(), key=lambda x: x[1]["total_score"], reverse=True ) # Format final results - return MORE alternatives (up to top_k) final_predictions = [] for food_name, data in sorted_foods[:top_k * 2]: # Get double to have enough after filtering pred = data["best_prediction"].copy() pred["ensemble_score"] = data["total_score"] pred["model_count"] = data["count"] pred["contributing_models"] = data["models"] pred["is_generic"] = data["is_generic"] pred["is_specific"] = data["is_specific"] final_predictions.append(pred) # STRICT CONFIDENCE FILTERING - Only high quality predictions filtered_predictions = [] seen_labels = set() for pred in final_predictions: label_lower = pred["raw_label"].lower().replace("_", " ").strip() # STRICT CONFIDENCE CHECK - Minimum 15% confidence if pred["confidence"] < MIN_CONFIDENCE_THRESHOLD: logger.info(f"โŒ LOW CONFIDENCE FILTERED: {pred['raw_label']} ({pred['confidence']:.1%})") continue # DOUBLE CHECK: Filter non-food items again is_non_food = any(non_food in label_lower for non_food in non_food_items) if is_non_food: continue # Skip non-food items # Skip if we've already seen very similar label if label_lower not in seen_labels: filtered_predictions.append(pred) seen_labels.add(label_lower) logger.info(f"โœ… ACCEPTED: {pred['raw_label']} ({pred['confidence']:.1%})") if len(filtered_predictions) >= top_k: break # FINAL VALIDATION - Prevent obvious classification errors validated_predictions = [] for pred in filtered_predictions: label_lower = pred["raw_label"].lower().replace("_", " ") # CRITICAL VALIDATION RULES validation_passed = True validation_reason = "" # Rule 1: Pancakes should NEVER be classified as dessert if any(breakfast_term in label_lower for breakfast_term in ['pancake', 'waffle', 'french_toast']) and \ any(dessert_term in label_lower for dessert_term in ['dessert', 'cake', 'sweet']): validation_passed = False validation_reason = "Breakfast item wrongly classified as dessert" # Rule 2: Fish and chips should be recognized as specific dish, not generic "fried food" if 'fish' in label_lower and 'chip' in label_lower and pred["confidence"] > 0.3: # This is clearly fish and chips - boost it! pred["confidence"] = min(pred["confidence"] * 1.5, 1.0) pred["label"] = "Fish and Chips" logger.info(f"๐ŸŸ FISH & CHIPS VALIDATION BOOST: {pred['confidence']:.1%}") # Rule 3: Natural validation - no hardcoded replacements if label_lower in ['food', 'meal', 'dish', 'object', 'item']: # Generic terms get penalty but no forced replacement pred["confidence"] *= 0.5 # 50% penalty for being too generic logger.info(f"โš ๏ธ GENERIC TERM PENALTY: {label_lower}") if validation_passed: validated_predictions.append(pred) else: logger.info(f"โŒ VALIDATION FAILED: {pred['raw_label']} - {validation_reason}") # Use validated predictions filtered_predictions = validated_predictions # PRIMARY RESULT with REAL MODEL PREDICTIONS ONLY if not filtered_predictions: # NO HARDCODED RESPONSES - Return error for manual input logger.warning("โŒ NO CONFIDENT PREDICTIONS FOUND - All predictions below threshold") return { "success": False, "error": "No confident food predictions found", "message": "Please try a clearer image or different angle", "confidence_threshold": MIN_CONFIDENCE_THRESHOLD, "alternatives": [], "system_info": { "available_models": self.available_models, "device": self.device.upper(), "total_classes": sum(FOOD_MODELS[m]["classes"] for m in self.available_models) } } primary = filtered_predictions[0] # CRITICAL FIX: ALWAYS use the prediction with HIGHEST confidence as primary # (regardless of is_generic flag - confidence is king!) if len(filtered_predictions) > 1: # Find prediction with highest confidence max_conf_idx = 0 max_conf = filtered_predictions[0].get("confidence", 0) for i, pred in enumerate(filtered_predictions[1:], 1): pred_conf = pred.get("confidence", 0) if pred_conf > max_conf: max_conf = pred_conf max_conf_idx = i # Swap if we found a better one if max_conf_idx > 0: filtered_predictions[0], filtered_predictions[max_conf_idx] = \ filtered_predictions[max_conf_idx], filtered_predictions[0] primary = filtered_predictions[0] logger.info(f"๐Ÿ”„ Swapped to highest confidence: {primary['label']} ({primary['confidence']:.1%})") # Note: Generic vs specific check removed - confidence is the only metric that matters # FILTER ALTERNATIVES by confidence - Only show good alternatives quality_alternatives = [] for alt in filtered_predictions[1:]: if alt["confidence"] >= MIN_ALTERNATIVE_CONFIDENCE: quality_alternatives.append(alt) if len(quality_alternatives) >= MAX_ALTERNATIVES: break return { "success": True, "label": primary["label"], "confidence": primary["confidence"], "primary_label": primary["raw_label"], "ensemble_score": primary.get("ensemble_score", 0), "alternatives": quality_alternatives, # Only high-confidence alternatives "model_results": model_results, "system_info": { "available_models": self.available_models, "device": self.device.upper(), "total_classes": sum(FOOD_MODELS[m]["classes"] for m in self.available_models), "confidence_thresholds": { "minimum": MIN_CONFIDENCE_THRESHOLD, "alternatives": MIN_ALTERNATIVE_CONFIDENCE } } } # ==================== LIFESPAN EVENTS ==================== @asynccontextmanager async def lifespan(app: FastAPI): """Application lifespan manager.""" # Startup logger.info("๐Ÿš€ Application startup complete") logger.info("=" * 60) logger.info("โœ… API READY FOR PRODUCTION") logger.info(f"๐Ÿ“ก Endpoints: /api/nutrition/analyze-food, /analyze") logger.info(f"๐Ÿ–ฅ๏ธ Device: {device.upper()}") logger.info(f"๐Ÿ“Š Models: {len(recognizer.available_models)} active models") logger.info(f"๐ŸŽฏ Total Food Categories: {sum(FOOD_MODELS[m]['classes'] for m in recognizer.available_models)}") logger.info(f"๐ŸŒ Translations: {'โœ… Enabled' if openai_client else 'โŒ Disabled'}") logger.info("=" * 60) yield # Shutdown logger.info("๐Ÿ”„ Shutting down...") # Cleanup GPU memory if device == "cuda": torch.cuda.empty_cache() # Garbage collection gc.collect() logger.info("โœ… Cleanup completed") # ==================== FASTAPI SETUP ==================== logger.info("=" * 60) logger.info("๐Ÿฝ๏ธ PRODUCTION AI FOOD RECOGNITION API") logger.info("=" * 60) # Initialize multi-model system device = select_device() recognizer = MultiModelFoodRecognizer(device) # Initialize OpenAI client BEFORE FastAPI app if OPENAI_API_KEY: try: openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY) logger.info(f"โœ… OpenAI client initialized (key: {OPENAI_API_KEY[:20]}...)") except Exception as e: logger.warning(f"โš ๏ธ OpenAI client initialization failed: {e}") openai_client = None else: logger.warning("โš ๏ธ OpenAI API key not found - translations disabled") # Create FastAPI app app = FastAPI( title="AI Food Recognition API", description="Production-ready food recognition with 101 categories (Food-101 dataset)", version="2.0.0", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan ) # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["GET", "POST", "OPTIONS"], allow_headers=["*"], ) # ==================== MIDDLEWARE ==================== @app.middleware("http") async def add_security_headers(request: Request, call_next): response = await call_next(request) response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" return response # ==================== UTILITY FUNCTIONS ==================== async def validate_and_read_image(file: UploadFile) -> Image.Image: """Validate and read uploaded image file.""" # Check file size if hasattr(file, 'size') and file.size > MAX_FILE_SIZE: raise HTTPException(status_code=413, detail="File too large (max 10MB)") # Check content type if file.content_type not in ALLOWED_TYPES: raise HTTPException( status_code=400, detail=f"Invalid file type. Allowed: {', '.join(ALLOWED_TYPES)}" ) try: # Read and validate image contents = await file.read() if len(contents) > MAX_FILE_SIZE: raise HTTPException(status_code=413, detail="File too large (max 10MB)") image = Image.open(BytesIO(contents)) return image except Exception as e: raise HTTPException(status_code=400, detail=f"Invalid image file: {str(e)}") # ==================== API ENDPOINTS ==================== @app.get("/") def root(): """Root endpoint with API information.""" return { "message": "๐Ÿฝ๏ธ AI Food Recognition API", "status": "online", "version": "2.0.0", "models": recognizer.available_models if recognizer.is_loaded else [], "total_categories": sum(FOOD_MODELS[m]["classes"] for m in recognizer.available_models) if recognizer.is_loaded else 0, "device": device.upper(), "endpoints": { "POST /api/nutrition/analyze-food": "Analyze food image (Next.js frontend)", "POST /analyze": "Analyze food image (Hugging Face Spaces)", "GET /health": "Health check", "GET /docs": "API documentation" } } @app.get("/health") def health_check(): """Comprehensive health check.""" return { "status": "healthy" if recognizer.is_loaded else "error", "models_loaded": recognizer.is_loaded, "available_models": recognizer.available_models if recognizer.is_loaded else [], "model_count": len(recognizer.available_models) if recognizer.is_loaded else 0, "total_categories": sum(FOOD_MODELS[m]["classes"] for m in recognizer.available_models) if recognizer.is_loaded else 0, "device": device.upper(), "memory_usage": f"{torch.cuda.memory_allocated() / 1024**2:.1f}MB" if device == "cuda" else "N/A" } @app.post("/api/nutrition/analyze-food") async def analyze_food_nutrition(request: Request, file: UploadFile = File(None)): """ Analyze food image or manual entry for Next.js frontend. Supports two modes: 1. Image upload: AI recognition + nutrition lookup 2. Manual entry: Direct nutrition lookup by food name Returns nutrition-focused response format with translations. """ try: # Parse form data form_data = await request.form() manual_input = form_data.get("manualInput", "false").lower() == "true" locale = form_data.get("locale", "en") # Get user's language preference logger.info(f"๐Ÿ“ฅ Request received - Mode: {'Manual' if manual_input else 'Image'}, Locale: {locale}") # MODE 1: Manual food entry (from alternatives or manual input) if manual_input: food_name = form_data.get("manualFoodName") serving_size = form_data.get("manualServingSize", "100") serving_unit = form_data.get("manualServingUnit", "g") description = form_data.get("manualDescription", "") if not food_name: raise HTTPException(status_code=400, detail="manualFoodName is required for manual entry") logger.info(f"๐Ÿฝ๏ธ Manual nutrition lookup: {food_name} ({serving_size}{serving_unit})") # Direct nutrition API lookup nutrition_data = await get_nutrition_from_apis(food_name) if not nutrition_data or nutrition_data.get("calories", 0) == 0: raise HTTPException( status_code=404, detail=f"Failed to retrieve nutrition information for manual entry" ) source = nutrition_data.get("source", "Unknown") logger.info(f"โœ… Manual lookup: {food_name} | Nutrition: {source}") # Translate food name and description translated_name = await translate_food_name(food_name, locale) base_description = description or f"Manual entry: {food_name}" translated_description = await translate_description(base_description, locale) # Return manual entry format return JSONResponse(content={ "data": { "label": translated_name, "confidence": 1.0, # Manual entry has 100% confidence "nutrition": { "calories": nutrition_data["calories"], "protein": nutrition_data["protein"], "carbs": nutrition_data["carbs"], "fat": nutrition_data["fat"] }, "servingSize": serving_size, "servingUnit": serving_unit, "description": translated_description, "alternatives": [], # No alternatives for manual entry "source": f"{source} Database", "isManualEntry": True } }) # MODE 2: Image upload (AI recognition) else: if not file: raise HTTPException(status_code=400, detail="File is required for image analysis") logger.info(f"๐Ÿฝ๏ธ Image analysis request: {file.filename}") # Validate and process image image = await validate_and_read_image(file) # Step 1: AI Model Prediction with strict confidence filtering results = recognizer.predict(image, top_k=10) # Check if prediction was successful if not results.get("success", True): raise HTTPException( status_code=422, detail=f"Food recognition failed: {results.get('message', 'Unknown error')}" ) # Step 2: API Nutrition Lookup nutrition_data = await get_nutrition_from_apis(results["primary_label"]) # Log result confidence_pct = f"{results['confidence']:.1%}" source = nutrition_data.get("source", "Unknown") logger.info(f"โœ… Prediction: {results['label']} ({confidence_pct}) | Nutrition: {source}") # BATCH TRANSLATION OPTIMIZATION: Translate all food names at once if locale != "en" and openai_client: # Collect all names to translate (primary + alternatives) names_to_translate = [results["label"]] if results.get("alternatives"): names_to_translate.extend([ alt.get("label", alt.get("raw_label", "")) for alt in results["alternatives"] ]) # Single API call for all translations translations = await translate_food_names_batch(names_to_translate, locale) # Apply translations translated_name = translations.get(results["label"], results["label"]) # Translate description base_description = f"{results['label']} identified with {int(results['confidence'] * 100)}% confidence" translated_description = await translate_description(base_description, locale) # Map alternatives with translations translated_alternatives = [] if results.get("alternatives"): for alt in results["alternatives"]: alt_name = alt.get("label", alt.get("raw_label", "")) translated_alternatives.append({ **alt, "label": translations.get(alt_name, alt_name), "original_label": alt_name }) else: # No translation needed translated_name = results["label"] translated_description = f"{results['label']} identified with {int(results['confidence'] * 100)}% confidence" translated_alternatives = results["alternatives"] # Return frontend-expected format return JSONResponse(content={ "data": { "label": translated_name, "confidence": results["confidence"], "description": translated_description, # Translated description "nutrition": { "calories": nutrition_data["calories"], "protein": nutrition_data["protein"], "carbs": nutrition_data["carbs"], "fat": nutrition_data["fat"] }, "alternatives": translated_alternatives, "source": f"AI Recognition + {source} Database", "isManualEntry": False, "locale": locale # Return locale for debugging } }) except HTTPException: raise except Exception as e: logger.error(f"โŒ Analysis failed: {e}") raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}") @app.post("/analyze") async def analyze_food_spaces(file: UploadFile = File(...)): """ Analyze food image for Hugging Face Spaces interface. Returns detailed response with model info. """ logger.info(f"๐Ÿš€ HF Spaces analysis request: {file.filename}") try: # Validate and process image image = await validate_and_read_image(file) # Step 1: AI Model Prediction (request top 10 for more alternatives) results = recognizer.predict(image, top_k=10) # Step 2: API Nutrition Lookup nutrition_data = await get_nutrition_from_apis(results["primary_label"]) # Log result confidence_pct = f"{results['confidence']:.1%}" source = nutrition_data.get("source", "Unknown") logger.info(f"โœ… Prediction: {results['label']} ({confidence_pct}) | Nutrition: {source}") # Return full response with nutrition data enhanced_results = results.copy() enhanced_results["nutrition"] = nutrition_data enhanced_results["data_source"] = source return JSONResponse(content=enhanced_results) except HTTPException: raise except Exception as e: logger.error(f"โŒ Analysis failed: {e}") raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}") # ==================== MAIN ==================== if __name__ == "__main__": port = int(os.environ.get("PORT", 7860)) logger.info("๐ŸŽฏ Starting production server...") uvicorn.run( app, host="0.0.0.0", port=port, log_level="info", access_log=True )