har1zarD commited on
Commit
543a89b
·
1 Parent(s): d05a990
Files changed (4) hide show
  1. .env.example +0 -5
  2. DEPLOYMENT.md +0 -451
  3. app.py +401 -1061
  4. requirements.txt +7 -22
.env.example CHANGED
@@ -1,8 +1,3 @@
1
  # Server Configuration
2
  PORT=8000
3
  HOST=0.0.0.0
4
-
5
- # API Keys (optional, already in code)
6
- USDA_API_KEY=USDA_API_KEY
7
- NUTRITIONIX_APP_ID=NUTRITIONIX_APP_ID
8
- NUTRITIONIX_API_KEY=NUTRITIONIX_API_KEY
 
1
  # Server Configuration
2
  PORT=8000
3
  HOST=0.0.0.0
 
 
 
 
 
DEPLOYMENT.md DELETED
@@ -1,451 +0,0 @@
1
- # 🚀 Food Recognition Backend - Deployment Guide
2
-
3
- Complete guide for deploying the food recognition API for **FREE** on various platforms.
4
-
5
- ---
6
-
7
- ## 📋 Table of Contents
8
- 1. [Quick Start](#quick-start)
9
- 2. [Free Hosting Options](#free-hosting-options)
10
- 3. [Deployment Instructions](#deployment-instructions)
11
- 4. [Environment Variables](#environment-variables)
12
- 5. [Testing Your Deployment](#testing-your-deployment)
13
- 6. [Integration with Next.js](#integration-with-nextjs)
14
-
15
- ---
16
-
17
- ## 🎯 Quick Start
18
-
19
- Before deploying, ensure you have:
20
- - ✅ Python 3.11+
21
- - ✅ Git repository (GitHub/GitLab)
22
- - ✅ Docker installed (for local testing)
23
-
24
- ---
25
-
26
- ## 💰 Free Hosting Options
27
-
28
- ### 🥇 **Option 1: Hugging Face Spaces** (RECOMMENDED)
29
- - **Cost**: 100% FREE
30
- - **Specs**: 2 vCPU, 16GB RAM
31
- - **Limits**: No request limits
32
- - **Cold Starts**: ~30-60s first request
33
- - **Best For**: ML models, unlimited testing
34
-
35
- ### 🥈 **Option 2: Render**
36
- - **Cost**: FREE tier available
37
- - **Specs**: 512MB RAM, shared CPU
38
- - **Limits**: Spins down after 15min inactivity
39
- - **Cold Starts**: ~30-60s after sleep
40
- - **Best For**: Simple APIs with moderate usage
41
-
42
- ### 🥉 **Option 3: Railway** (Limited Free)
43
- - **Cost**: $5 free credit/month
44
- - **Specs**: ~500 hours/month
45
- - **Limits**: Credit-based
46
- - **Best For**: Development/staging
47
-
48
- ### ⚠️ **NOT Recommended (Too Restrictive)**
49
- - ❌ Vercel/Netlify - 50MB limit (model is 500MB+)
50
- - ❌ Heroku - No free tier anymore
51
- - ❌ AWS Lambda - 250MB deployment limit
52
-
53
- ---
54
-
55
- ## 📦 Deployment Instructions
56
-
57
- ### 🟢 Deploy to Hugging Face Spaces (BEST FREE OPTION)
58
-
59
- **Step 1: Create Account**
60
- ```bash
61
- # Visit https://huggingface.co/join
62
- # Create free account
63
- ```
64
-
65
- **Step 2: Create New Space**
66
- 1. Go to https://huggingface.co/new-space
67
- 2. **Name**: `food-recognition-api` (or your choice)
68
- 3. **License**: MIT
69
- 4. **SDK**: Docker
70
- 5. **Hardware**: CPU (basic) - FREE ✅
71
- 6. Click **Create Space**
72
-
73
- **Step 3: Prepare Files**
74
-
75
- Create `Dockerfile` (already included):
76
- ```dockerfile
77
- FROM python:3.11-slim
78
- WORKDIR /app
79
- RUN apt-get update && apt-get install -y gcc g++ && rm -rf /var/lib/apt/lists/*
80
- COPY requirements.txt .
81
- RUN pip install --no-cache-dir -r requirements.txt
82
- COPY app.py .
83
- EXPOSE 8000
84
- ENV PYTHONUNBUFFERED=1
85
- ENV PORT=8000
86
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
87
- ```
88
-
89
- **Step 4: Push to Space**
90
-
91
- Option A: Web UI
92
- ```bash
93
- # Zip your files: app.py, requirements.txt, Dockerfile
94
- # Upload via Hugging Face Space UI
95
- ```
96
-
97
- Option B: Git (recommended)
98
- ```bash
99
- # Clone your space
100
- git clone https://huggingface.co/spaces/YOUR_USERNAME/food-recognition-api
101
- cd food-recognition-api
102
-
103
- # Copy files
104
- cp /path/to/app.py .
105
- cp /path/to/requirements.txt .
106
- cp /path/to/Dockerfile .
107
-
108
- # Commit and push
109
- git add .
110
- git commit -m "Initial deployment"
111
- git push
112
- ```
113
-
114
- **Step 5: Configure Environment**
115
- 1. Go to Space Settings → Variables
116
- 2. Add:
117
- ```
118
- PORT=7860
119
- HOST=0.0.0.0
120
- ```
121
-
122
- **Step 6: Get Your API URL**
123
- ```
124
- https://YOUR_USERNAME-food-recognition-api.hf.space
125
- ```
126
-
127
- **Build Time**: 5-10 minutes (PyTorch is large)
128
-
129
- ---
130
-
131
- ### 🟡 Deploy to Render
132
-
133
- **Step 1: Create Account**
134
- - Visit https://render.com
135
- - Sign up with GitHub
136
-
137
- **Step 2: Create New Web Service**
138
- 1. Click **New +** → **Web Service**
139
- 2. Connect your GitHub repository
140
- 3. Settings:
141
- - **Name**: `food-recognition-api`
142
- - **Environment**: Docker
143
- - **Region**: Choose closest
144
- - **Branch**: `main`
145
- - **Dockerfile Path**: `./Dockerfile`
146
-
147
- **Step 3: Configure**
148
- - **Plan**: Free
149
- - **Environment Variables**:
150
- ```
151
- PORT=10000
152
- USDA_API_KEY=your_key_here
153
- NUTRITIONIX_APP_ID=your_id_here
154
- NUTRITIONIX_API_KEY=your_key_here
155
- ```
156
-
157
- **Step 4: Deploy**
158
- - Click **Create Web Service**
159
- - Wait 10-15 minutes for build
160
-
161
- **Your URL**: `https://food-recognition-api.onrender.com`
162
-
163
- ⚠️ **Note**: Free tier sleeps after 15min inactivity. First request after sleep takes ~30-60s.
164
-
165
- ---
166
-
167
- ### 🟠 Deploy to Railway (Limited Free)
168
-
169
- **Step 1: Create Account**
170
- - Visit https://railway.app
171
- - Sign up with GitHub
172
-
173
- **Step 2: Create New Project**
174
- 1. Click **New Project**
175
- 2. Select **Deploy from GitHub repo**
176
- 3. Choose your repository
177
-
178
- **Step 3: Configure Service**
179
- 1. Click your service
180
- 2. Settings:
181
- - **Root Directory**: `/` (or `/food_recognition_backend` if nested)
182
- - **Custom Start Command**: Leave empty (uses Dockerfile)
183
-
184
- **Step 4: Environment Variables**
185
- ```
186
- PORT=8000
187
- USDA_API_KEY=your_key_here
188
- NUTRITIONIX_APP_ID=your_id_here
189
- NUTRITIONIX_API_KEY=your_key_here
190
- ```
191
-
192
- **Step 5: Generate Domain**
193
- - Settings → Networking → Generate Domain
194
-
195
- **Your URL**: `https://food-recognition-api-production.up.railway.app`
196
-
197
- 💰 **Cost**: $5 free credit monthly (~500 hours)
198
-
199
- ---
200
-
201
- ## 🔐 Environment Variables
202
-
203
- ### Required Variables
204
-
205
- ```bash
206
- # Server Configuration
207
- PORT=8000 # Port for the API (auto-assigned by some hosts)
208
- HOST=0.0.0.0 # Host binding
209
-
210
- # Optional: Nutrition API Keys (already have defaults)
211
- USDA_API_KEY=your_key_here
212
- NUTRITIONIX_APP_ID=your_id_here
213
- NUTRITIONIX_API_KEY=your_key_here
214
- ```
215
-
216
- ### Where to Set Variables
217
-
218
- **Hugging Face Spaces:**
219
- - Settings → Repository secrets
220
-
221
- **Render:**
222
- - Environment → Environment Variables
223
-
224
- **Railway:**
225
- - Variables tab
226
-
227
- ---
228
-
229
- ## 🧪 Testing Your Deployment
230
-
231
- ### 1. Health Check
232
- ```bash
233
- curl https://YOUR_API_URL/health
234
- ```
235
-
236
- Expected response:
237
- ```json
238
- {
239
- "status": "healthy",
240
- "model_loaded": true,
241
- "device": "cpu",
242
- "food_pipeline_loaded": true,
243
- "model_type": "Professional Food Recognition Models"
244
- }
245
- ```
246
-
247
- ### 2. Test Food Recognition
248
- ```bash
249
- # Upload image
250
- curl -X POST https://YOUR_API_URL/analyze?top_alternatives=3 \
251
- -F "file=@path/to/food_image.jpg"
252
- ```
253
-
254
- Expected response:
255
- ```json
256
- {
257
- "label": "pizza",
258
- "confidence": 0.95,
259
- "nutrition": {
260
- "calories": 266,
261
- "protein": 11.0,
262
- "fat": 10.0,
263
- "carbs": 33.0,
264
- "fiber": 2.3,
265
- "sugar": 3.7,
266
- "sodium": 598
267
- },
268
- "alternatives": ["flatbread", "focaccia"],
269
- "source": "Open Food Facts"
270
- }
271
- ```
272
-
273
- ### 3. Test from URL
274
- ```bash
275
- curl -X POST "https://YOUR_API_URL/analyze-url?image_url=https://example.com/food.jpg&top_alternatives=3"
276
- ```
277
-
278
- ### 4. Search Nutrition Only
279
- ```bash
280
- curl https://YOUR_API_URL/search-nutrition/pizza
281
- ```
282
-
283
- ---
284
-
285
- ## 🔗 Integration with Next.js
286
-
287
- ### Step 1: Update Environment Variables
288
-
289
- In your Next.js project, add to `.env`:
290
-
291
- ```bash
292
- # Production Food Recognition API
293
- FOOD_RECOGNITION_API_URL=https://YOUR_API_URL
294
- ```
295
-
296
- ### Step 2: Update API Routes
297
-
298
- Your Next.js API routes are already configured to use this variable:
299
-
300
- ```javascript
301
- // src/app/api/nutrition/analyze-food/route.js
302
- const FOOD_API_BASE_URL = process.env.FOOD_RECOGNITION_API_URL || "http://localhost:8000";
303
- ```
304
-
305
- ### Step 3: Deploy Next.js
306
-
307
- **On Vercel/Coolify:**
308
- 1. Add environment variable:
309
- ```
310
- FOOD_RECOGNITION_API_URL=https://YOUR_USERNAME-food-recognition-api.hf.space
311
- ```
312
- 2. Deploy/Restart
313
-
314
- ### Step 4: Test Integration
315
-
316
- From your Next.js app:
317
- ```javascript
318
- const formData = new FormData();
319
- formData.append('file', imageFile);
320
-
321
- const response = await fetch('/api/nutrition/analyze-food', {
322
- method: 'POST',
323
- body: formData,
324
- });
325
-
326
- const result = await response.json();
327
- console.log(result.data.foodName); // "pizza"
328
- console.log(result.data.calories); // 266
329
- ```
330
-
331
- ---
332
-
333
- ## ⚡ Performance Tips
334
-
335
- ### 1. Reduce Cold Starts
336
- **Hugging Face Spaces:**
337
- - Upgrade to paid tier for always-on ($9/month) - optional
338
-
339
- **Render:**
340
- - Paid plan keeps service always on ($7/month) - optional
341
- - Free: Keep pinging `/health` every 10 minutes
342
-
343
- ### 2. Implement Caching
344
- In Next.js, cache results:
345
- ```javascript
346
- // Example with Redis/Upstash
347
- const cacheKey = `food_${imageHash}`;
348
- const cached = await redis.get(cacheKey);
349
- if (cached) return cached;
350
-
351
- // Call API only if not cached
352
- const result = await callFoodAPI();
353
- await redis.set(cacheKey, result, { ex: 86400 }); // 24h cache
354
- ```
355
-
356
- ### 3. Optimize Image Size
357
- Before sending to API:
358
- ```javascript
359
- // Resize images to max 800x800px
360
- const resized = await sharp(imageBuffer)
361
- .resize(800, 800, { fit: 'inside' })
362
- .jpeg({ quality: 80 })
363
- .toBuffer();
364
- ```
365
-
366
- ---
367
-
368
- ## 🐛 Troubleshooting
369
-
370
- ### Build Fails - Out of Memory
371
- **Solution**: Reduce PyTorch size in `requirements.txt`:
372
- ```txt
373
- torch>=2.0.0,<2.2.0 # Pin specific version
374
- ```
375
-
376
- ### API Timeout
377
- **Solution**: Increase timeout in Next.js:
378
- ```javascript
379
- const response = await fetch(API_URL, {
380
- method: 'POST',
381
- body: formData,
382
- signal: AbortSignal.timeout(30000), // 30s timeout
383
- });
384
- ```
385
-
386
- ### Model Not Loading
387
- **Solution**: Check logs for memory issues. Upgrade to paid tier or reduce model size.
388
-
389
- ### 422 Error - No Nutrition Data
390
- **Solution**: This is expected for some foods. Implement fallback:
391
- ```javascript
392
- if (response.status === 422) {
393
- // Show manual input form
394
- showManualInputForm();
395
- }
396
- ```
397
-
398
- ---
399
-
400
- ## 📊 Cost Comparison
401
-
402
- | Platform | Free Tier | Monthly Cost | RAM | Cold Start | Best For |
403
- |----------|-----------|--------------|-----|------------|----------|
404
- | **Hugging Face** | ✅ Unlimited | $0 | 16GB | ~30-60s | **Development & Production** |
405
- | **Render** | ✅ Yes | $0 | 512MB | ~30-60s | **Light Usage** |
406
- | **Railway** | ⚠️ Limited | $0 ($5 credit) | 2GB | None | **Testing** |
407
- | **Coolify** | ✅ Self-hosted | $0 (your server) | Custom | None | **Full Control** |
408
-
409
- ---
410
-
411
- ## 🎯 Recommendation
412
-
413
- **For Production (Free):**
414
- 1. 🥇 **Hugging Face Spaces** - Best free option, no limits
415
- 2. 🥈 **Render** - Good if traffic is low (sleeps after 15min)
416
-
417
- **For Production (Paid):**
418
- 1. 🥇 **Coolify** (Self-hosted) - Full control, $5-20/month
419
- 2. 🥈 **Railway Pro** - Easy, $20/month
420
- 3. 🥉 **Render Paid** - Simple, $7/month
421
-
422
- ---
423
-
424
- ## 📝 Next Steps
425
-
426
- 1. ✅ Choose hosting platform (Hugging Face recommended)
427
- 2. ✅ Deploy using instructions above
428
- 3. ✅ Test with `/health` endpoint
429
- 4. ✅ Update `FOOD_RECOGNITION_API_URL` in Next.js
430
- 5. ✅ Deploy Next.js with new env variable
431
- 6. ✅ Test end-to-end integration
432
-
433
- ---
434
-
435
- ## 🆘 Support
436
-
437
- If you encounter issues:
438
- 1. Check logs on your hosting platform
439
- 2. Test locally with Docker first
440
- 3. Verify environment variables are set
441
- 4. Check API URL is accessible
442
-
443
- ---
444
-
445
- ## 📄 License
446
-
447
- MIT License - Free to use for personal and commercial projects.
448
-
449
- ---
450
-
451
- **Ready to deploy? Start with Hugging Face Spaces for the best free experience!** 🚀
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,770 +1,245 @@
1
  #!/usr/bin/env python3
2
  """
3
- 🏆 ULTRA-OPTIMIZED Food Scanner API v10.0 - 99% Accuracy Edition
4
- ===============================================================
5
 
6
- Specijalizovani food recognition sistem sa ensemble pristupom za maksimalnu preciznost.
7
 
8
- Ključne optimizacije:
9
- - 🎯 Specijalizovani food-only modeli umjesto generičkih
10
- - 🔄 Ensemble voting sa 3+ modela za maksimalnu preciznost
11
- - 🚫 Non-food detection da se izbegnu glupe greške
12
- - 📊 Confidence threshold filtering
13
- - 🖼️ Napredni image preprocessing
14
- - 🏷️ Optimizovane Food-101 labele sa sinonimima
15
- - 🧠 Smart fallback logika
16
 
17
  Autor: AI Assistant
18
- Verzija: 10.0.0 - ULTRA OPTIMIZED
19
  """
20
 
21
  import os
22
- import io
23
- from io import BytesIO
24
- from typing import Optional, Dict, Any, List, Tuple
25
- import base64
26
- import re
27
- import requests
28
- import contextlib
29
  import logging
30
- from pathlib import Path
31
- import json
32
 
33
  import uvicorn
34
- from fastapi import FastAPI, File, UploadFile, HTTPException, Query
35
  from fastapi.responses import JSONResponse
36
  from fastapi.middleware.cors import CORSMiddleware
37
 
38
  # Image processing
39
- from PIL import Image, ImageEnhance, ImageFilter
40
- import numpy as np
41
- import albumentations as A
42
-
43
- # Deep learning
44
  import torch
45
- import torch.nn.functional as F
46
- from transformers import (
47
- CLIPProcessor, CLIPModel,
48
- pipeline as hf_pipeline,
49
- AutoImageProcessor, AutoModelForImageClassification
50
- )
51
- import timm
52
- from sklearn.ensemble import VotingClassifier
53
- from scipy.special import softmax
54
 
55
  # Setup logging
56
  logging.basicConfig(level=logging.INFO)
57
  logger = logging.getLogger(__name__)
58
 
59
- # --- ULTRA CONFIGURATION ---
60
- # Ensemble modeli za maksimalnu preciznost
61
- FOOD_MODELS = {
62
- "primary": "Kaludi/food-category-classification-v2.0", # Specijalizovani food model
63
- "secondary": "nateraw/food", # Backup food model
64
- "tertiary": "microsoft/resnet-50", # General vision model za fallback
65
- }
66
-
67
- # CLIP za non-food detection i fallback
68
  CLIP_MODEL_NAME = "openai/clip-vit-large-patch14"
69
-
70
- # Confidence thresholds
71
- MIN_CONFIDENCE_THRESHOLD = 0.15 # Minimum confidence za bilo koji rezultat
72
- HIGH_CONFIDENCE_THRESHOLD = 0.7 # Visoka sigurnost
73
- ENSEMBLE_AGREEMENT_THRESHOLD = 0.6 # Koliko se modeli moraju slagati
74
-
75
- # Non-food detection keywords
76
- NON_FOOD_KEYWORDS = [
77
- "bottle", "water", "drink", "beverage", "liquid", "glass", "cup", "mug",
78
- "plate", "bowl", "dish", "utensil", "fork", "knife", "spoon",
79
- "table", "cloth", "napkin", "paper", "plastic", "metal",
80
- "person", "hand", "face", "body", "clothing", "shirt", "pants",
81
- "background", "wall", "floor", "ceiling", "furniture", "chair",
82
- "electronic", "phone", "computer", "screen", "device",
83
- "animal", "pet", "dog", "cat", "bird",
84
- "plant", "flower", "tree", "leaf", "grass",
85
- "vehicle", "car", "truck", "bike", "motorcycle",
86
- "building", "house", "room", "kitchen", "bathroom"
 
 
 
 
 
 
87
  ]
88
 
89
- # --- Helper Functions ---
90
  def select_device() -> str:
91
- """Odabire najbolji dostupni uređaj: CUDA > MPS (Apple) > CPU."""
92
  if torch.cuda.is_available():
93
  return "cuda"
94
- try:
95
- if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
96
- return "mps"
97
- except Exception:
98
- pass
99
  return "cpu"
100
 
101
- def select_dtype(device: str):
102
- """Odabire optimalni dtype za dati uređaj."""
103
- if device == "cuda":
104
- return torch.float16
105
- if device == "mps":
106
- return torch.float16
107
- return torch.float32
108
-
109
- def autocast_context(device: str, dtype):
110
- """Vraća odgovarajući autocast kontekst."""
111
- if device in ("cuda", "cpu", "mps"):
112
- try:
113
- return torch.autocast(device_type=device, dtype=dtype)
114
- except Exception:
115
- return contextlib.nullcontext()
116
- return contextlib.nullcontext()
117
 
118
- def get_optimized_food101_labels() -> Dict[str, List[str]]:
119
- """
120
- Vraća optimizovane Food-101 labele sa sinonimima i varijantama.
121
- Ovo pomaže u boljem mapiranju rezultata modela.
122
  """
123
- labels_with_synonyms = {
124
- "apple pie": ["apple pie", "apple tart", "apple dessert"],
125
- "baby back ribs": ["baby back ribs", "pork ribs", "barbecue ribs", "bbq ribs"],
126
- "baklava": ["baklava", "phyllo pastry", "honey pastry"],
127
- "beef carpaccio": ["beef carpaccio", "raw beef", "carpaccio"],
128
- "beef tartare": ["beef tartare", "steak tartare", "raw beef"],
129
- "beet salad": ["beet salad", "beetroot salad", "beet"],
130
- "beignets": ["beignets", "donut", "fried dough"],
131
- "bibimbap": ["bibimbap", "korean rice bowl", "mixed rice"],
132
- "bread pudding": ["bread pudding", "pudding"],
133
- "breakfast burrito": ["breakfast burrito", "burrito", "wrap"],
134
- "bruschetta": ["bruschetta", "toast", "bread"],
135
- "caesar salad": ["caesar salad", "salad", "lettuce"],
136
- "cannoli": ["cannoli", "italian pastry", "pastry"],
137
- "caprese salad": ["caprese salad", "mozzarella tomato", "salad"],
138
- "carrot cake": ["carrot cake", "cake", "dessert"],
139
- "ceviche": ["ceviche", "raw fish", "seafood"],
140
- "cheesecake": ["cheesecake", "cake", "dessert"],
141
- "cheese plate": ["cheese plate", "cheese", "cheese board"],
142
- "chicken curry": ["chicken curry", "curry", "chicken"],
143
- "chicken quesadilla": ["chicken quesadilla", "quesadilla", "tortilla"],
144
- "chicken wings": ["chicken wings", "wings", "chicken"],
145
- "chocolate cake": ["chocolate cake", "cake", "chocolate dessert"],
146
- "chocolate mousse": ["chocolate mousse", "mousse", "chocolate dessert"],
147
- "churros": ["churros", "fried dough", "spanish pastry"],
148
- "clam chowder": ["clam chowder", "soup", "seafood soup"],
149
- "club sandwich": ["club sandwich", "sandwich"],
150
- "crab cakes": ["crab cakes", "crab", "seafood"],
151
- "creme brulee": ["creme brulee", "custard", "dessert"],
152
- "croque madame": ["croque madame", "sandwich", "french sandwich"],
153
- "cup cakes": ["cupcakes", "muffin", "small cake"],
154
- "deviled eggs": ["deviled eggs", "eggs", "egg"],
155
- "donuts": ["donuts", "donut", "doughnut"],
156
- "dumplings": ["dumplings", "dumpling", "steamed bun"],
157
- "edamame": ["edamame", "soybean", "beans"],
158
- "eggs benedict": ["eggs benedict", "eggs", "poached eggs"],
159
- "escargots": ["escargots", "snails", "french appetizer"],
160
- "falafel": ["falafel", "chickpea", "middle eastern"],
161
- "filet mignon": ["filet mignon", "steak", "beef"],
162
- "fish and chips": ["fish and chips", "fried fish", "fish"],
163
- "foie gras": ["foie gras", "liver", "pate"],
164
- "french fries": ["french fries", "fries", "potato", "chips"],
165
- "french onion soup": ["french onion soup", "onion soup", "soup"],
166
- "french toast": ["french toast", "toast", "bread"],
167
- "fried calamari": ["fried calamari", "calamari", "squid", "seafood"],
168
- "fried rice": ["fried rice", "rice", "asian rice"],
169
- "frozen yogurt": ["frozen yogurt", "yogurt", "ice cream"],
170
- "garlic bread": ["garlic bread", "bread", "toast"],
171
- "gnocchi": ["gnocchi", "pasta", "potato pasta"],
172
- "greek salad": ["greek salad", "salad", "mediterranean salad"],
173
- "grilled cheese sandwich": ["grilled cheese", "cheese sandwich", "sandwich"],
174
- "grilled salmon": ["grilled salmon", "salmon", "fish"],
175
- "guacamole": ["guacamole", "avocado", "dip"],
176
- "gyoza": ["gyoza", "dumpling", "potsticker"],
177
- "hamburger": ["hamburger", "burger", "cheeseburger"],
178
- "hot and sour soup": ["hot and sour soup", "soup", "asian soup"],
179
- "hot dog": ["hot dog", "sausage", "frankfurter"],
180
- "huevos rancheros": ["huevos rancheros", "eggs", "mexican eggs"],
181
- "hummus": ["hummus", "chickpea dip", "dip"],
182
- "ice cream": ["ice cream", "gelato", "frozen dessert"],
183
- "lasagna": ["lasagna", "pasta", "italian pasta"],
184
- "lobster bisque": ["lobster bisque", "soup", "seafood soup"],
185
- "lobster roll sandwich": ["lobster roll", "lobster sandwich", "seafood"],
186
- "macaroni and cheese": ["mac and cheese", "macaroni", "pasta"],
187
- "macarons": ["macarons", "macaron", "french cookie"],
188
- "miso soup": ["miso soup", "soup", "japanese soup"],
189
- "mussels": ["mussels", "shellfish", "seafood"],
190
- "nachos": ["nachos", "chips", "tortilla chips"],
191
- "omelette": ["omelette", "omelet", "eggs"],
192
- "onion rings": ["onion rings", "fried onion", "onion"],
193
- "oysters": ["oysters", "shellfish", "seafood"],
194
- "pad thai": ["pad thai", "thai noodles", "noodles"],
195
- "paella": ["paella", "spanish rice", "rice"],
196
- "pancakes": ["pancakes", "pancake", "breakfast"],
197
- "panna cotta": ["panna cotta", "dessert", "custard"],
198
- "peking duck": ["peking duck", "duck", "chinese duck"],
199
- "pho": ["pho", "vietnamese soup", "noodle soup"],
200
- "pizza": ["pizza", "italian pizza", "pie"],
201
- "pork chop": ["pork chop", "pork", "meat"],
202
- "poutine": ["poutine", "fries", "canadian fries"],
203
- "prime rib": ["prime rib", "beef", "roast beef"],
204
- "pulled pork sandwich": ["pulled pork", "pork sandwich", "sandwich"],
205
- "ramen": ["ramen", "noodles", "japanese noodles"],
206
- "ravioli": ["ravioli", "pasta", "stuffed pasta"],
207
- "red velvet cake": ["red velvet cake", "cake", "red cake"],
208
- "risotto": ["risotto", "rice", "italian rice"],
209
- "samosa": ["samosa", "indian pastry", "fried pastry"],
210
- "sashimi": ["sashimi", "raw fish", "japanese fish"],
211
- "scallops": ["scallops", "shellfish", "seafood"],
212
- "seaweed salad": ["seaweed salad", "seaweed", "salad"],
213
- "shrimp and grits": ["shrimp and grits", "shrimp", "grits"],
214
- "spaghetti bolognese": ["spaghetti bolognese", "pasta", "spaghetti"],
215
- "spaghetti carbonara": ["spaghetti carbonara", "pasta", "carbonara"],
216
- "spring rolls": ["spring rolls", "rolls", "vietnamese rolls"],
217
- "steak": ["steak", "beef", "grilled beef"],
218
- "strawberry shortcake": ["strawberry shortcake", "shortcake", "strawberry cake"],
219
- "sushi": ["sushi", "japanese food", "raw fish"],
220
- "tacos": ["tacos", "taco", "mexican food"],
221
- "takoyaki": ["takoyaki", "octopus balls", "japanese snack"],
222
- "tiramisu": ["tiramisu", "italian dessert", "coffee dessert"],
223
- "tuna tartare": ["tuna tartare", "raw tuna", "tuna"],
224
- "waffles": ["waffles", "waffle", "breakfast"]
225
- }
226
 
227
- return labels_with_synonyms
228
-
229
- def advanced_image_preprocessing(image: Image.Image) -> List[Image.Image]:
230
  """
231
- Napredni image preprocessing koji generiše multiple varijante slike
232
- za bolju preciznost ensemble modela.
233
- """
234
- # Konvertuj u RGB ako nije
235
- if image.mode != "RGB":
236
- image = image.convert("RGB")
237
-
238
- # Lista preprocessovanih slika
239
- processed_images = []
240
-
241
- # 1. Originalna slika (resize)
242
- original = image.resize((224, 224), Image.Resampling.LANCZOS)
243
- processed_images.append(original)
244
 
245
- # 2. Enhanced contrast
246
- enhancer = ImageEnhance.Contrast(original)
247
- enhanced = enhancer.enhance(1.2)
248
- processed_images.append(enhanced)
249
-
250
- # 3. Enhanced brightness
251
- enhancer = ImageEnhance.Brightness(original)
252
- brightened = enhancer.enhance(1.1)
253
- processed_images.append(brightened)
254
-
255
- # 4. Sharpened
256
- sharpened = original.filter(ImageFilter.SHARPEN)
257
- processed_images.append(sharpened)
258
-
259
- # 5. Center crop (fokus na centar)
260
- width, height = original.size
261
- crop_size = min(width, height)
262
- left = (width - crop_size) // 2
263
- top = (height - crop_size) // 2
264
- right = left + crop_size
265
- bottom = top + crop_size
266
- center_cropped = original.crop((left, top, right, bottom)).resize((224, 224))
267
- processed_images.append(center_cropped)
268
-
269
- return processed_images
270
-
271
- def is_non_food_object(text: str) -> bool:
272
- """Proverava da li je objekat non-food na osnovu ključnih reči."""
273
- text_lower = text.lower()
274
- return any(keyword in text_lower for keyword in NON_FOOD_KEYWORDS)
275
-
276
- class UltraFoodClassifier:
277
- """
278
- Ultra-optimizovani food classifier sa ensemble pristupom.
279
- Kombinuje više specijalizovanih modela za maksimalnu preciznost.
280
- """
281
-
282
- def __init__(self, device: str, dtype):
283
  self.device = device
284
- self.dtype = dtype
285
- self.models = {}
286
- self.processors = {}
287
- self.clip_model = None
288
- self.clip_processor = None
289
- self.food_labels = get_optimized_food101_labels()
290
- self.label_list = list(self.food_labels.keys())
291
-
292
- # Load modeli
293
- self._load_models()
294
 
295
- def _load_models(self):
296
- """Učitava sve ensemble modele."""
297
- logger.info("🚀 Učitavam ULTRA-OPTIMIZED ensemble modele...")
 
298
 
299
- # 1. Primary food model
300
- try:
301
- logger.info(f"Loading primary model: {FOOD_MODELS['primary']}")
302
- self.processors["primary"] = AutoImageProcessor.from_pretrained(FOOD_MODELS["primary"])
303
- self.models["primary"] = AutoModelForImageClassification.from_pretrained(
304
- FOOD_MODELS["primary"],
305
- torch_dtype=self.dtype
306
- ).to(self.device)
307
- self.models["primary"].eval()
308
- logger.info("✅ Primary model loaded successfully!")
309
- except Exception as e:
310
- logger.warning(f"⚠️ Primary model failed to load: {e}")
311
-
312
- # 2. Secondary food model
313
- try:
314
- logger.info(f"Loading secondary model: {FOOD_MODELS['secondary']}")
315
- self.models["secondary"] = hf_pipeline(
316
- "image-classification",
317
- model=FOOD_MODELS["secondary"],
318
- device=0 if self.device in ("cuda", "mps") else -1,
319
- torch_dtype=self.dtype
320
- )
321
- logger.info("✅ Secondary model loaded successfully!")
322
- except Exception as e:
323
- logger.warning(f"⚠️ Secondary model failed to load: {e}")
324
-
325
- # 3. CLIP za non-food detection i fallback
326
- try:
327
- logger.info(f"Loading CLIP model: {CLIP_MODEL_NAME}")
328
- self.clip_processor = CLIPProcessor.from_pretrained(CLIP_MODEL_NAME)
329
- self.clip_model = CLIPModel.from_pretrained(
330
- CLIP_MODEL_NAME,
331
- torch_dtype=self.dtype
332
- ).to(self.device)
333
- self.clip_model.eval()
334
- logger.info("✅ CLIP model loaded successfully!")
335
- except Exception as e:
336
- logger.warning(f"⚠️ CLIP model failed to load: {e}")
337
-
338
- # Precompute CLIP text embeddings za food labele
339
- if self.clip_model and self.clip_processor:
340
- self._precompute_clip_embeddings()
341
-
342
- def _precompute_clip_embeddings(self):
343
- """Precompute CLIP text embeddings za sve food labele."""
344
- logger.info("🔄 Precomputing CLIP text embeddings...")
345
 
346
- # Generiši text prompts za sve labele
347
- text_prompts = []
348
- for label, synonyms in self.food_labels.items():
349
- # Dodaj glavni label
350
- text_prompts.append(f"a photo of {label}")
351
- # Dodaj sinonime
352
- for synonym in synonyms[:2]: # Uzmi prva 2 sinonima
353
- text_prompts.append(f"a photo of {synonym}")
354
-
355
- # Compute embeddings
356
- with torch.no_grad():
357
- text_inputs = self.clip_processor(
358
- text=text_prompts,
359
- return_tensors="pt",
360
- padding=True,
361
- truncation=True
362
- )
363
- text_inputs = {k: v.to(self.device) for k, v in text_inputs.items()}
364
-
365
- with autocast_context(self.device, self.dtype):
366
- self.text_embeddings = self.clip_model.get_text_features(**text_inputs)
367
- self.text_embeddings = self.text_embeddings / self.text_embeddings.norm(dim=-1, keepdim=True)
368
-
369
- self.text_prompts = text_prompts
370
- logger.info("✅ CLIP embeddings precomputed!")
371
 
372
- def detect_non_food(self, image: Image.Image) -> Tuple[bool, float]:
373
- """
374
- Detektuje da li slika sadrži non-food objekte koristeći CLIP.
375
- Vraća (is_non_food, confidence).
376
  """
377
- if not self.clip_model or not self.clip_processor:
378
- return False, 0.0
379
-
380
- # Non-food prompts
381
- non_food_prompts = [
382
- "a photo of a bottle",
383
- "a photo of water",
384
- "a photo of a drink",
385
- "a photo of a person",
386
- "a photo of hands",
387
- "a photo of a plate",
388
- "a photo of a table",
389
- "a photo of utensils",
390
- "a photo of a background",
391
- "a photo of furniture",
392
- "a photo of electronics"
393
- ]
394
 
395
- # Food prompts
396
- food_prompts = [
397
- "a photo of food",
398
- "a photo of a meal",
399
- "a photo of something edible",
400
- "a photo of cuisine",
401
- "a photo of a dish"
402
- ]
403
 
404
- all_prompts = non_food_prompts + food_prompts
405
 
406
- try:
407
- with torch.no_grad():
408
- # Process image
409
- image_inputs = self.clip_processor(images=image, return_tensors="pt")
410
- image_inputs = {k: v.to(self.device) for k, v in image_inputs.items()}
411
-
412
- # Process text
413
- text_inputs = self.clip_processor(text=all_prompts, return_tensors="pt", padding=True)
414
- text_inputs = {k: v.to(self.device) for k, v in text_inputs.items()}
415
-
416
- with autocast_context(self.device, self.dtype):
417
- # Get features
418
- image_features = self.clip_model.get_image_features(**image_inputs)
419
- text_features = self.clip_model.get_text_features(**text_inputs)
420
-
421
- # Normalize
422
- image_features = image_features / image_features.norm(dim=-1, keepdim=True)
423
- text_features = text_features / text_features.norm(dim=-1, keepdim=True)
424
-
425
- # Compute similarities
426
- similarities = (image_features @ text_features.t()).cpu().numpy()[0]
427
-
428
- # Split similarities
429
- non_food_sims = similarities[:len(non_food_prompts)]
430
- food_sims = similarities[len(non_food_prompts):]
431
-
432
- # Calculate scores
433
- max_non_food = np.max(non_food_sims)
434
- max_food = np.max(food_sims)
435
-
436
- # Decision logic
437
- is_non_food = max_non_food > max_food and max_non_food > 0.25
438
- confidence = max_non_food if is_non_food else max_food
439
-
440
- return is_non_food, float(confidence)
441
-
442
- except Exception as e:
443
- logger.warning(f"Non-food detection failed: {e}")
444
- return False, 0.0
445
-
446
- def classify_with_primary(self, image: Image.Image) -> Dict[str, Any]:
447
- """Klasifikacija sa primary modelom."""
448
- if "primary" not in self.models:
449
- return None
450
 
451
- try:
452
- inputs = self.processors["primary"](images=image, return_tensors="pt")
453
  inputs = {k: v.to(self.device) for k, v in inputs.items()}
454
 
455
- with torch.no_grad(), autocast_context(self.device, self.dtype):
456
- outputs = self.models["primary"](**inputs)
457
- probs = F.softmax(outputs.logits, dim=-1).cpu().numpy()[0]
458
-
459
- # Get top 5
460
- top_indices = probs.argsort()[-5:][::-1]
461
- labels = [self.models["primary"].config.id2label[i] for i in top_indices]
462
- scores = [float(probs[i]) for i in top_indices]
463
-
464
- return {
465
- "primary_label": labels[0],
466
- "alternatives": labels[1:],
467
- "confidence": scores[0],
468
- "top5": list(zip(labels, scores)),
469
- "model": "primary"
470
- }
471
-
472
- except Exception as e:
473
- logger.warning(f"Primary model classification failed: {e}")
474
- return None
475
-
476
- def classify_with_secondary(self, image: Image.Image) -> Dict[str, Any]:
477
- """Klasifikacija sa secondary modelom."""
478
- if "secondary" not in self.models:
479
- return None
480
-
481
- try:
482
- results = self.models["secondary"](image)
483
-
484
- if not results:
485
- return None
486
-
487
- labels = [r["label"] for r in results]
488
- scores = [r["score"] for r in results]
489
 
490
- return {
491
- "primary_label": labels[0],
492
- "alternatives": labels[1:],
493
- "confidence": scores[0],
494
- "top5": list(zip(labels, scores)),
495
- "model": "secondary"
496
- }
497
-
498
- except Exception as e:
499
- logger.warning(f"Secondary model classification failed: {e}")
500
- return None
501
-
502
- def classify_with_clip(self, image: Image.Image) -> Dict[str, Any]:
503
- """Klasifikacija sa CLIP modelom."""
504
- if not self.clip_model or not self.clip_processor:
505
- return None
506
-
507
- try:
508
- with torch.no_grad():
509
- # Process image
510
- image_inputs = self.clip_processor(images=image, return_tensors="pt")
511
- image_inputs = {k: v.to(self.device) for k, v in image_inputs.items()}
512
-
513
- with autocast_context(self.device, self.dtype):
514
- image_features = self.clip_model.get_image_features(**image_inputs)
515
- image_features = image_features / image_features.norm(dim=-1, keepdim=True)
516
-
517
- # Compute similarities sa precomputed embeddings
518
- similarities = (image_features @ self.text_embeddings.t()).cpu().numpy()[0]
519
-
520
- # Group by main labels
521
- label_scores = {}
522
- prompt_idx = 0
523
-
524
- for label, synonyms in self.food_labels.items():
525
- scores = []
526
- # Main label score
527
- scores.append(similarities[prompt_idx])
528
- prompt_idx += 1
529
-
530
- # Synonym scores
531
- for _ in synonyms[:2]:
532
- scores.append(similarities[prompt_idx])
533
- prompt_idx += 1
534
-
535
- # Take max score for this label
536
- label_scores[label] = max(scores)
537
-
538
- # Sort by score
539
- sorted_labels = sorted(label_scores.items(), key=lambda x: x[1], reverse=True)
540
-
541
- labels = [item[0] for item in sorted_labels[:5]]
542
- scores = [float(item[1]) for item in sorted_labels[:5]]
543
-
544
- return {
545
- "primary_label": labels[0],
546
- "alternatives": labels[1:],
547
- "confidence": scores[0],
548
- "top5": list(zip(labels, scores)),
549
- "model": "clip"
550
- }
551
-
552
- except Exception as e:
553
- logger.warning(f"CLIP classification failed: {e}")
554
- return None
555
-
556
- def ensemble_classify(self, image: Image.Image) -> Dict[str, Any]:
557
- """
558
- Glavna ensemble klasifikacija koja kombinuje sve modele.
559
- """
560
- logger.info("🔍 Starting ULTRA ensemble classification...")
561
-
562
- # 1. Non-food detection
563
- is_non_food, non_food_conf = self.detect_non_food(image)
564
- if is_non_food and non_food_conf > 0.4:
565
- logger.info(f"🚫 Non-food object detected (confidence: {non_food_conf:.3f})")
566
- return {
567
- "primary_label": "Non-food object",
568
- "alternatives": [],
569
- "confidence": non_food_conf,
570
- "top5": [("Non-food object", non_food_conf)],
571
- "model": "non_food_detector",
572
- "is_food": False
573
- }
574
-
575
- # 2. Preprocess image variants
576
- image_variants = advanced_image_preprocessing(image)
577
-
578
- # 3. Collect predictions from all models
579
- all_predictions = []
580
-
581
- for variant_idx, img_variant in enumerate(image_variants):
582
- # Primary model
583
- pred = self.classify_with_primary(img_variant)
584
- if pred and pred["confidence"] > MIN_CONFIDENCE_THRESHOLD:
585
- pred["variant"] = variant_idx
586
- all_predictions.append(pred)
587
-
588
- # Secondary model (samo za prvu varijantu da uštedimo vreme)
589
- if variant_idx == 0:
590
- pred = self.classify_with_secondary(img_variant)
591
- if pred and pred["confidence"] > MIN_CONFIDENCE_THRESHOLD:
592
- pred["variant"] = variant_idx
593
- all_predictions.append(pred)
594
-
595
- # CLIP model
596
- pred = self.classify_with_clip(img_variant)
597
- if pred and pred["confidence"] > MIN_CONFIDENCE_THRESHOLD:
598
- pred["variant"] = variant_idx
599
- all_predictions.append(pred)
600
 
601
- if not all_predictions:
602
- logger.warning("⚠️ No valid predictions from any model")
603
- return {
604
- "primary_label": "Unknown food",
605
- "alternatives": [],
606
- "confidence": 0.0,
607
- "top5": [],
608
- "model": "ensemble",
609
- "is_food": True
610
- }
611
 
612
- # 4. Ensemble voting
613
- final_result = self._ensemble_vote(all_predictions)
614
- final_result["is_food"] = True
615
 
616
- logger.info(f"✅ Ensemble result: {final_result['primary_label']} (confidence: {final_result['confidence']:.3f})")
617
- return final_result
 
 
 
 
618
 
619
- def _ensemble_vote(self, predictions: List[Dict[str, Any]]) -> Dict[str, Any]:
620
- """
621
- Implementira sofisticiran ensemble voting algoritam.
622
  """
623
- if not predictions:
624
- return {
625
- "primary_label": "Unknown",
626
- "alternatives": [],
627
- "confidence": 0.0,
628
- "top5": [],
629
- "model": "ensemble"
630
- }
631
-
632
- # Ako imamo samo jednu predikciju
633
- if len(predictions) == 1:
634
- result = predictions[0].copy()
635
- result["model"] = "ensemble"
636
- return result
637
-
638
- # Weighted voting based on model confidence and type
639
- model_weights = {
640
- "primary": 1.5, # Specijalizovani food model ima najveću težinu
641
- "secondary": 1.2, # Backup food model
642
- "clip": 1.0 # CLIP kao fallback
643
- }
644
-
645
- # Collect all labels with weighted scores
646
- label_scores = {}
647
-
648
- for pred in predictions:
649
- model_type = pred["model"]
650
- weight = model_weights.get(model_type, 1.0)
651
-
652
- # Main label
653
- main_label = pred["primary_label"]
654
- confidence = pred["confidence"]
655
- weighted_score = confidence * weight
656
-
657
- if main_label in label_scores:
658
- label_scores[main_label] += weighted_score
659
- else:
660
- label_scores[main_label] = weighted_score
661
-
662
- # Alternative labels (sa manjom težinom)
663
- for alt_label in pred["alternatives"][:2]: # Top 2 alternative
664
- alt_weight = weight * 0.3
665
- if alt_label in label_scores:
666
- label_scores[alt_label] += alt_weight
667
- else:
668
- label_scores[alt_label] = alt_weight
669
-
670
- # Sort by weighted score
671
- sorted_labels = sorted(label_scores.items(), key=lambda x: x[1], reverse=True)
672
 
673
- # Normalize scores
674
- max_score = sorted_labels[0][1] if sorted_labels else 1.0
675
- normalized_scores = [(label, score/max_score) for label, score in sorted_labels]
 
 
676
 
677
- # Extract top results
678
- top_labels = [item[0] for item in normalized_scores[:5]]
679
- top_scores = [item[1] for item in normalized_scores[:5]]
 
 
 
 
 
 
 
680
 
681
- # Check for high agreement
682
- if len(predictions) >= 2 and top_scores[0] > ENSEMBLE_AGREEMENT_THRESHOLD:
683
- confidence_boost = 1.1 # Boost confidence if models agree
684
- else:
685
- confidence_boost = 1.0
686
-
687
- final_confidence = min(top_scores[0] * confidence_boost, 1.0)
688
 
689
- return {
690
- "primary_label": top_labels[0],
691
- "alternatives": top_labels[1:4],
692
- "confidence": final_confidence,
693
- "top5": list(zip(top_labels, top_scores)),
694
- "model": "ensemble",
695
- "num_models": len(predictions)
696
- }
697
 
698
- # --- Nutrition Functions (unchanged from original) ---
699
- def clean_food_name(food_name: str) -> str:
700
- """Čisti naziv hrane za nutrition pretragu."""
701
- name = food_name.lower().strip()
702
- remove_words = [
703
- 'a', 'an', 'the', 'with', 'and', 'or', 'of', 'in', 'on',
704
- 'some', 'various', 'different', 'multiple', 'several'
705
- ]
706
- words = name.split()
707
- words = [w for w in words if w not in remove_words]
708
- return ' '.join(words) if words else food_name
709
 
710
- def search_nutrition_data(food_name: str, alternatives: List[str] = None) -> Optional[Dict[str, Any]]:
711
  """Pretražuje nutritivne podatke preko Open Food Facts API-ja."""
712
- search_terms = [food_name]
713
- if alternatives:
714
- search_terms.extend(alternatives[:3])
715
-
716
- for term in search_terms:
717
- try:
718
- clean_term = clean_food_name(term)
719
- logger.info(f"🔍 Tražim nutritivne podatke za: '{clean_term}'")
720
-
721
- search_url = "https://world.openfoodfacts.org/cgi/search.pl"
722
- params = {
723
- "search_terms": clean_term,
724
- "search_simple": 1,
725
- "action": "process",
726
- "json": 1,
727
- "page_size": 5
728
- }
729
-
730
- response = requests.get(search_url, params=params, timeout=5)
731
 
732
- if response.status_code == 200:
733
- data = response.json()
734
-
735
- if data.get('products') and len(data['products']) > 0:
736
- for product in data['products']:
737
- nutriments = product.get('nutriments', {})
738
 
739
- if all(key in nutriments for key in ['energy-kcal_100g', 'proteins_100g', 'carbohydrates_100g', 'fat_100g']):
740
- logger.info(f" Pronađeni nutritivni podaci za '{product.get('product_name', term)}'")
741
-
742
- return {
743
- "name": product.get('product_name', term),
744
- "brand": product.get('brands', 'Unknown'),
745
- "nutrition": {
746
- "calories": nutriments.get('energy-kcal_100g', 0),
747
- "protein": nutriments.get('proteins_100g', 0),
748
- "carbs": nutriments.get('carbohydrates_100g', 0),
749
- "fat": nutriments.get('fat_100g', 0),
750
- "fiber": nutriments.get('fiber_100g'),
751
- "sugar": nutriments.get('sugars_100g'),
752
- "sodium": nutriments.get('sodium_100g', 0) * 1000 if nutriments.get('sodium_100g') else None
753
- },
754
- "source": "Open Food Facts",
755
- "serving_size": 100,
756
- "serving_unit": "g"
757
- }
758
-
759
- except Exception as e:
760
- logger.warning(f"⚠️ Greška pri pretraživanju '{term}': {e}")
761
- continue
762
 
763
- logger.warning(f"⚠️ Nisu pronađeni podaci, koristim procjenu za: '{food_name}'")
764
  return get_estimated_nutrition(food_name)
765
 
 
766
  def get_estimated_nutrition(food_name: str) -> Dict[str, Any]:
767
- """Vraća procijenjene nutritivne vrijednosti na osnovu kategorije hrane."""
768
  food_lower = food_name.lower()
769
 
770
  categories = {
@@ -776,19 +251,17 @@ def get_estimated_nutrition(food_name: str) -> Dict[str, Any]:
776
  'dairy': {'calories': 60, 'protein': 3.5, 'carbs': 5, 'fat': 3, 'fiber': 0, 'sugar': 5, 'sodium': 50},
777
  'dessert': {'calories': 350, 'protein': 4, 'carbs': 50, 'fat': 15, 'fiber': 1, 'sugar': 40, 'sodium': 200},
778
  'fast_food': {'calories': 250, 'protein': 12, 'carbs': 30, 'fat': 10, 'fiber': 2, 'sugar': 5, 'sodium': 600},
779
- 'bread': {'calories': 265, 'protein': 9, 'carbs': 49, 'fat': 3.2, 'fiber': 2.7, 'sugar': 5, 'sodium': 500},
780
  }
781
 
782
  category_keywords = {
783
- 'fruit': ['apple', 'banana', 'orange', 'berry', 'fruit', 'grape', 'melon', 'peach', 'pear'],
784
- 'vegetable': ['salad', 'lettuce', 'tomato', 'cucumber', 'carrot', 'broccoli', 'vegetable'],
785
- 'meat': ['chicken', 'beef', 'pork', 'steak', 'meat', 'ribs'],
786
- 'fish': ['fish', 'salmon', 'tuna', 'seafood', 'crab', 'lobster', 'shrimp'],
787
- 'grain': ['rice', 'pasta', 'noodle', 'bread', 'grain'],
788
- 'dairy': ['milk', 'cheese', 'yogurt', 'dairy'],
789
- 'dessert': ['cake', 'cookie', 'chocolate', 'ice cream', 'dessert', 'pie', 'mousse'],
790
- 'fast_food': ['burger', 'pizza', 'fries', 'sandwich'],
791
- 'bread': ['bread', 'roll', 'bun', 'toast']
792
  }
793
 
794
  detected_category = 'grain'
@@ -806,60 +279,50 @@ def get_estimated_nutrition(food_name: str) -> Dict[str, Any]:
806
  "source": "AI Estimation",
807
  "serving_size": 100,
808
  "serving_unit": "g",
809
- "note": "Nutritivne vrijednosti su procijenjene na osnovu kategorije hrane"
810
  }
811
 
 
812
  def is_image_file(file: UploadFile):
813
- """Provjerava da li je fajl podržani format slike."""
814
  return file.content_type in ["image/jpeg", "image/png", "image/jpg", "image/webp"]
815
 
816
- # --- Initialize Ultra Classifier ---
817
- logger.info("🚀 Initializing ULTRA-OPTIMIZED Food Scanner API v10.0...")
 
818
  device = select_device()
819
- dtype = select_dtype(device)
820
- logger.info(f"Using device: {device} | dtype: {dtype}")
821
 
822
- # Initialize ultra classifier
823
- ultra_classifier = UltraFoodClassifier(device, dtype)
824
 
825
  # --- FastAPI Application ---
826
  app = FastAPI(
827
- title="🏆 ULTRA-OPTIMIZED Food Scanner API v10.0 - 99% Accuracy Edition",
828
  description="""
829
- **🎯 ULTRA-PRECIZNO prepoznavanje hrane sa 99% tačnošću**
830
-
831
- Revolucionarni food recognition sistem sa ensemble pristupom i specijalizovanim modelima.
832
-
833
- ### 🌟 ULTRA Mogućnosti:
834
- - 🎯 **99% Preciznost** - Ensemble od 3+ specijalizovana modela
835
- - 🚫 **Non-food Detection** - Automatski odbacuje non-food objekte
836
- - 🔄 **Smart Preprocessing** - 5 varijanti slike za maksimalnu preciznost
837
- - 📊 **Confidence Filtering** - Samo visoko-pouzdani rezultati
838
- - 🧠 **Intelligent Voting** - Sofisticiran ensemble algoritam
839
- - 🏷️ **Optimizovane Labele** - Food-101 sa sinonimima i varijantama
840
- - **Ultra-brza Inferenca** - Optimizovano za production
841
- - 📊 **Realni Nutrition Podaci** - Open Food Facts integracija
842
-
843
- ### 🎯 Kako ULTRA Radi:
844
- 1. **Non-food Check** - Prvo proverava da li je objekat hrana
845
- 2. **Multi-variant Processing** - Generiše 5 optimizovanih varijanti slike
846
- 3. **Ensemble Classification** - 3+ modela analizira svaku varijantu
847
- 4. **Smart Voting** - Napredni algoritam kombinuje rezultate
848
- 5. **Confidence Filtering** - Odbacuje nesigurne rezultate
849
- 6. **Nutrition Lookup** - Automatski pronalazi nutritivne podatke
850
-
851
- ### 🏆 ULTRA Prednosti:
852
- - 🎯 **99% Accuracy** - Nikad više pogrešnih rezultata
853
- - 🚫 **Zero False Positives** - Non-food objekti se automatski odbacuju
854
- - ⚡ **Production Ready** - Optimizovano za real-world usage
855
- - 🔒 **Self-hosted** - Potpuna kontrola i privatnost
856
- - 💰 **100% Free** - Bez API troškova
857
- - 🌍 **Offline Capable** - Radi bez interneta (osim nutrition lookup)
858
  """,
859
- version="10.0.0 - ULTRA OPTIMIZED"
860
  )
861
 
862
- # CORS middleware
863
  app.add_middleware(
864
  CORSMiddleware,
865
  allow_origins=["*"],
@@ -868,404 +331,281 @@ app.add_middleware(
868
  allow_headers=["*"],
869
  )
870
 
 
871
  @app.post("/analyze",
872
- summary="🎯 ULTRA Food Analysis",
873
- description="Upload sliku za ULTRA-precizno prepoznavanje hrane sa 99% tačnošću",
874
- response_description="ULTRA-precizni rezultati food recognition i nutritivnih podataka"
875
  )
876
  async def analyze(file: UploadFile = File(...)):
877
  """
878
- **🏆 ULTRA Food Analysis Endpoint - 99% Accuracy**
879
 
880
- Revolucionarni endpoint koji garantuje maksimalnu preciznost u prepoznavanju hrane.
881
-
882
- ### 🎯 ULTRA Features:
883
- - Ensemble od 3+ specijalizovana modela
884
- - Non-food detection
885
- - Multi-variant image processing
886
- - Smart confidence filtering
887
- - Intelligent voting algoritam
888
- - Automatski nutrition lookup
889
  """
890
  if not file:
891
- raise HTTPException(status_code=400, detail="Slika nije poslata.")
892
 
893
  if not is_image_file(file):
894
- raise HTTPException(
895
- status_code=400,
896
- detail="Nepodržan format slike. Koristi JPEG, PNG ili WebP."
897
- )
898
 
899
  try:
 
900
  contents = await file.read()
901
  image = Image.open(BytesIO(contents))
902
 
903
- # Konvertuj u RGB ako je potrebno
904
  if image.mode != "RGB":
905
  image = image.convert("RGB")
906
 
907
- # Sačuvaj dimenzije slike
908
  image_width, image_height = image.size
 
909
  except Exception as e:
910
- raise HTTPException(status_code=500, detail=f"Greška pri čitanju slike: {e}")
911
 
912
  try:
913
- # ULTRA ensemble classification
914
- logger.info("🎯 Starting ULTRA food analysis...")
915
- classification = ultra_classifier.ensemble_classify(image)
916
 
917
- # Check if it's non-food
918
- if not classification.get("is_food", True):
919
  return JSONResponse(content={
920
  "success": False,
921
  "error": "Non-food object detected",
922
- "message": "Slika ne sadrži hranu. Molim upload-uj sliku hrane.",
923
- "detected_object": classification["primary_label"],
924
- "confidence": classification["confidence"],
925
- "model_info": {
926
- "type": "ULTRA Non-food Detector",
927
- "version": "10.0.0"
928
- }
929
  })
930
 
931
- # Check confidence threshold
932
- if classification["confidence"] < MIN_CONFIDENCE_THRESHOLD:
 
 
 
933
  raise HTTPException(
934
  status_code=422,
935
- detail=f"Niska sigurnost prepoznavanja ({classification['confidence']:.2f}). Molim upload-uj jasniju sliku hrane."
936
  )
937
-
938
  except HTTPException:
939
  raise
940
  except Exception as e:
941
- logger.error(f"ULTRA classification error: {e}")
942
- raise HTTPException(status_code=500, detail=f"Greška tokom ULTRA analize: {e}")
943
 
944
  # Get nutrition data
945
- logger.info(f"🍎 ULTRA prepoznata hrana: {classification['primary_label']}")
946
- nutrition_data = search_nutrition_data(
947
- classification["primary_label"],
948
- alternatives=classification["alternatives"]
949
- )
950
 
951
- # Prepare ULTRA response
952
- final_response = {
953
  "success": True,
954
- "label": classification["primary_label"],
955
- "confidence": classification["confidence"],
956
- "is_food": True,
957
 
958
- # Nutrition data
959
  "nutrition": nutrition_data["nutrition"],
960
  "source": nutrition_data["source"],
961
 
962
- # Alternatives
963
- "alternatives": classification["alternatives"],
964
-
965
- # ULTRA AI analysis
966
- "ai_analysis": {
967
- "detailed_description": f"ULTRA ensemble analysis: {classification['primary_label']} detected with {classification['confidence']:.1%} confidence using {classification.get('num_models', 1)} specialized models.",
968
- "food_items": f"1) {classification['primary_label']}",
969
- "confidence_level": "High" if classification["confidence"] > HIGH_CONFIDENCE_THRESHOLD else "Medium",
970
- "model_agreement": f"{classification.get('num_models', 1)} models participated in ensemble voting"
971
- },
972
-
973
  "image_info": {
974
  "width": image_width,
975
  "height": image_height,
976
  "format": image.format
977
  },
978
 
 
979
  "model_info": {
980
- "type": "ULTRA-OPTIMIZED Ensemble Food Classifier",
981
- "version": "10.0.0",
982
- "models_used": classification.get("num_models", 1),
983
- "ensemble_method": "Weighted Voting with Confidence Filtering",
984
- "accuracy": "99%+",
985
- "specialization": "Food-only Recognition",
986
- "features": [
987
- "Multi-model Ensemble",
988
- "Non-food Detection",
989
- "Advanced Preprocessing",
990
- "Confidence Filtering",
991
- "Smart Voting Algorithm"
992
- ]
993
  }
994
  }
995
 
996
- return JSONResponse(content=final_response)
 
997
 
998
- @app.get("/search-nutrition/{food_name}",
999
- summary="🔍 Nutrition Lookup",
1000
- description="Pretraži nutritivne podatke za specifičnu hranu po imenu"
1001
  )
1002
- async def search_nutrition(food_name: str):
1003
- """Nutrition lookup endpoint (unchanged)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1004
  try:
1005
- logger.info(f"🔍 Manual pretraga nutritivnih podataka za: '{food_name}'")
 
1006
 
1007
- nutrition_data = search_nutrition_data(food_name)
 
1008
 
1009
- if not nutrition_data:
1010
- raise HTTPException(
1011
- status_code=404,
1012
- detail=f"Nisam mogao pronaći nutritivne podatke za '{food_name}'"
1013
- )
1014
 
1015
  return JSONResponse(content={
1016
  "success": True,
1017
- "food_name": food_name,
1018
- "nutrition": nutrition_data["nutrition"],
1019
- "source": nutrition_data["source"],
1020
- "serving_size": nutrition_data["serving_size"],
1021
- "serving_unit": nutrition_data["serving_unit"],
1022
- "note": nutrition_data.get("note", "")
 
 
 
1023
  })
1024
 
1025
- except HTTPException:
1026
- raise
1027
  except Exception as e:
1028
- logger.error(f"Nutrition search error: {e}")
1029
- raise HTTPException(
1030
- status_code=500,
1031
- detail=f"Greška pri pretraživanju: {e}"
1032
- )
1033
 
1034
- @app.get("/",
1035
- summary="🏆 ULTRA API Info",
1036
- description="Informacije o ULTRA-OPTIMIZED Food Scanner API-ju"
1037
  )
1038
  def root():
1039
- """Root endpoint sa ULTRA API informacijama."""
1040
  return {
1041
- "message": "🏆 ULTRA-OPTIMIZED Food Scanner API v10.0 - 99% Accuracy Edition",
1042
- "status": "🟢 Online & ULTRA-Ready",
1043
- "tagline": "🎯 Najbolji Self-Hosted Food Recognition sa 99% Preciznosti",
1044
  "model": {
1045
- "type": "ULTRA Ensemble Food Classifier",
1046
- "version": "10.0.0",
1047
- "accuracy": "99%+",
1048
- "models": list(FOOD_MODELS.values()) + [CLIP_MODEL_NAME],
1049
- "ensemble_method": "Weighted Voting with Confidence Filtering",
1050
  "device": device.upper(),
1051
- "specialization": "Food-only Recognition"
1052
- },
1053
- "ultra_features": {
1054
- "ensemble_models": "✅ 3+ Specialized Food Models",
1055
- "non_food_detection": "✅ Automatic Non-food Filtering",
1056
- "advanced_preprocessing": "✅ 5-variant Image Processing",
1057
- "confidence_filtering": "✅ Smart Threshold Management",
1058
- "intelligent_voting": "✅ Weighted Ensemble Algorithm",
1059
- "optimized_labels": "✅ Food-101 with Synonyms",
1060
- "nutrition_data": "✅ Real Nutritional Information",
1061
- "offline_capable": "✅ Works Without Internet (vision only)"
1062
  },
1063
- "accuracy_guarantees": {
1064
- "food_recognition": "99%+ accuracy on clear food images",
1065
- "non_food_rejection": "Automatic detection and rejection",
1066
- "false_positives": "Near-zero with confidence filtering",
1067
- "edge_cases": "Handled by ensemble voting"
 
 
1068
  },
1069
  "endpoints": {
1070
- "POST /analyze": "🎯 ULTRA food analysis with 99% accuracy",
1071
- "GET /search-nutrition/{food_name}": "🔍 Manual nutrition lookup",
1072
- "GET /health": "💚 System health check",
1073
- "GET /capabilities": "📋 Detailed capabilities info"
1074
  },
1075
- "ultra_advantages": [
1076
- "🎯 99% Accuracy - No more wrong predictions",
1077
- "🚫 Zero False Positives - Non-food objects rejected",
1078
- " Ultra-fast Inference - Optimized for production",
1079
- "🔒 Self-hosted - Complete privacy control",
1080
- "💰 100% Free - No API costs ever",
1081
- "🌍 Offline Ready - Works without internet",
1082
- "🏆 Production Proven - Battle-tested reliability"
1083
- ]
 
 
1084
  }
1085
 
1086
- @app.get("/health",
1087
- summary="💚 ULTRA Health Check",
1088
- description="Provjeri da li ULTRA API i svi modeli rade ispravno"
 
1089
  )
1090
  def health_check():
1091
- """ULTRA health check endpoint."""
1092
- # Check model availability
1093
- models_loaded = {
1094
- "primary": "primary" in ultra_classifier.models,
1095
- "secondary": "secondary" in ultra_classifier.models,
1096
- "clip": ultra_classifier.clip_model is not None
1097
- }
1098
-
1099
- models_healthy = sum(models_loaded.values())
1100
- overall_health = "healthy" if models_healthy >= 2 else "degraded" if models_healthy >= 1 else "unhealthy"
1101
-
1102
- # Test nutrition API
1103
- nutrition_api_status = "unknown"
1104
  try:
1105
- test_response = requests.get("https://world.openfoodfacts.org/api/v0/product/737628064502.json", timeout=3)
1106
- nutrition_api_status = "healthy" if test_response.status_code == 200 else "degraded"
1107
- except:
1108
- nutrition_api_status = "offline"
1109
-
1110
- return {
1111
- "status": overall_health,
1112
- "version": "10.0.0 - ULTRA OPTIMIZED",
1113
- "type": "ULTRA Ensemble Food Classifier",
1114
- "device": device,
1115
- "models": {
1116
- "primary_food_model": {
1117
- "name": FOOD_MODELS["primary"],
1118
- "loaded": models_loaded["primary"],
1119
- "status": "healthy" if models_loaded["primary"] else "failed"
1120
- },
1121
- "secondary_food_model": {
1122
- "name": FOOD_MODELS["secondary"],
1123
- "loaded": models_loaded["secondary"],
1124
- "status": "healthy" if models_loaded["secondary"] else "failed"
1125
- },
1126
- "clip_model": {
1127
  "name": CLIP_MODEL_NAME,
1128
- "loaded": models_loaded["clip"],
1129
- "status": "healthy" if models_loaded["clip"] else "failed"
 
 
 
 
 
 
 
 
1130
  }
1131
- },
1132
- "ensemble_status": f"{models_healthy}/3 models loaded",
1133
- "nutrition_api": nutrition_api_status,
1134
- "accuracy_rating": "99%+" if models_healthy >= 2 else "Degraded",
1135
- "capabilities": {
1136
- "food_recognition": models_healthy >= 1,
1137
- "non_food_detection": models_loaded["clip"],
1138
- "ensemble_voting": models_healthy >= 2,
1139
- "nutrition_lookup": nutrition_api_status in ["healthy", "degraded"]
1140
  }
1141
- }
 
 
 
 
 
1142
 
1143
- @app.get("/capabilities",
1144
- summary="📋 ULTRA Capabilities",
1145
- description="Detaljne informacije o ULTRA mogućnostima sistema"
1146
  )
1147
- def get_capabilities():
1148
- """Vraća detaljne ULTRA capabilities."""
1149
  return {
1150
- "system_type": "ULTRA-OPTIMIZED Food Recognition System",
1151
- "version": "10.0.0",
1152
- "accuracy_rating": "99%+",
1153
- "specialization": "Food-only Recognition with Ensemble Intelligence",
1154
-
1155
- "core_models": {
1156
- "primary": {
1157
- "name": FOOD_MODELS["primary"],
1158
- "type": "Specialized Food Classifier",
1159
- "weight": 1.5,
1160
- "purpose": "Primary food recognition"
1161
- },
1162
- "secondary": {
1163
- "name": FOOD_MODELS["secondary"],
1164
- "type": "Food Classification Pipeline",
1165
- "weight": 1.2,
1166
- "purpose": "Backup food recognition"
1167
- },
1168
- "clip": {
1169
- "name": CLIP_MODEL_NAME,
1170
- "type": "Vision-Language Model",
1171
- "weight": 1.0,
1172
- "purpose": "Non-food detection & fallback"
1173
- }
1174
- },
1175
-
1176
- "ultra_features": {
1177
- "ensemble_classification": {
1178
- "description": "Combines 3+ specialized models using weighted voting",
1179
- "method": "Confidence-weighted ensemble with agreement thresholds",
1180
- "accuracy_boost": "15-25% over single model"
1181
- },
1182
- "non_food_detection": {
1183
- "description": "Automatically detects and rejects non-food objects",
1184
- "method": "CLIP-based semantic understanding",
1185
- "false_positive_reduction": "95%+"
1186
- },
1187
- "advanced_preprocessing": {
1188
- "description": "Generates 5 optimized image variants for analysis",
1189
- "variants": ["Original", "Enhanced contrast", "Brightened", "Sharpened", "Center cropped"],
1190
- "accuracy_improvement": "10-15%"
1191
- },
1192
- "confidence_filtering": {
1193
- "description": "Rejects low-confidence predictions to ensure quality",
1194
- "min_threshold": MIN_CONFIDENCE_THRESHOLD,
1195
- "high_threshold": HIGH_CONFIDENCE_THRESHOLD,
1196
- "reliability": "99%+"
1197
- },
1198
- "optimized_labels": {
1199
- "description": "Food-101 labels enhanced with synonyms and variants",
1200
- "total_labels": len(get_optimized_food101_labels()),
1201
- "synonym_mapping": "2-3 synonyms per label",
1202
- "coverage": "Comprehensive food categories"
1203
- }
1204
- },
1205
-
1206
- "performance_metrics": {
1207
- "accuracy": "99%+ on clear food images",
1208
- "precision": "98%+ (very few false positives)",
1209
- "recall": "97%+ (catches most food items)",
1210
- "f1_score": "98%+",
1211
- "non_food_rejection": "95%+ accuracy",
1212
- "inference_time": "< 2 seconds per image"
1213
- },
1214
-
1215
- "use_cases": [
1216
- "🍽️ Professional nutrition tracking applications",
1217
- "📱 Consumer calorie counting apps",
1218
- "🏥 Medical dietary monitoring systems",
1219
- "🍕 Restaurant menu digitalization",
1220
- "🛒 Grocery shopping assistants",
1221
- "👨‍🍳 Recipe analysis and ingredient detection",
1222
- "📊 Food industry quality control",
1223
- "🎓 Educational food recognition tools",
1224
- "🔬 Research applications in food science",
1225
- "🌍 Agricultural product classification"
1226
- ],
1227
-
1228
- "technical_advantages": [
1229
- "🎯 Highest accuracy in food recognition",
1230
- "🚫 Eliminates false positives with non-food detection",
1231
- "⚡ Production-optimized for real-world usage",
1232
- "🔒 Complete privacy with self-hosting",
1233
- "💰 Zero ongoing costs (no API fees)",
1234
- "🌍 Works offline for vision tasks",
1235
- "🔄 Continuous improvement through ensemble learning",
1236
- "📊 Real nutritional data integration",
1237
- "🛡️ Robust error handling and fallbacks",
1238
- "⚙️ Highly configurable and extensible"
1239
- ]
1240
  }
1241
 
1242
- # --- Run ULTRA API ---
 
1243
  if __name__ == "__main__":
1244
- print("=" * 100)
1245
- print("🏆 ULTRA-OPTIMIZED FOOD SCANNER API v10.0 - 99% ACCURACY EDITION")
1246
- print("=" * 100)
1247
- print("🎯 ULTRA Features:")
1248
- print(" ✅ Ensemble od 3+ specijalizovana modela")
1249
- print(" ✅ 99%+ preciznost u prepoznavanju hrane")
1250
- print(" ✅ Automatska non-food detekcija")
1251
- print(" ✅ Napredni image preprocessing (5 varijanti)")
1252
- print(" ✅ Confidence filtering za maksimalnu pouzdanost")
1253
- print(" Intelligent voting algoritam")
1254
- print(" Optimizovane Food-101 labele sa sinonimima")
1255
- print(" ✅ Realni nutritivni podaci iz Open Food Facts")
1256
- print("=" * 100)
1257
- print(f"🤖 Primary Model: {FOOD_MODELS['primary']}")
1258
- print(f"🤖 Secondary Model: {FOOD_MODELS['secondary']}")
1259
- print(f"🤖 CLIP Model: {CLIP_MODEL_NAME}")
1260
  print(f"💻 Device: {device.upper()}")
1261
- print(f"🎯 Accuracy: 99%+ (Guaranteed)")
1262
- print(f" Status: ULTRA-Ready for Production")
1263
- print("=" * 100)
1264
 
1265
  run_port = int(os.environ.get("PORT", "8000"))
1266
- print(f"🌍 ULTRA API Server: http://0.0.0.0:{run_port}")
1267
- print(f"📚 ULTRA Docs: http://0.0.0.0:{run_port}/docs")
1268
- print("🏆 ULTRA Food Scanner - Nikad više pogrešnih rezultata!")
1269
- print("=" * 100)
1270
 
1271
  uvicorn.run(app, host="0.0.0.0", port=run_port)
 
1
  #!/usr/bin/env python3
2
  """
3
+ 🎯 Zero-Shot Food Recognition API - CLIP Edition
4
+ ================================================
5
 
6
+ Jednostavan i moćan food recognition sistem baziran na CLIP modelu.
7
 
8
+ Ključne mogućnosti:
9
+ - 🌍 Zero-shot prepoznavanje - prepoznaje bilo šta bez dodatnog treninga
10
+ - 🎯 Veliki spektar objekata - ne samo hrana, već sve
11
+ - 🚀 Jednostavan i čist kod
12
+ - 📊 Visoka preciznost sa CLIP-om
13
+ - 🏷️ Customizabilne labele
14
+ - Brza inferenca
 
15
 
16
  Autor: AI Assistant
17
+ Verzija: 11.0.0 - ZERO-SHOT CLIP EDITION
18
  """
19
 
20
  import os
 
 
 
 
 
 
 
21
  import logging
22
+ from io import BytesIO
23
+ from typing import Optional, Dict, Any, List
24
 
25
  import uvicorn
26
+ from fastapi import FastAPI, File, UploadFile, HTTPException
27
  from fastapi.responses import JSONResponse
28
  from fastapi.middleware.cors import CORSMiddleware
29
 
30
  # Image processing
31
+ from PIL import Image
 
 
 
 
32
  import torch
33
+ from transformers import CLIPProcessor, CLIPModel
34
+
35
+ # Nutrition lookup
36
+ import requests
 
 
 
 
 
37
 
38
  # Setup logging
39
  logging.basicConfig(level=logging.INFO)
40
  logger = logging.getLogger(__name__)
41
 
42
+ # --- CONFIGURATION ---
43
+ # CLIP model - najbolji za zero-shot classification
 
 
 
 
 
 
 
44
  CLIP_MODEL_NAME = "openai/clip-vit-large-patch14"
45
+ MIN_CONFIDENCE = 0.15
46
+
47
+ # Food-101 categories za food recognition
48
+ FOOD_CATEGORIES = [
49
+ "apple pie", "baby back ribs", "baklava", "beef carpaccio", "beef tartare",
50
+ "beet salad", "beignets", "bibimbap", "bread pudding", "breakfast burrito",
51
+ "bruschetta", "caesar salad", "cannoli", "caprese salad", "carrot cake",
52
+ "ceviche", "cheesecake", "cheese plate", "chicken curry", "chicken quesadilla",
53
+ "chicken wings", "chocolate cake", "chocolate mousse", "churros", "clam chowder",
54
+ "club sandwich", "crab cakes", "creme brulee", "croque madame", "cup cakes",
55
+ "deviled eggs", "donuts", "dumplings", "edamame", "eggs benedict",
56
+ "escargots", "falafel", "filet mignon", "fish and chips", "foie gras",
57
+ "french fries", "french onion soup", "french toast", "fried calamari", "fried rice",
58
+ "frozen yogurt", "garlic bread", "gnocchi", "greek salad", "grilled cheese sandwich",
59
+ "grilled salmon", "guacamole", "gyoza", "hamburger", "hot and sour soup",
60
+ "hot dog", "huevos rancheros", "hummus", "ice cream", "lasagna",
61
+ "lobster bisque", "lobster roll sandwich", "macaroni and cheese", "macarons", "miso soup",
62
+ "mussels", "nachos", "omelette", "onion rings", "oysters",
63
+ "pad thai", "paella", "pancakes", "panna cotta", "peking duck",
64
+ "pho", "pizza", "pork chop", "poutine", "prime rib",
65
+ "pulled pork sandwich", "ramen", "ravioli", "red velvet cake", "risotto",
66
+ "samosa", "sashimi", "scallops", "seaweed salad", "shrimp and grits",
67
+ "spaghetti bolognese", "spaghetti carbonara", "spring rolls", "steak", "strawberry shortcake",
68
+ "sushi", "tacos", "takoyaki", "tiramisu", "tuna tartare", "waffles"
69
  ]
70
 
71
+
72
  def select_device() -> str:
73
+ """Odabire najbolji dostupni uređaj."""
74
  if torch.cuda.is_available():
75
  return "cuda"
76
+ if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
77
+ return "mps"
 
 
 
78
  return "cpu"
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
+ class ZeroShotFoodClassifier:
 
 
 
82
  """
83
+ Zero-shot food classifier baziran na CLIP modelu.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
+ CLIP (Contrastive Language-Image Pre-training) je model koji može
86
+ prepoznati bilo koji objekat bez dodatnog treninga - jednostavno mu
87
+ kažeš šta da traži i on to prepoznaje.
88
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
+ def __init__(self, device: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  self.device = device
92
+ logger.info(f"🚀 Loading CLIP model: {CLIP_MODEL_NAME}")
 
 
 
 
 
 
 
 
 
93
 
94
+ # Load CLIP model i processor
95
+ self.processor = CLIPProcessor.from_pretrained(CLIP_MODEL_NAME)
96
+ self.model = CLIPModel.from_pretrained(CLIP_MODEL_NAME).to(device)
97
+ self.model.eval()
98
 
99
+ logger.info("✅ CLIP model loaded successfully!")
100
+
101
+ def classify_food(self, image: Image.Image, custom_categories: List[str] = None) -> Dict[str, Any]:
102
+ """
103
+ Klasifikuje hranu na slici koristeći zero-shot CLIP pristup.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
+ Args:
106
+ image: PIL slika za analizu
107
+ custom_categories: Opcione custom kategorije (ako nisu date, koristi Food-101)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
+ Returns:
110
+ Dictionary sa rezultatima klasifikacije
 
 
111
  """
112
+ # Koristi custom kategorije ili default food categories
113
+ categories = custom_categories if custom_categories else FOOD_CATEGORIES
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
+ # Generiši text prompts za svaku kategoriju
116
+ text_prompts = [f"a photo of {category}" for category in categories]
 
 
 
 
 
 
117
 
118
+ logger.info(f"🔍 Analyzing image with {len(categories)} categories...")
119
 
120
+ # Process inputs
121
+ with torch.no_grad():
122
+ inputs = self.processor(
123
+ text=text_prompts,
124
+ images=image,
125
+ return_tensors="pt",
126
+ padding=True
127
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
+ # Move to device
 
130
  inputs = {k: v.to(self.device) for k, v in inputs.items()}
131
 
132
+ # Get predictions
133
+ outputs = self.model(**inputs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
+ # Calculate similarity scores
136
+ logits_per_image = outputs.logits_per_image
137
+ probs = logits_per_image.softmax(dim=1).cpu().numpy()[0]
138
+
139
+ # Sort by probability
140
+ sorted_indices = probs.argsort()[::-1]
141
+
142
+ # Get top 5 results
143
+ top5_results = []
144
+ for idx in sorted_indices[:5]:
145
+ category = categories[idx]
146
+ confidence = float(probs[idx])
147
+ top5_results.append({
148
+ "label": category,
149
+ "confidence": confidence
150
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
+ # Best result
153
+ best_label = categories[sorted_indices[0]]
154
+ best_confidence = float(probs[sorted_indices[0]])
 
 
 
 
 
 
 
155
 
156
+ logger.info(f"✅ Best match: {best_label} ({best_confidence:.2%})")
 
 
157
 
158
+ return {
159
+ "primary_label": best_label,
160
+ "confidence": best_confidence,
161
+ "top5": top5_results,
162
+ "alternatives": [r["label"] for r in top5_results[1:4]]
163
+ }
164
 
165
+ def detect_if_food(self, image: Image.Image) -> tuple[bool, float]:
 
 
166
  """
167
+ Detektuje da li slika sadrži hranu.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
+ Returns:
170
+ (is_food, confidence) tuple
171
+ """
172
+ categories = ["food", "non-food object"]
173
+ text_prompts = [f"a photo of {cat}" for cat in categories]
174
 
175
+ with torch.no_grad():
176
+ inputs = self.processor(
177
+ text=text_prompts,
178
+ images=image,
179
+ return_tensors="pt",
180
+ padding=True
181
+ )
182
+ inputs = {k: v.to(self.device) for k, v in inputs.items()}
183
+ outputs = self.model(**inputs)
184
+ probs = outputs.logits_per_image.softmax(dim=1).cpu().numpy()[0]
185
 
186
+ is_food = probs[0] > probs[1]
187
+ confidence = float(probs[0] if is_food else probs[1])
 
 
 
 
 
188
 
189
+ return is_food, confidence
 
 
 
 
 
 
 
190
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
+ def search_nutrition_data(food_name: str) -> Optional[Dict[str, Any]]:
193
  """Pretražuje nutritivne podatke preko Open Food Facts API-ja."""
194
+ try:
195
+ logger.info(f"🔍 Searching nutrition data for: '{food_name}'")
196
+
197
+ search_url = "https://world.openfoodfacts.org/cgi/search.pl"
198
+ params = {
199
+ "search_terms": food_name,
200
+ "search_simple": 1,
201
+ "action": "process",
202
+ "json": 1,
203
+ "page_size": 5
204
+ }
205
+
206
+ response = requests.get(search_url, params=params, timeout=5)
207
+
208
+ if response.status_code == 200:
209
+ data = response.json()
 
 
 
210
 
211
+ if data.get('products') and len(data['products']) > 0:
212
+ for product in data['products']:
213
+ nutriments = product.get('nutriments', {})
214
+
215
+ if all(key in nutriments for key in ['energy-kcal_100g', 'proteins_100g', 'carbohydrates_100g', 'fat_100g']):
216
+ logger.info(f"✅ Found nutrition data")
217
 
218
+ return {
219
+ "name": product.get('product_name', food_name),
220
+ "brand": product.get('brands', 'Unknown'),
221
+ "nutrition": {
222
+ "calories": nutriments.get('energy-kcal_100g', 0),
223
+ "protein": nutriments.get('proteins_100g', 0),
224
+ "carbs": nutriments.get('carbohydrates_100g', 0),
225
+ "fat": nutriments.get('fat_100g', 0),
226
+ "fiber": nutriments.get('fiber_100g'),
227
+ "sugar": nutriments.get('sugars_100g'),
228
+ "sodium": nutriments.get('sodium_100g', 0) * 1000 if nutriments.get('sodium_100g') else None
229
+ },
230
+ "source": "Open Food Facts",
231
+ "serving_size": 100,
232
+ "serving_unit": "g"
233
+ }
234
+
235
+ except Exception as e:
236
+ logger.warning(f"⚠️ Nutrition search error: {e}")
 
 
 
 
237
 
 
238
  return get_estimated_nutrition(food_name)
239
 
240
+
241
  def get_estimated_nutrition(food_name: str) -> Dict[str, Any]:
242
+ """Vraća procijenjene nutritivne vrijednosti."""
243
  food_lower = food_name.lower()
244
 
245
  categories = {
 
251
  'dairy': {'calories': 60, 'protein': 3.5, 'carbs': 5, 'fat': 3, 'fiber': 0, 'sugar': 5, 'sodium': 50},
252
  'dessert': {'calories': 350, 'protein': 4, 'carbs': 50, 'fat': 15, 'fiber': 1, 'sugar': 40, 'sodium': 200},
253
  'fast_food': {'calories': 250, 'protein': 12, 'carbs': 30, 'fat': 10, 'fiber': 2, 'sugar': 5, 'sodium': 600},
 
254
  }
255
 
256
  category_keywords = {
257
+ 'fruit': ['apple', 'banana', 'orange', 'berry', 'fruit'],
258
+ 'vegetable': ['salad', 'vegetable', 'tomato'],
259
+ 'meat': ['chicken', 'beef', 'pork', 'steak', 'meat'],
260
+ 'fish': ['fish', 'salmon', 'tuna', 'seafood'],
261
+ 'grain': ['rice', 'pasta', 'noodle', 'bread'],
262
+ 'dairy': ['cheese', 'yogurt', 'milk'],
263
+ 'dessert': ['cake', 'cookie', 'chocolate', 'ice cream'],
264
+ 'fast_food': ['burger', 'pizza', 'fries'],
 
265
  }
266
 
267
  detected_category = 'grain'
 
279
  "source": "AI Estimation",
280
  "serving_size": 100,
281
  "serving_unit": "g",
282
+ "note": "Estimated values based on food category"
283
  }
284
 
285
+
286
  def is_image_file(file: UploadFile):
287
+ """Provjerava da li je fajl slika."""
288
  return file.content_type in ["image/jpeg", "image/png", "image/jpg", "image/webp"]
289
 
290
+
291
+ # --- Initialize Classifier ---
292
+ logger.info("🚀 Initializing Zero-Shot Food Recognition API...")
293
  device = select_device()
294
+ logger.info(f"Using device: {device}")
 
295
 
296
+ classifier = ZeroShotFoodClassifier(device)
 
297
 
298
  # --- FastAPI Application ---
299
  app = FastAPI(
300
+ title="🎯 Zero-Shot Food Recognition API - CLIP Edition",
301
  description="""
302
+ **Jednostavan i moćan food recognition sistem sa CLIP modelom**
303
+
304
+ ### 🌟 Ključne mogućnosti:
305
+ - 🌍 **Zero-shot Learning** - Prepoznaje bilo šta bez dodatnog treninga
306
+ - 🎯 **Veliki spektar** - Ne samo hrana, već bilo koji objekat
307
+ - 🚀 **Jednostavan** - Clean i razumljiv kod
308
+ - 📊 **Pouzdan** - CLIP model sa state-of-the-art performansama
309
+ - 🏷️ **Fleksibilan** - Customizabilne kategorije
310
+ - **Brz** - Optimizovana inferenca
311
+
312
+ ### 📖 Kako CLIP radi:
313
+ CLIP je vision-language model koji razume vezu između slika i teksta.
314
+ Može prepoznati bilo koji objekat - samo mu kažeš šta da traži!
315
+
316
+ ### 🎯 Primjena:
317
+ - Food recognition i nutrition tracking
318
+ - Općenita object detection
319
+ - Visual search
320
+ - Image classification za bilo koju domenu
 
 
 
 
 
 
 
 
 
 
321
  """,
322
+ version="11.0.0"
323
  )
324
 
325
+ # CORS
326
  app.add_middleware(
327
  CORSMiddleware,
328
  allow_origins=["*"],
 
331
  allow_headers=["*"],
332
  )
333
 
334
+
335
  @app.post("/analyze",
336
+ summary="🎯 Analyze Food Image",
337
+ description="Upload sliku za zero-shot food recognition"
 
338
  )
339
  async def analyze(file: UploadFile = File(...)):
340
  """
341
+ Analizira sliku i prepoznaje hranu koristeći CLIP zero-shot pristup.
342
 
343
+ Model automatski prepoznaje hranu iz Food-101 kategorija bez potrebe
344
+ za dodatnim treningom.
 
 
 
 
 
 
 
345
  """
346
  if not file:
347
+ raise HTTPException(status_code=400, detail="No image provided")
348
 
349
  if not is_image_file(file):
350
+ raise HTTPException(status_code=400, detail="Unsupported image format. Use JPEG, PNG or WebP.")
 
 
 
351
 
352
  try:
353
+ # Load image
354
  contents = await file.read()
355
  image = Image.open(BytesIO(contents))
356
 
 
357
  if image.mode != "RGB":
358
  image = image.convert("RGB")
359
 
 
360
  image_width, image_height = image.size
361
+
362
  except Exception as e:
363
+ raise HTTPException(status_code=500, detail=f"Error reading image: {e}")
364
 
365
  try:
366
+ # Check if it's food
367
+ is_food, food_confidence = classifier.detect_if_food(image)
 
368
 
369
+ if not is_food and food_confidence > 0.6:
 
370
  return JSONResponse(content={
371
  "success": False,
372
  "error": "Non-food object detected",
373
+ "message": "Image doesn't contain food. Please upload a food image.",
374
+ "confidence": food_confidence
 
 
 
 
 
375
  })
376
 
377
+ # Classify food
378
+ logger.info("🔍 Classifying food...")
379
+ result = classifier.classify_food(image)
380
+
381
+ if result["confidence"] < MIN_CONFIDENCE:
382
  raise HTTPException(
383
  status_code=422,
384
+ detail=f"Low confidence ({result['confidence']:.2%}). Please upload a clearer image."
385
  )
386
+
387
  except HTTPException:
388
  raise
389
  except Exception as e:
390
+ logger.error(f"Classification error: {e}")
391
+ raise HTTPException(status_code=500, detail=f"Classification error: {e}")
392
 
393
  # Get nutrition data
394
+ logger.info(f"🍎 Recognized food: {result['primary_label']}")
395
+ nutrition_data = search_nutrition_data(result["primary_label"])
 
 
 
396
 
397
+ # Prepare response
398
+ response = {
399
  "success": True,
400
+ "label": result["primary_label"],
401
+ "confidence": result["confidence"],
402
+ "alternatives": result["alternatives"],
403
 
404
+ # Nutrition
405
  "nutrition": nutrition_data["nutrition"],
406
  "source": nutrition_data["source"],
407
 
408
+ # Image info
 
 
 
 
 
 
 
 
 
 
409
  "image_info": {
410
  "width": image_width,
411
  "height": image_height,
412
  "format": image.format
413
  },
414
 
415
+ # Model info
416
  "model_info": {
417
+ "type": "Zero-Shot CLIP Classifier",
418
+ "model": CLIP_MODEL_NAME,
419
+ "version": "11.0.0",
420
+ "method": "Zero-shot learning",
421
+ "categories": len(FOOD_CATEGORIES),
422
+ "device": device
 
 
 
 
 
 
 
423
  }
424
  }
425
 
426
+ return JSONResponse(content=response)
427
+
428
 
429
+ @app.post("/analyze-custom",
430
+ summary="🎯 Analyze with Custom Categories",
431
+ description="Upload sliku i definiši custom kategorije za prepoznavanje"
432
  )
433
+ async def analyze_custom(
434
+ file: UploadFile = File(...),
435
+ categories: str = None
436
+ ):
437
+ """
438
+ Zero-shot analiza sa custom kategorijama.
439
+
440
+ Primjer: categories="pizza,burger,pasta,salad"
441
+
442
+ Ovo demonstrira moć CLIP-a - može prepoznati bilo šta što mu kažeš!
443
+ """
444
+ if not file:
445
+ raise HTTPException(status_code=400, detail="No image provided")
446
+
447
+ if not is_image_file(file):
448
+ raise HTTPException(status_code=400, detail="Unsupported image format")
449
+
450
+ # Parse categories
451
+ custom_categories = None
452
+ if categories:
453
+ custom_categories = [cat.strip() for cat in categories.split(",")]
454
+ logger.info(f"Using custom categories: {custom_categories}")
455
+
456
  try:
457
+ contents = await file.read()
458
+ image = Image.open(BytesIO(contents))
459
 
460
+ if image.mode != "RGB":
461
+ image = image.convert("RGB")
462
 
463
+ except Exception as e:
464
+ raise HTTPException(status_code=500, detail=f"Error reading image: {e}")
465
+
466
+ try:
467
+ result = classifier.classify_food(image, custom_categories=custom_categories)
468
 
469
  return JSONResponse(content={
470
  "success": True,
471
+ "label": result["primary_label"],
472
+ "confidence": result["confidence"],
473
+ "top5": result["top5"],
474
+ "categories_used": custom_categories if custom_categories else "Food-101 default",
475
+ "model_info": {
476
+ "type": "Zero-Shot CLIP Classifier",
477
+ "model": CLIP_MODEL_NAME,
478
+ "method": "Custom zero-shot classification"
479
+ }
480
  })
481
 
 
 
482
  except Exception as e:
483
+ logger.error(f"Classification error: {e}")
484
+ raise HTTPException(status_code=500, detail=f"Classification error: {e}")
485
+
 
 
486
 
487
+ @app.get("/",
488
+ summary="🎯 API Info",
489
+ description="Informacije o Zero-Shot Food Recognition API-ju"
490
  )
491
  def root():
492
+ """Root endpoint sa API informacijama."""
493
  return {
494
+ "message": "🎯 Zero-Shot Food Recognition API - CLIP Edition",
495
+ "status": "🟢 Online & Ready",
496
+ "tagline": "Jednostavan i moćan food recognition sa zero-shot learning",
497
  "model": {
498
+ "name": CLIP_MODEL_NAME,
499
+ "type": "Vision-Language Model (CLIP)",
500
+ "capabilities": "Zero-shot classification",
 
 
501
  "device": device.upper(),
502
+ "food_categories": len(FOOD_CATEGORIES)
 
 
 
 
 
 
 
 
 
 
503
  },
504
+ "features": {
505
+ "zero_shot": " Prepoznaje bilo šta bez dodatnog treninga",
506
+ "customizable": " Customizabilne kategorije",
507
+ "fast": " Brza inferenca",
508
+ "simple": " Jednostavan i čist kod",
509
+ "nutrition": "✅ Automatski nutrition lookup",
510
+ "open_source": "✅ 100% open-source"
511
  },
512
  "endpoints": {
513
+ "POST /analyze": "🎯 Standard food analysis (Food-101 categories)",
514
+ "POST /analyze-custom": "🎨 Custom category analysis",
515
+ "GET /health": "💚 Health check",
516
+ "GET /categories": "📋 List all food categories"
517
  },
518
+ "about_clip": {
519
+ "what_is_clip": "CLIP (Contrastive Language-Image Pre-training) je model koji razume vezu između slika i teksta",
520
+ "zero_shot": "Može prepoznati bilo šta - samo mu kažeš šta da traži!",
521
+ "trained_on": "400+ miliona image-text parova sa interneta",
522
+ "advantages": [
523
+ "Prepoznaje širok spektar objekata",
524
+ "Nema potrebe za dodatnim treningom",
525
+ "Fleksibilan - radi sa bilo kojim kategorijama",
526
+ "State-of-the-art performanse"
527
+ ]
528
+ }
529
  }
530
 
531
+
532
+ @app.get("/health",
533
+ summary="💚 Health Check",
534
+ description="Provjeri status sistema"
535
  )
536
  def health_check():
537
+ """Health check endpoint."""
 
 
 
 
 
 
 
 
 
 
 
 
538
  try:
539
+ model_loaded = classifier.model is not None
540
+
541
+ # Test nutrition API
542
+ nutrition_api_status = "unknown"
543
+ try:
544
+ test_response = requests.get(
545
+ "https://world.openfoodfacts.org/api/v0/product/737628064502.json",
546
+ timeout=3
547
+ )
548
+ nutrition_api_status = "healthy" if test_response.status_code == 200 else "degraded"
549
+ except:
550
+ nutrition_api_status = "offline"
551
+
552
+ return {
553
+ "status": "healthy" if model_loaded else "unhealthy",
554
+ "version": "11.0.0 - ZERO-SHOT CLIP EDITION",
555
+ "model": {
 
 
 
 
 
556
  "name": CLIP_MODEL_NAME,
557
+ "loaded": model_loaded,
558
+ "device": device,
559
+ "type": "Zero-shot CLIP"
560
+ },
561
+ "nutrition_api": nutrition_api_status,
562
+ "capabilities": {
563
+ "food_recognition": model_loaded,
564
+ "zero_shot_classification": model_loaded,
565
+ "custom_categories": model_loaded,
566
+ "nutrition_lookup": nutrition_api_status in ["healthy", "degraded"]
567
  }
 
 
 
 
 
 
 
 
 
568
  }
569
+ except Exception as e:
570
+ return {
571
+ "status": "error",
572
+ "error": str(e)
573
+ }
574
+
575
 
576
+ @app.get("/categories",
577
+ summary="📋 List Food Categories",
578
+ description="Lista svih dostupnih food kategorija"
579
  )
580
+ def get_categories():
581
+ """Vraća listu svih Food-101 kategorija."""
582
  return {
583
+ "total": len(FOOD_CATEGORIES),
584
+ "categories": sorted(FOOD_CATEGORIES),
585
+ "note": "You can also use custom categories with /analyze-custom endpoint"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
586
  }
587
 
588
+
589
+ # --- Run API ---
590
  if __name__ == "__main__":
591
+ print("=" * 80)
592
+ print("🎯 ZERO-SHOT FOOD RECOGNITION API - CLIP EDITION")
593
+ print("=" * 80)
594
+ print("🌟 Features:")
595
+ print(" ✅ Zero-shot learning - prepoznaje bilo šta!")
596
+ print(" ✅ CLIP model - state-of-the-art performanse")
597
+ print(" ✅ Jednostavan kod - lako razumljiv i održiv")
598
+ print(" ✅ Customizabilne kategorije")
599
+ print(" ✅ Automatski nutrition lookup")
600
+ print("=" * 80)
601
+ print(f"🤖 Model: {CLIP_MODEL_NAME}")
 
 
 
 
 
602
  print(f"💻 Device: {device.upper()}")
603
+ print(f"🏷️ Categories: {len(FOOD_CATEGORIES)} (Food-101)")
604
+ print("=" * 80)
 
605
 
606
  run_port = int(os.environ.get("PORT", "8000"))
607
+ print(f"🌍 Server: http://0.0.0.0:{run_port}")
608
+ print(f"📚 Docs: http://0.0.0.0:{run_port}/docs")
609
+ print("=" * 80)
 
610
 
611
  uvicorn.run(app, host="0.0.0.0", port=run_port)
requirements.txt CHANGED
@@ -1,5 +1,5 @@
1
- # ULTRA-OPTIMIZED Food Scanner API - Multi-Model Ensemble Edition
2
- # Specijalizovani requirements za 99% preciznost food recognition
3
 
4
  # Core API Framework
5
  fastapi==0.115.0
@@ -8,31 +8,16 @@ python-multipart==0.0.12
8
 
9
  # Image Processing
10
  pillow==11.0.0
11
- opencv-python-headless==4.10.0.84
12
 
13
- # Deep Learning / Transformers
14
- # NOTE: Due to CVE-2025-32434, torch must be >=2.6 to allow torch.load() via transformers
15
  torch>=2.6.0
16
  torchvision>=0.19.0
17
- safetensors>=0.4.3
18
 
19
- # Transformers (Multiple specialized models)
20
  transformers>=4.44.2
21
- timm>=1.0.9
22
 
23
- # Computer Vision utilities
24
- albumentations>=1.4.15
25
-
26
- # HTTP util
27
  requests>=2.32.0
28
 
29
- # Scientific computing
30
- numpy>=1.24.0
31
- scipy>=1.11.0
32
-
33
- # Additional ML utilities
34
- scikit-learn>=1.3.0
35
-
36
- # Napomena: ULTRA varijanta koristi ensemble pristup sa specijalizovanim modelima
37
- # za maksimalnu preciznost u food recognition
38
-
 
1
+ # Zero-Shot Food Recognition API - CLIP Edition
2
+ # Minimalni requirements za jednostavan i moćan food recognition
3
 
4
  # Core API Framework
5
  fastapi==0.115.0
 
8
 
9
  # Image Processing
10
  pillow==11.0.0
 
11
 
12
+ # Deep Learning - PyTorch sa CVE fix
 
13
  torch>=2.6.0
14
  torchvision>=0.19.0
 
15
 
16
+ # Transformers za CLIP model
17
  transformers>=4.44.2
 
18
 
19
+ # HTTP za nutrition API
 
 
 
20
  requests>=2.32.0
21
 
22
+ # Napomena: Ovaj setup koristi samo CLIP model za zero-shot classification
23
+ # što je jednostavnije i dovoljno moćno za većinu use-case-ova