har1zarD commited on
Commit
9ccc31e
·
1 Parent(s): 213ba68
Files changed (3) hide show
  1. Dockerfile +51 -19
  2. app.py +544 -80
  3. requirements.txt +3 -0
Dockerfile CHANGED
@@ -1,14 +1,22 @@
1
- # Advanced Food Recognition API - Optimized for HF Spaces
 
 
 
2
  FROM python:3.11-slim
3
 
4
- # Create user for Hugging Face Spaces
 
 
 
 
 
5
  RUN useradd -m -u 1000 user
6
 
7
  # Set working directory
8
  WORKDIR /app
9
 
10
- # Install system dependencies for advanced image processing
11
- RUN apt-get update && apt-get install -y \
12
  gcc \
13
  g++ \
14
  libglib2.0-0 \
@@ -19,30 +27,48 @@ RUN apt-get update && apt-get install -y \
19
  libgl1-mesa-dev \
20
  libglib2.0-dev \
21
  curl \
22
- && rm -rf /var/lib/apt/lists/*
 
23
 
24
- # Copy requirements first (for better caching)
25
  COPY --chown=user:user requirements.txt .
26
 
27
- # Install NumPy 1.x first to ensure compatibility
28
- RUN pip install --no-cache-dir "numpy>=1.24.0,<2.0.0"
 
 
29
 
30
- # Install remaining Python dependencies as root
31
  RUN pip install --no-cache-dir -r requirements.txt
32
 
33
- # Copy application code with correct ownership
34
- COPY --chown=user:user app.py app.py
35
 
36
- # Create cache directory with correct permissions
37
- RUN mkdir -p /home/user/.cache /tmp/transformers /tmp/huggingface /tmp/torch && chown -R user:user /home/user/.cache /tmp/transformers /tmp/huggingface /tmp/torch
 
 
 
 
 
38
 
39
- # Switch to non-root user
40
  USER user
41
 
42
- # Set environment variables
 
 
 
43
  ENV PYTHONUNBUFFERED=1
 
 
 
44
  ENV PORT=7860
 
 
45
  ENV HOME=/home/user
 
 
46
  ENV HF_HOME=/tmp/huggingface
47
  ENV TRANSFORMERS_CACHE=/tmp/transformers
48
  ENV XDG_CACHE_HOME=/tmp
@@ -50,17 +76,23 @@ ENV TORCH_HOME=/tmp/torch
50
  ENV HF_HUB_DISABLE_TELEMETRY=1
51
  ENV HF_HUB_ENABLE_HF_TRANSFER=0
52
 
 
 
 
 
 
 
53
  # Performance optimizations
54
  ENV TOKENIZERS_PARALLELISM=false
55
  ENV OMP_NUM_THREADS=2
56
  ENV MKL_NUM_THREADS=2
57
 
58
- # Expose port (7860 for Hugging Face Spaces)
59
  EXPOSE 7860
60
 
61
- # Health check for container monitoring
62
- HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
63
  CMD curl -f http://localhost:7860/health || exit 1
64
 
65
- # Run the optimized food recognition API
66
  CMD ["python", "app.py"]
 
1
+ # ============================================================
2
+ # 🍽️ Trainera Food Recognition API
3
+ # Production-Ready Multilingual AI Food Recognition
4
+ # ============================================================
5
  FROM python:3.11-slim
6
 
7
+ # Metadata
8
+ LABEL maintainer="Trainera Team"
9
+ LABEL description="AI Food Recognition API with OpenAI translations (101+ food categories)"
10
+ LABEL version="2.0.0"
11
+
12
+ # Create non-root user for security (HF Spaces requirement)
13
  RUN useradd -m -u 1000 user
14
 
15
  # Set working directory
16
  WORKDIR /app
17
 
18
+ # Install system dependencies for ML and image processing
19
+ RUN apt-get update && apt-get install -y --no-install-recommends \
20
  gcc \
21
  g++ \
22
  libglib2.0-0 \
 
27
  libgl1-mesa-dev \
28
  libglib2.0-dev \
29
  curl \
30
+ && rm -rf /var/lib/apt/lists/* \
31
+ && apt-get clean
32
 
33
+ # Copy requirements first (Docker layer caching optimization)
34
  COPY --chown=user:user requirements.txt .
35
 
36
+ # Install Python dependencies
37
+ # Step 1: Install NumPy 1.x first (transformers compatibility)
38
+ RUN pip install --no-cache-dir --upgrade pip && \
39
+ pip install --no-cache-dir "numpy>=1.24.0,<2.0.0"
40
 
41
+ # Step 2: Install remaining dependencies
42
  RUN pip install --no-cache-dir -r requirements.txt
43
 
44
+ # Copy application code
45
+ COPY --chown=user:user app.py .
46
 
47
+ # Create cache directories with correct permissions
48
+ RUN mkdir -p \
49
+ /home/user/.cache \
50
+ /tmp/transformers \
51
+ /tmp/huggingface \
52
+ /tmp/torch \
53
+ && chown -R user:user /home/user/.cache /tmp/transformers /tmp/huggingface /tmp/torch
54
 
55
+ # Switch to non-root user (security best practice)
56
  USER user
57
 
58
+ # Environment Variables
59
+ # ============================================================
60
+
61
+ # Python configuration
62
  ENV PYTHONUNBUFFERED=1
63
+ ENV PYTHONDONTWRITEBYTECODE=1
64
+
65
+ # Port configuration (7860 = HF Spaces standard)
66
  ENV PORT=7860
67
+
68
+ # User home
69
  ENV HOME=/home/user
70
+
71
+ # Hugging Face cache directories
72
  ENV HF_HOME=/tmp/huggingface
73
  ENV TRANSFORMERS_CACHE=/tmp/transformers
74
  ENV XDG_CACHE_HOME=/tmp
 
76
  ENV HF_HUB_DISABLE_TELEMETRY=1
77
  ENV HF_HUB_ENABLE_HF_TRANSFER=0
78
 
79
+ # OpenAI API Key (set via HF Spaces secrets or docker run -e)
80
+ ENV OPENAI_API_KEY=${OPENAI_API_KEY:-}
81
+
82
+ # USDA API Keys (optional - defaults to DEMO_KEY)
83
+ ENV USDA_API_KEY=${USDA_API_KEY:-DEMO_KEY}
84
+
85
  # Performance optimizations
86
  ENV TOKENIZERS_PARALLELISM=false
87
  ENV OMP_NUM_THREADS=2
88
  ENV MKL_NUM_THREADS=2
89
 
90
+ # Expose port
91
  EXPOSE 7860
92
 
93
+ # Health check (monitors API health every 30s)
94
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \
95
  CMD curl -f http://localhost:7860/health || exit 1
96
 
97
+ # Run the application
98
  CMD ["python", "app.py"]
app.py CHANGED
@@ -19,13 +19,25 @@ import aiohttp
19
  import re
20
  from typing import Dict, Any, List, Optional
21
  from io import BytesIO
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  import torch
24
  import torch.nn.functional as F
25
  from PIL import Image, ImageEnhance
26
  import numpy as np
27
 
28
- from fastapi import FastAPI, File, UploadFile, HTTPException, Request
29
  from fastapi.middleware.cors import CORSMiddleware
30
  from fastapi.responses import JSONResponse
31
  import uvicorn
@@ -33,11 +45,18 @@ import uvicorn
33
  from transformers import AutoImageProcessor, AutoModelForImageClassification
34
  from contextlib import asynccontextmanager
35
 
 
 
 
36
  # ==================== CONFIGURATION ====================
37
  MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
38
  MAX_IMAGE_SIZE = 512
39
  ALLOWED_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp"]
40
 
 
 
 
 
41
  # ==================== MULTI-MODEL FOOD RECOGNITION ====================
42
  FOOD_MODELS = {
43
  # Primary specialize food models
@@ -86,9 +105,9 @@ PRIMARY_MODEL = "food101"
86
  COMPREHENSIVE_FOOD_CATEGORIES = {
87
  # Food-101 categories
88
  "pizza", "hamburger", "sushi", "ice_cream", "french_fries", "chicken_wings",
89
- "chocolate_cake", "caesar_salad", "steak", "tacos", "pancakes", "lasagna",
90
- "apple_pie", "chicken_curry", "pad_thai", "ramen", "waffles", "donuts",
91
- "cheesecake", "fish_and_chips", "fried_rice", "greek_salad", "guacamole",
92
 
93
  # Balkanska/Srpska tradicionalna jela
94
  "cevapi", "cevapcici", "burek", "pljeskavica", "sarma", "klepe", "dolma",
@@ -365,10 +384,19 @@ def clean_food_name_for_search(raw_name: str) -> str:
365
  """Smart cleaning of Food-101 names for better API searches."""
366
  # Remove underscores and replace with spaces
367
  cleaned = raw_name.replace("_", " ")
368
-
 
 
 
 
 
 
 
 
 
369
  # Remove common Food-101 artifacts
370
  cleaned = re.sub(r'\b(and|with|the|a)\b', ' ', cleaned, flags=re.IGNORECASE)
371
-
372
  # Handle specific Food-101 patterns
373
  replacements = {
374
  "cup cakes": "cupcakes",
@@ -378,15 +406,16 @@ def clean_food_name_for_search(raw_name: str) -> str:
378
  "shrimp and grits": "shrimp grits",
379
  "macaroni and cheese": "mac and cheese"
380
  }
381
-
382
  for old, new in replacements.items():
383
  if old in cleaned.lower():
384
  cleaned = new
385
  break
386
-
387
- # Clean whitespace
388
  cleaned = re.sub(r'\s+', ' ', cleaned).strip()
389
-
 
390
  return cleaned
391
 
392
  async def search_openfoodfacts_nutrition(food_name: str) -> Optional[Dict[str, Any]]:
@@ -472,9 +501,9 @@ async def get_nutrition_from_apis(food_name: str) -> Dict[str, Any]:
472
  """Get nutrition data from multiple FREE databases with comprehensive fallback."""
473
  # Clean the Food-101 name for better searches
474
  cleaned_name = clean_food_name_for_search(food_name)
475
-
476
  logger.info(f"🔍 Searching nutrition for: '{food_name}' → '{cleaned_name}'")
477
-
478
  # Try APIs in order: Free/Unlimited first, then limited APIs
479
  nutrition_sources = [
480
  ("OpenFoodFacts", search_openfoodfacts_nutrition), # FREE, 2M+ products
@@ -483,23 +512,238 @@ async def get_nutrition_from_apis(food_name: str) -> Dict[str, Any]:
483
  ("Edamam", search_edamam_nutrition), # 1000/month limit
484
  ("Spoonacular", search_spoonacular_nutrition) # 150/day limit
485
  ]
486
-
 
487
  for source_name, search_func in nutrition_sources:
488
  try:
489
  nutrition_data = await search_func(cleaned_name)
490
  if nutrition_data and nutrition_data.get("calories", 0) > 0:
491
  nutrition_data["source"] = source_name
 
492
  return nutrition_data
493
  except Exception as e:
494
- logger.warning(f"⚠️ {source_name} search failed: {e}")
495
  continue
496
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
  # All APIs failed, return default values
498
- logger.warning(f"🚨 No nutrition data found for '{cleaned_name}', using defaults")
499
  default_nutrition = DEFAULT_NUTRITION.copy()
500
  default_nutrition["source"] = "Default (APIs unavailable)"
501
  return default_nutrition
502
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  # ==================== MULTI-MODEL FOOD RECOGNIZER ====================
504
  class MultiModelFoodRecognizer:
505
  """Production-ready multi-model ensemble for comprehensive food recognition."""
@@ -657,84 +901,186 @@ class MultiModelFoodRecognizer:
657
  """Main predict method - uses ensemble if available, fallback to primary."""
658
  return self.predict_ensemble(image, top_k)
659
 
660
- def predict_ensemble(self, image: Image.Image, top_k: int = 5) -> Dict[str, Any]:
661
- """Ensemble prediction using all available models."""
662
  if not self.is_loaded:
663
  raise RuntimeError("Models not loaded")
664
-
665
  all_predictions = []
666
  model_results = {}
667
-
668
- # Get predictions from all models
 
 
669
  for model_key in self.available_models:
670
- predictions = self._predict_with_model(image, model_key, top_k)
671
  if predictions:
672
  model_results[model_key] = predictions
673
  all_predictions.extend(predictions)
674
-
675
  if not all_predictions:
676
  raise RuntimeError("No models produced valid predictions")
677
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
  # Ensemble voting: weight by model priority and confidence
679
  food_scores = {}
 
 
680
  for pred in all_predictions:
 
 
 
 
 
 
 
 
 
681
  model_key = pred["model"]
682
  priority_weight = 1.0 / FOOD_MODELS[model_key]["priority"] # Higher priority = lower number = higher weight
683
  confidence_weight = pred["confidence"]
684
-
685
- # Combined score
686
- combined_score = priority_weight * confidence_weight
687
-
 
 
 
 
 
 
 
 
 
 
688
  food_name = pred["raw_label"]
689
  if food_name not in food_scores:
690
  food_scores[food_name] = {
691
  "total_score": 0,
692
  "count": 0,
693
  "best_prediction": pred,
694
- "models": []
 
695
  }
696
-
697
  food_scores[food_name]["total_score"] += combined_score
698
  food_scores[food_name]["count"] += 1
699
  food_scores[food_name]["models"].append(model_key)
700
-
701
  # Keep the prediction with highest confidence as representative
702
  if pred["confidence"] > food_scores[food_name]["best_prediction"]["confidence"]:
703
  food_scores[food_name]["best_prediction"] = pred
704
-
 
 
 
705
  # Sort by ensemble score
706
  sorted_foods = sorted(
707
- food_scores.items(),
708
- key=lambda x: x[1]["total_score"],
709
  reverse=True
710
  )
711
-
712
- # Format final results
713
  final_predictions = []
714
- for food_name, data in sorted_foods[:top_k]:
715
  pred = data["best_prediction"].copy()
716
  pred["ensemble_score"] = data["total_score"]
717
  pred["model_count"] = data["count"]
718
  pred["contributing_models"] = data["models"]
 
719
  final_predictions.append(pred)
720
-
721
- # Primary result
722
- primary = final_predictions[0] if final_predictions else {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
723
  "label": "Unknown Food",
724
  "raw_label": "unknown",
725
  "confidence": 0.0,
726
  "ensemble_score": 0.0,
727
  "model_count": 0,
728
- "contributing_models": []
 
729
  }
730
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
731
  return {
732
  "success": True,
733
  "label": primary["label"],
734
  "confidence": primary["confidence"],
735
  "primary_label": primary["raw_label"],
736
  "ensemble_score": primary.get("ensemble_score", 0),
737
- "alternatives": final_predictions[1:],
738
  "model_results": model_results,
739
  "system_info": {
740
  "available_models": self.available_models,
@@ -756,8 +1102,9 @@ async def lifespan(app: FastAPI):
756
  logger.info(f"🖥️ Device: {device.upper()}")
757
  logger.info(f"📊 Models: {len(recognizer.available_models)} active models")
758
  logger.info(f"🎯 Total Food Categories: {sum(FOOD_MODELS[m]['classes'] for m in recognizer.available_models)}")
 
759
  logger.info("=" * 60)
760
-
761
  yield
762
 
763
  # Shutdown
@@ -780,6 +1127,17 @@ logger.info("=" * 60)
780
  device = select_device()
781
  recognizer = MultiModelFoodRecognizer(device)
782
 
 
 
 
 
 
 
 
 
 
 
 
783
  # Create FastAPI app
784
  app = FastAPI(
785
  title="AI Food Recognition API",
@@ -867,43 +1225,149 @@ def health_check():
867
  }
868
 
869
  @app.post("/api/nutrition/analyze-food")
870
- async def analyze_food_nutrition(file: UploadFile = File(...)):
871
  """
872
- Analyze food image for Next.js frontend.
873
-
874
- Returns nutrition-focused response format.
 
 
 
 
875
  """
876
- logger.info(f"🍽️ Nutrition analysis request: {file.filename}")
877
-
878
  try:
879
- # Validate and process image
880
- image = await validate_and_read_image(file)
881
-
882
- # Step 1: AI Model Prediction
883
- results = recognizer.predict(image, top_k=5)
884
-
885
- # Step 2: API Nutrition Lookup
886
- nutrition_data = await get_nutrition_from_apis(results["primary_label"])
887
-
888
- # Log result
889
- confidence_pct = f"{results['confidence']:.1%}"
890
- source = nutrition_data.get("source", "Unknown")
891
- logger.info(f" Prediction: {results['label']} ({confidence_pct}) | Nutrition: {source}")
892
-
893
- # Return frontend-expected format
894
- return JSONResponse(content={
895
- "label": results["label"],
896
- "confidence": results["confidence"],
897
- "nutrition": {
898
- "calories": nutrition_data["calories"],
899
- "protein": nutrition_data["protein"],
900
- "carbs": nutrition_data["carbs"],
901
- "fat": nutrition_data["fat"]
902
- },
903
- "alternatives": results["alternatives"],
904
- "source": f"AI Recognition + {source} Database"
905
- })
906
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
907
  except HTTPException:
908
  raise
909
  except Exception as e:
@@ -923,8 +1387,8 @@ async def analyze_food_spaces(file: UploadFile = File(...)):
923
  # Validate and process image
924
  image = await validate_and_read_image(file)
925
 
926
- # Step 1: AI Model Prediction
927
- results = recognizer.predict(image, top_k=5)
928
 
929
  # Step 2: API Nutrition Lookup
930
  nutrition_data = await get_nutrition_from_apis(results["primary_label"])
 
19
  import re
20
  from typing import Dict, Any, List, Optional
21
  from io import BytesIO
22
+ from pathlib import Path
23
+
24
+ # Load .env file if exists
25
+ try:
26
+ from dotenv import load_dotenv
27
+ env_path = Path(__file__).parent / '.env'
28
+ load_dotenv(dotenv_path=env_path)
29
+ logging.info(f"✅ Loaded .env from {env_path}")
30
+ except ImportError:
31
+ logging.warning("⚠️ python-dotenv not installed, using system environment variables")
32
+ except Exception as e:
33
+ logging.warning(f"⚠️ Could not load .env: {e}")
34
 
35
  import torch
36
  import torch.nn.functional as F
37
  from PIL import Image, ImageEnhance
38
  import numpy as np
39
 
40
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Request, Form
41
  from fastapi.middleware.cors import CORSMiddleware
42
  from fastapi.responses import JSONResponse
43
  import uvicorn
 
45
  from transformers import AutoImageProcessor, AutoModelForImageClassification
46
  from contextlib import asynccontextmanager
47
 
48
+ # OpenAI for translations
49
+ from openai import AsyncOpenAI
50
+
51
  # ==================== CONFIGURATION ====================
52
  MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
53
  MAX_IMAGE_SIZE = 512
54
  ALLOWED_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp"]
55
 
56
+ # OpenAI Configuration (will be initialized after logger is set up)
57
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
58
+ openai_client = None # Will be initialized in lifespan startup
59
+
60
  # ==================== MULTI-MODEL FOOD RECOGNITION ====================
61
  FOOD_MODELS = {
62
  # Primary specialize food models
 
105
  COMPREHENSIVE_FOOD_CATEGORIES = {
106
  # Food-101 categories
107
  "pizza", "hamburger", "sushi", "ice_cream", "french_fries", "chicken_wings",
108
+ "chocolate_cake", "caesar_salad", "steak", "tacos", "pancakes", "pancake", "lasagna",
109
+ "apple_pie", "chicken_curry", "pad_thai", "ramen", "waffles", "waffle", "donuts",
110
+ "cheesecake", "fish_and_chips", "fried_rice", "greek_salad", "guacamole", "crepe", "crepes",
111
 
112
  # Balkanska/Srpska tradicionalna jela
113
  "cevapi", "cevapcici", "burek", "pljeskavica", "sarma", "klepe", "dolma",
 
384
  """Smart cleaning of Food-101 names for better API searches."""
385
  # Remove underscores and replace with spaces
386
  cleaned = raw_name.replace("_", " ")
387
+
388
+ # Handle comma-separated names - take the first part (usually English name)
389
+ # Example: "Pineapple, Ananas" → "Pineapple"
390
+ if "," in cleaned:
391
+ parts = cleaned.split(",")
392
+ # Try to detect which part is English (usually the first one)
393
+ # Keep the part that's more likely to be in nutrition databases
394
+ cleaned = parts[0].strip()
395
+ logger.info(f"🧹 Cleaned comma-separated name: '{raw_name}' → '{cleaned}'")
396
+
397
  # Remove common Food-101 artifacts
398
  cleaned = re.sub(r'\b(and|with|the|a)\b', ' ', cleaned, flags=re.IGNORECASE)
399
+
400
  # Handle specific Food-101 patterns
401
  replacements = {
402
  "cup cakes": "cupcakes",
 
406
  "shrimp and grits": "shrimp grits",
407
  "macaroni and cheese": "mac and cheese"
408
  }
409
+
410
  for old, new in replacements.items():
411
  if old in cleaned.lower():
412
  cleaned = new
413
  break
414
+
415
+ # Clean whitespace and extra punctuation
416
  cleaned = re.sub(r'\s+', ' ', cleaned).strip()
417
+ cleaned = re.sub(r'[^\w\s-]', '', cleaned) # Remove special chars except hyphens
418
+
419
  return cleaned
420
 
421
  async def search_openfoodfacts_nutrition(food_name: str) -> Optional[Dict[str, Any]]:
 
501
  """Get nutrition data from multiple FREE databases with comprehensive fallback."""
502
  # Clean the Food-101 name for better searches
503
  cleaned_name = clean_food_name_for_search(food_name)
504
+
505
  logger.info(f"🔍 Searching nutrition for: '{food_name}' → '{cleaned_name}'")
506
+
507
  # Try APIs in order: Free/Unlimited first, then limited APIs
508
  nutrition_sources = [
509
  ("OpenFoodFacts", search_openfoodfacts_nutrition), # FREE, 2M+ products
 
512
  ("Edamam", search_edamam_nutrition), # 1000/month limit
513
  ("Spoonacular", search_spoonacular_nutrition) # 150/day limit
514
  ]
515
+
516
+ # First attempt with cleaned name
517
  for source_name, search_func in nutrition_sources:
518
  try:
519
  nutrition_data = await search_func(cleaned_name)
520
  if nutrition_data and nutrition_data.get("calories", 0) > 0:
521
  nutrition_data["source"] = source_name
522
+ logger.info(f"✅ Found nutrition data from {source_name} for '{cleaned_name}'")
523
  return nutrition_data
524
  except Exception as e:
525
+ logger.warning(f"⚠️ {source_name} search failed for '{cleaned_name}': {e}")
526
  continue
527
+
528
+ # If cleaned name failed and it's different from original, try original name too
529
+ if cleaned_name.lower() != food_name.lower():
530
+ logger.info(f"🔄 Retrying with original name: '{food_name}'")
531
+ for source_name, search_func in nutrition_sources:
532
+ try:
533
+ nutrition_data = await search_func(food_name)
534
+ if nutrition_data and nutrition_data.get("calories", 0) > 0:
535
+ nutrition_data["source"] = source_name
536
+ logger.info(f"✅ Found nutrition data from {source_name} for original '{food_name}'")
537
+ return nutrition_data
538
+ except Exception as e:
539
+ logger.warning(f"⚠️ {source_name} search failed for original '{food_name}': {e}")
540
+ continue
541
+
542
+ # Try with just the first word as last resort (e.g., "pineapple juice" → "pineapple")
543
+ words = cleaned_name.split()
544
+ if len(words) > 1:
545
+ first_word = words[0]
546
+ logger.info(f"🔄 Last resort: trying first word only: '{first_word}'")
547
+ for source_name, search_func in nutrition_sources:
548
+ try:
549
+ nutrition_data = await search_func(first_word)
550
+ if nutrition_data and nutrition_data.get("calories", 0) > 0:
551
+ nutrition_data["source"] = f"{source_name} (matched: {first_word})"
552
+ logger.info(f"✅ Found nutrition data from {source_name} for '{first_word}'")
553
+ return nutrition_data
554
+ except Exception as e:
555
+ logger.warning(f"⚠️ {source_name} search failed for '{first_word}': {e}")
556
+ continue
557
+
558
  # All APIs failed, return default values
559
+ logger.warning(f"🚨 No nutrition data found for '{food_name}' after all attempts, using defaults")
560
  default_nutrition = DEFAULT_NUTRITION.copy()
561
  default_nutrition["source"] = "Default (APIs unavailable)"
562
  return default_nutrition
563
 
564
+ # ==================== TRANSLATION SYSTEM ====================
565
+
566
+ # In-memory translation cache to reduce API calls
567
+ translation_cache: Dict[str, Dict[str, str]] = {} # {locale: {english: translated}}
568
+
569
+ # Language code mapping (i18n locale → full language name)
570
+ LANGUAGE_MAP = {
571
+ "en": "English",
572
+ "bs": "Bosnian",
573
+ "de": "German",
574
+ "es": "Spanish",
575
+ "fr": "French",
576
+ "it": "Italian",
577
+ "pt": "Portuguese",
578
+ "ar": "Arabic",
579
+ "tr": "Turkish",
580
+ "nl": "Dutch",
581
+ "ru": "Russian",
582
+ "zh": "Chinese",
583
+ "ja": "Japanese",
584
+ "ko": "Korean",
585
+ "hi": "Hindi",
586
+ "sr": "Serbian",
587
+ "hr": "Croatian",
588
+ "sq": "Albanian",
589
+ "mk": "Macedonian",
590
+ }
591
+
592
+ async def translate_food_names_batch(food_names: List[str], target_locale: str) -> Dict[str, str]:
593
+ """
594
+ Translate multiple food names in a single API call (COST OPTIMIZATION).
595
+
596
+ Args:
597
+ food_names: List of food names in English
598
+ target_locale: Target language code
599
+
600
+ Returns:
601
+ Dictionary mapping original names to translated names
602
+ """
603
+ # Skip translation if target is English or no OpenAI client
604
+ if target_locale == "en" or not openai_client or not OPENAI_API_KEY:
605
+ return {name: name for name in food_names}
606
+
607
+ # Check cache first
608
+ if target_locale not in translation_cache:
609
+ translation_cache[target_locale] = {}
610
+
611
+ translations = {}
612
+ needs_translation = []
613
+
614
+ # Separate cached and uncached items
615
+ for name in food_names:
616
+ if name in translation_cache[target_locale]:
617
+ translations[name] = translation_cache[target_locale][name]
618
+ logger.info(f"💾 Cache hit: '{name}' → '{translations[name]}' ({target_locale})")
619
+ else:
620
+ needs_translation.append(name)
621
+
622
+ # If all cached, return immediately
623
+ if not needs_translation:
624
+ return translations
625
+
626
+ # Get target language name
627
+ target_language = LANGUAGE_MAP.get(target_locale, target_locale)
628
+
629
+ try:
630
+ logger.info(f"🌐 Batch translating {len(needs_translation)} items to {target_language}")
631
+
632
+ # Create batch translation prompt (1 API call for multiple items)
633
+ food_list = "\n".join(f"{i+1}. {name}" for i, name in enumerate(needs_translation))
634
+
635
+ response = await openai_client.chat.completions.create(
636
+ model="gpt-4o-mini",
637
+ messages=[
638
+ {
639
+ "role": "system",
640
+ "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."
641
+ },
642
+ {
643
+ "role": "user",
644
+ "content": f"Translate these food names to {target_language}:\n{food_list}"
645
+ }
646
+ ],
647
+ max_tokens=150,
648
+ temperature=0.3,
649
+ )
650
+
651
+ translated_lines = response.choices[0].message.content.strip().split('\n')
652
+
653
+ # Parse translations and update cache
654
+ for i, name in enumerate(needs_translation):
655
+ if i < len(translated_lines):
656
+ # Remove numbering if present (e.g., "1. Ananas" → "Ananas")
657
+ translated = translated_lines[i].strip()
658
+ translated = translated.split('. ', 1)[-1] if '. ' in translated else translated
659
+
660
+ translations[name] = translated
661
+ translation_cache[target_locale][name] = translated
662
+ logger.info(f"✅ '{name}' → '{translated}'")
663
+
664
+ return translations
665
+
666
+ except Exception as e:
667
+ logger.warning(f"⚠️ Batch translation failed: {e}")
668
+ # Return originals on failure
669
+ for name in needs_translation:
670
+ translations[name] = name
671
+ return translations
672
+
673
+ async def translate_food_name(food_name: str, target_locale: str) -> str:
674
+ """
675
+ Translate single food name (uses batch function internally for caching).
676
+
677
+ Args:
678
+ food_name: Food name in English
679
+ target_locale: Target language code
680
+
681
+ Returns:
682
+ Translated food name or original if translation fails/not needed
683
+ """
684
+ result = await translate_food_names_batch([food_name], target_locale)
685
+ return result.get(food_name, food_name)
686
+
687
+ async def translate_description(description: str, target_locale: str) -> str:
688
+ """
689
+ Translate food description to target language using OpenAI with caching.
690
+
691
+ Args:
692
+ description: Description in English
693
+ target_locale: Target language code
694
+
695
+ Returns:
696
+ Translated description or original if translation fails/not needed
697
+ """
698
+ # Skip translation if target is English or no OpenAI client
699
+ if target_locale == "en" or not openai_client or not OPENAI_API_KEY:
700
+ return description
701
+
702
+ # Simple cache key (hash of description + locale)
703
+ cache_key = f"desc_{hash(description)}_{target_locale}"
704
+
705
+ # Check if cached in locale cache
706
+ if target_locale not in translation_cache:
707
+ translation_cache[target_locale] = {}
708
+
709
+ if cache_key in translation_cache[target_locale]:
710
+ logger.info(f"💾 Description cache hit ({target_locale})")
711
+ return translation_cache[target_locale][cache_key]
712
+
713
+ # Get target language name
714
+ target_language = LANGUAGE_MAP.get(target_locale, target_locale)
715
+
716
+ try:
717
+ logger.info(f"🌐 Translating description to {target_language}")
718
+
719
+ response = await openai_client.chat.completions.create(
720
+ model="gpt-4o-mini",
721
+ messages=[
722
+ {
723
+ "role": "system",
724
+ "content": f"You are a food description translator. Translate to {target_language}. Keep it natural and concise. Return ONLY the translation."
725
+ },
726
+ {
727
+ "role": "user",
728
+ "content": description
729
+ }
730
+ ],
731
+ max_tokens=100,
732
+ temperature=0.3,
733
+ )
734
+
735
+ translated = response.choices[0].message.content.strip()
736
+
737
+ # Cache the result
738
+ translation_cache[target_locale][cache_key] = translated
739
+ logger.info(f"✅ Description translated to {target_language}")
740
+
741
+ return translated
742
+
743
+ except Exception as e:
744
+ logger.warning(f"⚠️ Description translation failed: {e}")
745
+ return description
746
+
747
  # ==================== MULTI-MODEL FOOD RECOGNIZER ====================
748
  class MultiModelFoodRecognizer:
749
  """Production-ready multi-model ensemble for comprehensive food recognition."""
 
901
  """Main predict method - uses ensemble if available, fallback to primary."""
902
  return self.predict_ensemble(image, top_k)
903
 
904
+ def predict_ensemble(self, image: Image.Image, top_k: int = 10) -> Dict[str, Any]:
905
+ """Ensemble prediction using all available models with smart filtering."""
906
  if not self.is_loaded:
907
  raise RuntimeError("Models not loaded")
908
+
909
  all_predictions = []
910
  model_results = {}
911
+
912
+ # Get MORE predictions from all models (top 15 instead of 5)
913
+ predictions_per_model = 15
914
+
915
  for model_key in self.available_models:
916
+ predictions = self._predict_with_model(image, model_key, predictions_per_model)
917
  if predictions:
918
  model_results[model_key] = predictions
919
  all_predictions.extend(predictions)
920
+
921
  if not all_predictions:
922
  raise RuntimeError("No models produced valid predictions")
923
+
924
+ # NON-FOOD items that should be COMPLETELY FILTERED OUT
925
+ non_food_items = {
926
+ # Kitchen utensils & cookware
927
+ 'plate', 'dish', 'bowl', 'cup', 'glass', 'mug', 'spoon', 'fork', 'knife',
928
+ 'spatula', 'pan', 'pot', 'tray', 'napkin', 'table', 'cloth', 'placemat',
929
+ 'chopsticks', 'straw', 'bottle', 'container', 'lid', 'wrapper', 'packaging',
930
+ 'cutting board', 'grater', 'whisk', 'ladle', 'tongs', 'peeler', 'sieve',
931
+ 'colander', 'mixer', 'blender', 'toaster', 'oven', 'microwave', 'fridge',
932
+ 'freezer', 'dishwasher', 'sink', 'counter', 'shelf', 'cabinet', 'drawer',
933
+ 'waffle iron', 'frying pan', 'frypan', 'skillet', 'saucepan', 'stockpot',
934
+ 'baking sheet', 'baking pan', 'baking dish', 'loaf pan', 'muffin tin',
935
+ 'rolling pin', 'measuring cup', 'measuring spoon', 'kitchen scale',
936
+ 'bakery', 'bakeshop', 'bakehouse', 'restaurant', 'kitchen', 'dining room',
937
+
938
+ # Animals (NOT food!)
939
+ 'dog', 'cat', 'bird', 'fish', 'horse', 'cow', 'pig', 'chicken',
940
+ 'terrier', 'retriever', 'bulldog', 'poodle', 'beagle', 'dachshund',
941
+ 'lobster', 'crab', 'shrimp', 'hunting dog', 'hyena', 'wolf', 'fox',
942
+
943
+ # Objects/Electronics
944
+ 'joystick', 'controller', 'remote', 'phone', 'computer', 'mouse', 'keyboard',
945
+ 'water jug', 'jug', 'pitcher', 'vase', 'flowerpot'
946
+ }
947
+
948
+ # Generic FOOD terms that should be deprioritized (but not removed)
949
+ generic_terms = {
950
+ 'fruit', 'vegetable', 'food', 'meal', 'snack', 'dessert',
951
+ 'salad', 'soup', 'drink', 'beverage', 'meat', 'fish', 'seafood',
952
+ 'bread', 'pastry', 'cake', 'cookie', 'candy', 'chocolate'
953
+ }
954
+
955
  # Ensemble voting: weight by model priority and confidence
956
  food_scores = {}
957
+ filtered_count = 0
958
+
959
  for pred in all_predictions:
960
+ food_label_lower = pred["raw_label"].lower().replace("_", " ")
961
+
962
+ # FILTER OUT non-food items completely
963
+ is_non_food = any(non_food in food_label_lower for non_food in non_food_items)
964
+ if is_non_food:
965
+ filtered_count += 1
966
+ logger.info(f"🚫 Filtered non-food item: '{pred['raw_label']}'")
967
+ continue # Skip this prediction entirely
968
+
969
  model_key = pred["model"]
970
  priority_weight = 1.0 / FOOD_MODELS[model_key]["priority"] # Higher priority = lower number = higher weight
971
  confidence_weight = pred["confidence"]
972
+
973
+ # PENALTY for generic terms - reduce their score significantly
974
+ is_generic = any(generic in food_label_lower for generic in generic_terms)
975
+
976
+ # If it's a single-word generic term, penalize it even more
977
+ is_single_generic = food_label_lower in generic_terms
978
+
979
+ if is_single_generic:
980
+ combined_score = priority_weight * confidence_weight * 0.1 # 90% penalty
981
+ elif is_generic:
982
+ combined_score = priority_weight * confidence_weight * 0.5 # 50% penalty
983
+ else:
984
+ combined_score = priority_weight * confidence_weight # Full score for specific items
985
+
986
  food_name = pred["raw_label"]
987
  if food_name not in food_scores:
988
  food_scores[food_name] = {
989
  "total_score": 0,
990
  "count": 0,
991
  "best_prediction": pred,
992
+ "models": [],
993
+ "is_generic": is_generic
994
  }
995
+
996
  food_scores[food_name]["total_score"] += combined_score
997
  food_scores[food_name]["count"] += 1
998
  food_scores[food_name]["models"].append(model_key)
999
+
1000
  # Keep the prediction with highest confidence as representative
1001
  if pred["confidence"] > food_scores[food_name]["best_prediction"]["confidence"]:
1002
  food_scores[food_name]["best_prediction"] = pred
1003
+
1004
+ if filtered_count > 0:
1005
+ logger.info(f"✅ Filtered out {filtered_count} non-food items")
1006
+
1007
  # Sort by ensemble score
1008
  sorted_foods = sorted(
1009
+ food_scores.items(),
1010
+ key=lambda x: x[1]["total_score"],
1011
  reverse=True
1012
  )
1013
+
1014
+ # Format final results - return MORE alternatives (up to top_k)
1015
  final_predictions = []
1016
+ for food_name, data in sorted_foods[:top_k * 2]: # Get double to have enough after filtering
1017
  pred = data["best_prediction"].copy()
1018
  pred["ensemble_score"] = data["total_score"]
1019
  pred["model_count"] = data["count"]
1020
  pred["contributing_models"] = data["models"]
1021
+ pred["is_generic"] = data["is_generic"]
1022
  final_predictions.append(pred)
1023
+
1024
+ # Remove duplicates AND non-food items (double check)
1025
+ filtered_predictions = []
1026
+ seen_labels = set()
1027
+
1028
+ for pred in final_predictions:
1029
+ label_lower = pred["raw_label"].lower().replace("_", " ").strip()
1030
+
1031
+ # DOUBLE CHECK: Filter non-food items again
1032
+ is_non_food = any(non_food in label_lower for non_food in non_food_items)
1033
+ if is_non_food:
1034
+ continue # Skip non-food items
1035
+
1036
+ # Skip if we've already seen very similar label
1037
+ if label_lower not in seen_labels:
1038
+ filtered_predictions.append(pred)
1039
+ seen_labels.add(label_lower)
1040
+
1041
+ if len(filtered_predictions) >= top_k:
1042
+ break
1043
+
1044
+ # Primary result - prefer specific over generic AND high confidence
1045
+ primary = filtered_predictions[0] if filtered_predictions else {
1046
  "label": "Unknown Food",
1047
  "raw_label": "unknown",
1048
  "confidence": 0.0,
1049
  "ensemble_score": 0.0,
1050
  "model_count": 0,
1051
+ "contributing_models": [],
1052
+ "is_generic": False
1053
  }
1054
+
1055
+ # QUALITY CHECK: If primary confidence is < 10%, try to find better alternative
1056
+ MIN_CONFIDENCE = 0.10 # 10%
1057
+ if primary.get("confidence", 0) < MIN_CONFIDENCE and len(filtered_predictions) > 1:
1058
+ logger.warning(f"⚠️ Low confidence ({primary['confidence']:.1%}) for '{primary['label']}', checking alternatives...")
1059
+ # Find first alternative with higher confidence
1060
+ for i, pred in enumerate(filtered_predictions[1:], 1):
1061
+ if pred.get("confidence", 0) >= MIN_CONFIDENCE / 2: # At least 5%
1062
+ filtered_predictions[0], filtered_predictions[i] = filtered_predictions[i], filtered_predictions[0]
1063
+ primary = filtered_predictions[0]
1064
+ logger.info(f"🔄 Swapped low-confidence primary with better alternative: {primary['label']} ({primary['confidence']:.1%})")
1065
+ break
1066
+
1067
+ # If primary is generic but we have specific alternatives, swap them
1068
+ if primary.get("is_generic") and len(filtered_predictions) > 1:
1069
+ for i, pred in enumerate(filtered_predictions[1:], 1):
1070
+ if not pred.get("is_generic"):
1071
+ # Swap primary with this specific prediction
1072
+ filtered_predictions[0], filtered_predictions[i] = filtered_predictions[i], filtered_predictions[0]
1073
+ primary = filtered_predictions[0]
1074
+ logger.info(f"🔄 Swapped generic primary with specific: {primary['label']}")
1075
+ break
1076
+
1077
  return {
1078
  "success": True,
1079
  "label": primary["label"],
1080
  "confidence": primary["confidence"],
1081
  "primary_label": primary["raw_label"],
1082
  "ensemble_score": primary.get("ensemble_score", 0),
1083
+ "alternatives": filtered_predictions[1:], # Now returns up to 9 alternatives
1084
  "model_results": model_results,
1085
  "system_info": {
1086
  "available_models": self.available_models,
 
1102
  logger.info(f"🖥️ Device: {device.upper()}")
1103
  logger.info(f"📊 Models: {len(recognizer.available_models)} active models")
1104
  logger.info(f"🎯 Total Food Categories: {sum(FOOD_MODELS[m]['classes'] for m in recognizer.available_models)}")
1105
+ logger.info(f"🌐 Translations: {'✅ Enabled' if openai_client else '❌ Disabled'}")
1106
  logger.info("=" * 60)
1107
+
1108
  yield
1109
 
1110
  # Shutdown
 
1127
  device = select_device()
1128
  recognizer = MultiModelFoodRecognizer(device)
1129
 
1130
+ # Initialize OpenAI client BEFORE FastAPI app
1131
+ if OPENAI_API_KEY:
1132
+ try:
1133
+ openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY)
1134
+ logger.info(f"✅ OpenAI client initialized (key: {OPENAI_API_KEY[:20]}...)")
1135
+ except Exception as e:
1136
+ logger.warning(f"⚠️ OpenAI client initialization failed: {e}")
1137
+ openai_client = None
1138
+ else:
1139
+ logger.warning("⚠️ OpenAI API key not found - translations disabled")
1140
+
1141
  # Create FastAPI app
1142
  app = FastAPI(
1143
  title="AI Food Recognition API",
 
1225
  }
1226
 
1227
  @app.post("/api/nutrition/analyze-food")
1228
+ async def analyze_food_nutrition(request: Request, file: UploadFile = File(None)):
1229
  """
1230
+ Analyze food image or manual entry for Next.js frontend.
1231
+
1232
+ Supports two modes:
1233
+ 1. Image upload: AI recognition + nutrition lookup
1234
+ 2. Manual entry: Direct nutrition lookup by food name
1235
+
1236
+ Returns nutrition-focused response format with translations.
1237
  """
 
 
1238
  try:
1239
+ # Parse form data
1240
+ form_data = await request.form()
1241
+ manual_input = form_data.get("manualInput", "false").lower() == "true"
1242
+ locale = form_data.get("locale", "en") # Get user's language preference
1243
+
1244
+ logger.info(f"📥 Request received - Mode: {'Manual' if manual_input else 'Image'}, Locale: {locale}")
1245
+
1246
+ # MODE 1: Manual food entry (from alternatives or manual input)
1247
+ if manual_input:
1248
+ food_name = form_data.get("manualFoodName")
1249
+ serving_size = form_data.get("manualServingSize", "100")
1250
+ serving_unit = form_data.get("manualServingUnit", "g")
1251
+ description = form_data.get("manualDescription", "")
1252
+
1253
+ if not food_name:
1254
+ raise HTTPException(status_code=400, detail="manualFoodName is required for manual entry")
1255
+
1256
+ logger.info(f"🍽️ Manual nutrition lookup: {food_name} ({serving_size}{serving_unit})")
1257
+
1258
+ # Direct nutrition API lookup
1259
+ nutrition_data = await get_nutrition_from_apis(food_name)
1260
+
1261
+ if not nutrition_data or nutrition_data.get("calories", 0) == 0:
1262
+ raise HTTPException(
1263
+ status_code=404,
1264
+ detail=f"Failed to retrieve nutrition information for manual entry"
1265
+ )
1266
+
1267
+ source = nutrition_data.get("source", "Unknown")
1268
+ logger.info(f"✅ Manual lookup: {food_name} | Nutrition: {source}")
1269
+
1270
+ # Translate food name and description
1271
+ translated_name = await translate_food_name(food_name, locale)
1272
+ base_description = description or f"Manual entry: {food_name}"
1273
+ translated_description = await translate_description(base_description, locale)
1274
+
1275
+ # Return manual entry format
1276
+ return JSONResponse(content={
1277
+ "data": {
1278
+ "label": translated_name,
1279
+ "confidence": 1.0, # Manual entry has 100% confidence
1280
+ "nutrition": {
1281
+ "calories": nutrition_data["calories"],
1282
+ "protein": nutrition_data["protein"],
1283
+ "carbs": nutrition_data["carbs"],
1284
+ "fat": nutrition_data["fat"]
1285
+ },
1286
+ "servingSize": serving_size,
1287
+ "servingUnit": serving_unit,
1288
+ "description": translated_description,
1289
+ "alternatives": [], # No alternatives for manual entry
1290
+ "source": f"{source} Database",
1291
+ "isManualEntry": True
1292
+ }
1293
+ })
1294
+
1295
+ # MODE 2: Image upload (AI recognition)
1296
+ else:
1297
+ if not file:
1298
+ raise HTTPException(status_code=400, detail="File is required for image analysis")
1299
+
1300
+ logger.info(f"🍽️ Image analysis request: {file.filename}")
1301
+
1302
+ # Validate and process image
1303
+ image = await validate_and_read_image(file)
1304
+
1305
+ # Step 1: AI Model Prediction (request top 10 for more alternatives)
1306
+ results = recognizer.predict(image, top_k=10)
1307
+
1308
+ # Step 2: API Nutrition Lookup
1309
+ nutrition_data = await get_nutrition_from_apis(results["primary_label"])
1310
+
1311
+ # Log result
1312
+ confidence_pct = f"{results['confidence']:.1%}"
1313
+ source = nutrition_data.get("source", "Unknown")
1314
+ logger.info(f"✅ Prediction: {results['label']} ({confidence_pct}) | Nutrition: {source}")
1315
+
1316
+ # BATCH TRANSLATION OPTIMIZATION: Translate all food names at once
1317
+ if locale != "en" and openai_client:
1318
+ # Collect all names to translate (primary + alternatives)
1319
+ names_to_translate = [results["label"]]
1320
+ if results.get("alternatives"):
1321
+ names_to_translate.extend([
1322
+ alt.get("label", alt.get("raw_label", ""))
1323
+ for alt in results["alternatives"]
1324
+ ])
1325
+
1326
+ # Single API call for all translations
1327
+ translations = await translate_food_names_batch(names_to_translate, locale)
1328
+
1329
+ # Apply translations
1330
+ translated_name = translations.get(results["label"], results["label"])
1331
+
1332
+ # Translate description
1333
+ base_description = f"{results['label']} identified with {int(results['confidence'] * 100)}% confidence"
1334
+ translated_description = await translate_description(base_description, locale)
1335
+
1336
+ # Map alternatives with translations
1337
+ translated_alternatives = []
1338
+ if results.get("alternatives"):
1339
+ for alt in results["alternatives"]:
1340
+ alt_name = alt.get("label", alt.get("raw_label", ""))
1341
+ translated_alternatives.append({
1342
+ **alt,
1343
+ "label": translations.get(alt_name, alt_name),
1344
+ "original_label": alt_name
1345
+ })
1346
+ else:
1347
+ # No translation needed
1348
+ translated_name = results["label"]
1349
+ translated_description = f"{results['label']} identified with {int(results['confidence'] * 100)}% confidence"
1350
+ translated_alternatives = results["alternatives"]
1351
+
1352
+ # Return frontend-expected format
1353
+ return JSONResponse(content={
1354
+ "data": {
1355
+ "label": translated_name,
1356
+ "confidence": results["confidence"],
1357
+ "description": translated_description, # Translated description
1358
+ "nutrition": {
1359
+ "calories": nutrition_data["calories"],
1360
+ "protein": nutrition_data["protein"],
1361
+ "carbs": nutrition_data["carbs"],
1362
+ "fat": nutrition_data["fat"]
1363
+ },
1364
+ "alternatives": translated_alternatives,
1365
+ "source": f"AI Recognition + {source} Database",
1366
+ "isManualEntry": False,
1367
+ "locale": locale # Return locale for debugging
1368
+ }
1369
+ })
1370
+
1371
  except HTTPException:
1372
  raise
1373
  except Exception as e:
 
1387
  # Validate and process image
1388
  image = await validate_and_read_image(file)
1389
 
1390
+ # Step 1: AI Model Prediction (request top 10 for more alternatives)
1391
+ results = recognizer.predict(image, top_k=10)
1392
 
1393
  # Step 2: API Nutrition Lookup
1394
  nutrition_data = await get_nutrition_from_apis(results["primary_label"])
requirements.txt CHANGED
@@ -21,6 +21,9 @@ python-multipart>=0.0.6
21
  # Async HTTP client for USDA API
22
  aiohttp>=3.8.0
23
 
 
 
 
24
  # Utilities
25
  python-dotenv>=1.0.0
26
 
 
21
  # Async HTTP client for USDA API
22
  aiohttp>=3.8.0
23
 
24
+ # OpenAI for translations
25
+ openai>=1.0.0
26
+
27
  # Utilities
28
  python-dotenv>=1.0.0
29