har1zarD commited on
Commit
4a532ec
·
0 Parent(s):
Files changed (8) hide show
  1. .dockerignore +18 -0
  2. .env.example +8 -0
  3. .gitignore +41 -0
  4. DEPLOYMENT.md +451 -0
  5. Dockerfile +30 -0
  6. README.md +310 -0
  7. app.py +643 -0
  8. requirements.txt +20 -0
.dockerignore ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .Python
6
+ *.so
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ .env
12
+ .venv
13
+ venv/
14
+ ENV/
15
+ start_server.py
16
+ *.md
17
+ .git/
18
+ .gitignore
.env.example ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
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
.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ *.whl
12
+
13
+ # Environment
14
+ .env
15
+ .venv
16
+ venv/
17
+ ENV/
18
+ env/
19
+
20
+ # IDE
21
+ .vscode/
22
+ .idea/
23
+ *.swp
24
+ *.swo
25
+ *~
26
+
27
+ # OS
28
+ .DS_Store
29
+ Thumbs.db
30
+
31
+ # Logs
32
+ *.log
33
+
34
+ # Testing
35
+ .pytest_cache/
36
+ .coverage
37
+ htmlcov/
38
+
39
+ # Model cache (Hugging Face)
40
+ models/
41
+ .cache/
DEPLOYMENT.md ADDED
@@ -0,0 +1,451 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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!** 🚀
Dockerfile ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.11 slim image
2
+ FROM python:3.11-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y \
9
+ gcc \
10
+ g++ \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # Copy requirements first (for better caching)
14
+ COPY requirements.txt .
15
+
16
+ # Install Python dependencies
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy application code
20
+ COPY app.py .
21
+
22
+ # Expose port
23
+ EXPOSE 8000
24
+
25
+ # Set environment variables
26
+ ENV PYTHONUNBUFFERED=1
27
+ ENV PORT=8000
28
+
29
+ # Run the application
30
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
README.md ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🍕 Food Recognition Backend API
2
+
3
+ Fast and accurate food recognition API powered by Hugging Face Transformers and Open Food Facts nutrition database.
4
+
5
+ ## ✨ Features
6
+
7
+ - 🎯 **Accurate Food Recognition** - Uses specialized `nateraw/food` model
8
+ - 📊 **Nutrition Data** - Integrates with Open Food Facts database
9
+ - ⚡ **Fast Processing** - Optimized for quick inference
10
+ - 🔄 **Multiple Formats** - Upload files or provide image URLs
11
+ - 🌐 **CORS Enabled** - Ready for web integration
12
+ - 📝 **Auto Documentation** - Built-in FastAPI docs at `/docs`
13
+
14
+ ## 🚀 Quick Start
15
+
16
+ ### Local Development
17
+
18
+ 1. **Clone the repository**
19
+ ```bash
20
+ git clone https://github.com/YOUR_USERNAME/food_recognition_backend.git
21
+ cd food_recognition_backend
22
+ ```
23
+
24
+ 2. **Install dependencies**
25
+ ```bash
26
+ pip install -r requirements.txt
27
+ ```
28
+
29
+ 3. **Set up environment**
30
+ ```bash
31
+ cp .env.example .env
32
+ # Edit .env with your API keys (optional)
33
+ ```
34
+
35
+ 4. **Run the server**
36
+ ```bash
37
+ python app.py
38
+ ```
39
+
40
+ Server will start at `http://localhost:8000`
41
+
42
+ 5. **Test the API**
43
+ ```bash
44
+ # Health check
45
+ curl http://localhost:8000/health
46
+
47
+ # Analyze food image
48
+ curl -X POST http://localhost:8000/analyze \
49
+ -F "file=@path/to/food.jpg"
50
+ ```
51
+
52
+ ### Docker
53
+
54
+ ```bash
55
+ # Build image
56
+ docker build -t food-recognition-api .
57
+
58
+ # Run container
59
+ docker run -p 8000:8000 \
60
+ -e PORT=8000 \
61
+ food-recognition-api
62
+ ```
63
+
64
+ ## 📚 API Documentation
65
+
66
+ ### Endpoints
67
+
68
+ #### `GET /` - Health Check
69
+ ```bash
70
+ curl http://localhost:8000/
71
+ ```
72
+
73
+ Response:
74
+ ```json
75
+ {
76
+ "status": "healthy",
77
+ "message": "Fast Food Recognition Backend",
78
+ "version": "3.0.0",
79
+ "device": "cpu"
80
+ }
81
+ ```
82
+
83
+ #### `GET /health` - Detailed Health
84
+ ```bash
85
+ curl http://localhost:8000/health
86
+ ```
87
+
88
+ Response:
89
+ ```json
90
+ {
91
+ "status": "healthy",
92
+ "model_loaded": true,
93
+ "device": "cpu",
94
+ "food_pipeline_loaded": true,
95
+ "model_type": "Professional Food Recognition Models"
96
+ }
97
+ ```
98
+
99
+ #### `POST /analyze` - Analyze Food Image (Upload)
100
+ ```bash
101
+ curl -X POST http://localhost:8000/analyze?top_alternatives=3 \
102
+ -F "file=@pizza.jpg"
103
+ ```
104
+
105
+ Response:
106
+ ```json
107
+ {
108
+ "label": "pizza",
109
+ "confidence": 0.95,
110
+ "nutrition": {
111
+ "calories": 266.0,
112
+ "protein": 11.0,
113
+ "fat": 10.0,
114
+ "carbs": 33.0,
115
+ "fiber": 2.3,
116
+ "sugar": 3.7,
117
+ "sodium": 598.0
118
+ },
119
+ "alternatives": ["flatbread", "focaccia", "calzone"],
120
+ "source": "Open Food Facts",
121
+ "off_product_id": "1234567890"
122
+ }
123
+ ```
124
+
125
+ #### `POST /analyze-url` - Analyze Food Image (URL)
126
+ ```bash
127
+ curl -X POST "http://localhost:8000/analyze-url?image_url=https://example.com/pizza.jpg&top_alternatives=3"
128
+ ```
129
+
130
+ Response: Same as `/analyze`
131
+
132
+ #### `GET /search-nutrition/{food_name}` - Search Nutrition Only
133
+ ```bash
134
+ curl http://localhost:8000/search-nutrition/pizza
135
+ ```
136
+
137
+ Response:
138
+ ```json
139
+ {
140
+ "food_name": "pizza",
141
+ "nutrition": {
142
+ "calories": 266.0,
143
+ "protein": 11.0,
144
+ "fat": 10.0,
145
+ "carbs": 33.0,
146
+ "fiber": 2.3,
147
+ "sugar": 3.7,
148
+ "sodium": 598.0
149
+ },
150
+ "source": "Open Food Facts",
151
+ "off_product_id": "1234567890"
152
+ }
153
+ ```
154
+
155
+ ### Interactive Documentation
156
+
157
+ Visit `http://localhost:8000/docs` for interactive Swagger UI documentation.
158
+
159
+ ## 🏗️ Architecture
160
+
161
+ ```
162
+ ┌─────────────┐
163
+ │ Client │
164
+ └──────┬──────┘
165
+
166
+
167
+ ┌─────────────────────────┐
168
+ │ FastAPI Server │
169
+ │ (app.py) │
170
+ └──────┬──────────────────┘
171
+
172
+ ├──────────────────┐
173
+ │ │
174
+ ▼ ▼
175
+ ┌──────────────┐ ┌──────────────┐
176
+ │ Food Model │ │ Nutrition │
177
+ │ (nateraw) │ │ Client │
178
+ └──────────────┘ └──────┬───────┘
179
+
180
+
181
+ ┌──────────────┐
182
+ │ Open Food │
183
+ │ Facts API │
184
+ └──────────────┘
185
+ ```
186
+
187
+ ## 📦 Models Used
188
+
189
+ - **Primary**: `nateraw/food` - Specialized food classifier
190
+ - **Fallback**: `Kaludi/food-category-classification-v2.0`
191
+ - **Alternative**: `google/vit-base-patch16-224`
192
+
193
+ ## 🔧 Configuration
194
+
195
+ ### Environment Variables
196
+
197
+ ```bash
198
+ # Server
199
+ PORT=8000
200
+ HOST=0.0.0.0
201
+
202
+ # Optional API Keys (defaults included)
203
+ USDA_API_KEY=your_key
204
+ NUTRITIONIX_APP_ID=your_id
205
+ NUTRITIONIX_API_KEY=your_key
206
+ ```
207
+
208
+ ### Model Selection
209
+
210
+ Edit `app.py` line 48 to switch models:
211
+ ```python
212
+ MODEL_NAME = "nateraw/food" # Current
213
+ # MODEL_NAME = "dwililiya/food101-model-classification" # Alternative
214
+ ```
215
+
216
+ ## 📊 Performance
217
+
218
+ - **First Request**: ~30-60s (model loading)
219
+ - **Subsequent Requests**: ~0.5-1s per image
220
+ - **Memory Usage**: ~1.5GB RAM
221
+ - **Model Size**: ~500MB
222
+
223
+ ## 🌐 Deployment
224
+
225
+ See [DEPLOYMENT.md](DEPLOYMENT.md) for comprehensive deployment guide including:
226
+ - 🆓 Free hosting options (Hugging Face, Render, Railway)
227
+ - 💰 Paid options (Coolify, AWS, GCP)
228
+ - ⚙️ Configuration examples
229
+ - 🔗 Next.js integration guide
230
+
231
+ ## 🛠️ Tech Stack
232
+
233
+ - **Framework**: FastAPI
234
+ - **ML Library**: PyTorch + Transformers
235
+ - **Image Processing**: Pillow
236
+ - **HTTP Client**: aiohttp (async)
237
+ - **Validation**: Pydantic v2
238
+ - **Server**: Uvicorn
239
+
240
+ ## 📝 Requirements
241
+
242
+ ```txt
243
+ Python >= 3.11
244
+ torch >= 2.0.0
245
+ transformers >= 4.30.0
246
+ fastapi >= 0.104.1
247
+ uvicorn[standard] >= 0.24.0
248
+ ```
249
+
250
+ See [requirements.txt](requirements.txt) for full dependencies.
251
+
252
+ ## 🧪 Testing
253
+
254
+ ```bash
255
+ # Test with sample image
256
+ curl -X POST http://localhost:8000/analyze \
257
+ -F "file=@test_images/pizza.jpg"
258
+
259
+ # Test with URL
260
+ curl -X POST "http://localhost:8000/analyze-url?image_url=https://example.com/burger.jpg"
261
+
262
+ # Test nutrition search
263
+ curl http://localhost:8000/search-nutrition/pasta
264
+ ```
265
+
266
+ ## 🐛 Known Issues
267
+
268
+ 1. **422 Error - No Nutrition Data**: Some foods don't have data in Open Food Facts
269
+ - Solution: Implement fallback or manual input
270
+
271
+ 2. **Cold Start**: First request is slow due to model loading
272
+ - Solution: Use paid hosting tier or implement keep-alive pings
273
+
274
+ 3. **Memory Usage**: Large model requires 1.5GB+ RAM
275
+ - Solution: Use cloud hosting with sufficient memory
276
+
277
+ ## 🔐 Security
278
+
279
+ - ✅ CORS configured for web integration
280
+ - ✅ Input validation with Pydantic
281
+ - ✅ File type validation
282
+ - ✅ Error handling and logging
283
+ - ⚠️ Add rate limiting for production use
284
+
285
+ ## 📄 License
286
+
287
+ MIT License - Free for personal and commercial use
288
+
289
+ ## 🤝 Contributing
290
+
291
+ Contributions welcome! Areas for improvement:
292
+ - Add more ML models
293
+ - Improve nutrition data accuracy
294
+ - Add caching layer
295
+ - Implement rate limiting
296
+ - Add authentication
297
+
298
+ ## 📧 Contact
299
+
300
+ For issues or questions, open a GitHub issue.
301
+
302
+ ## 🙏 Acknowledgments
303
+
304
+ - [Hugging Face](https://huggingface.co) - ML models
305
+ - [Open Food Facts](https://world.openfoodfacts.org) - Nutrition database
306
+ - [FastAPI](https://fastapi.tiangolo.com) - Web framework
307
+
308
+ ---
309
+
310
+ **Ready to deploy?** Check out [DEPLOYMENT.md](DEPLOYMENT.md) for step-by-step instructions! 🚀
app.py ADDED
@@ -0,0 +1,643 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Optimized Food Recognition Backend
4
+ Fast CLIP-based food identification + Open Food Facts nutrition
5
+ """
6
+
7
+ import asyncio
8
+ import aiohttp
9
+ import json
10
+ import logging
11
+ import os
12
+ import re
13
+ import time
14
+ from io import BytesIO
15
+ from pathlib import Path
16
+ from typing import Dict, List, Optional, Any, Tuple
17
+ from contextlib import asynccontextmanager
18
+
19
+ import torch
20
+ from PIL import Image
21
+ from transformers import CLIPProcessor, CLIPModel, AutoFeatureExtractor, AutoModelForImageClassification, pipeline
22
+
23
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Query
24
+ from fastapi.middleware.cors import CORSMiddleware
25
+ from fastapi.responses import JSONResponse
26
+ from pydantic import BaseModel, Field
27
+ import uvicorn
28
+
29
+ # Configure logging
30
+ logging.basicConfig(level=logging.INFO)
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # Configuration
34
+ class Config:
35
+ """Application configuration"""
36
+ # Server Configuration
37
+ HOST = os.getenv("HOST", "0.0.0.0")
38
+ PORT = int(os.getenv("PORT", "8000"))
39
+
40
+ # Device Configuration
41
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
42
+
43
+ # Open Food Facts API
44
+ OFF_API_BASE = "https://world.openfoodfacts.org/api/v0"
45
+ OFF_SEARCH_URL = "https://world.openfoodfacts.org/cgi/search.pl"
46
+ OFF_USER_AGENT = "FoodRecognitionApp/1.0 (contact@foodapp.com)"
47
+
48
+ # Nutrition APIs - Load from environment variables
49
+ USDA_API_KEY = os.getenv("USDA_API_KEY", "kgw5ZaUGy92zoFoCzAo1pGq688u0jYXEA17ZlzO9")
50
+ NUTRITIONIX_APP_ID = os.getenv("NUTRITIONIX_APP_ID", "4224c603")
51
+ NUTRITIONIX_API_KEY = os.getenv("NUTRITIONIX_API_KEY", "3f4717bb1433fcbf57799a36318301ab")
52
+
53
+ # Model Configuration - Switch between models by commenting/uncommenting the lines below
54
+ MODEL_NAME = "nateraw/food" # Specialized food classifier (high accuracy, currently active)
55
+ # MODEL_NAME = "dwililiya/food101-model-classification" # EfficientNet-B0, 101 specific foods, lighter than nateraw
56
+ # MODEL_NAME = "google/mobilenet_v2_1.0_224" # Google MobileNet v2 (general purpose, not food-specific)
57
+
58
+ # Best Hugging Face models for food recognition
59
+ FOOD_MODELS = {
60
+ "primary": MODEL_NAME, # Currently selected model
61
+ "secondary": "google/vit-base-patch16-224", # Vision Transformer
62
+ "fallback": "microsoft/resnet-50", # ResNet for general classification
63
+ "food_specific": "Kaludi/food-category-classification-v2.0", # Another food-specific model
64
+ }
65
+
66
+ config = Config()
67
+
68
+ # Data Models
69
+ class NutritionInfo(BaseModel):
70
+ calories: float = Field(..., description="Calories per 100g")
71
+ protein: float = Field(..., description="Protein in grams per 100g")
72
+ fat: float = Field(..., description="Fat in grams per 100g")
73
+ carbs: float = Field(..., description="Carbohydrates in grams per 100g")
74
+ fiber: Optional[float] = Field(None, description="Fiber in grams per 100g")
75
+ sugar: Optional[float] = Field(None, description="Sugar in grams per 100g")
76
+ sodium: Optional[float] = Field(None, description="Sodium in mg per 100g")
77
+
78
+ class FoodAnalysisResponse(BaseModel):
79
+ label: str = Field(..., description="Identified food name")
80
+ confidence: float = Field(..., description="Recognition confidence (0-1)")
81
+ nutrition: NutritionInfo = Field(..., description="Nutritional information")
82
+ alternatives: List[str] = Field(default=[], description="Alternative food predictions")
83
+ source: str = Field(..., description="Data source")
84
+ off_product_id: Optional[str] = Field(None, description="Open Food Facts product ID")
85
+
86
+ class ErrorResponse(BaseModel):
87
+ error: str
88
+ detail: Optional[str] = None
89
+
90
+ # Professional Food Recognition Model
91
+ class FoodRecognitionModel:
92
+ """Professional food recognition using specialized Hugging Face models"""
93
+
94
+ def __init__(self):
95
+ self.device = config.DEVICE
96
+ self.primary_model = None
97
+ self.secondary_model = None
98
+ self.food_pipeline = None
99
+ self._load_models()
100
+
101
+ def _load_models(self):
102
+ """Load specialized food recognition models"""
103
+ try:
104
+ logger.info(f"Loading specialized food recognition models on {self.device}")
105
+
106
+ # Load primary food-specific model
107
+ try:
108
+ logger.info(f"Loading primary food model: {FOOD_MODELS['primary']}")
109
+ self.food_pipeline = pipeline(
110
+ "image-classification",
111
+ model=FOOD_MODELS["primary"],
112
+ device=0 if "cuda" in str(self.device) else -1
113
+ )
114
+ logger.info("✅ Primary food model loaded successfully")
115
+ except Exception as e:
116
+ logger.warning(f"Primary model failed: {e}, trying secondary...")
117
+
118
+ # Fallback to secondary model
119
+ try:
120
+ logger.info("Loading secondary food model: Kaludi/food-category-classification-v2.0")
121
+ self.food_pipeline = pipeline(
122
+ "image-classification",
123
+ model=FOOD_MODELS["food_specific"],
124
+ device=0 if "cuda" in str(self.device) else -1
125
+ )
126
+ logger.info("✅ Secondary food model loaded successfully")
127
+ except Exception as e2:
128
+ logger.warning(f"Secondary model failed: {e2}, using Vision Transformer...")
129
+
130
+ # Final fallback to ViT
131
+ self.food_pipeline = pipeline(
132
+ "image-classification",
133
+ model=FOOD_MODELS["secondary"],
134
+ device=0 if "cuda" in str(self.device) else -1
135
+ )
136
+ logger.info("✅ Vision Transformer model loaded as fallback")
137
+
138
+ except Exception as e:
139
+ logger.error(f"Failed to load any food recognition model: {e}")
140
+ raise
141
+
142
+
143
+ def recognize_food(self, image: Image.Image) -> Tuple[str, float, List[str]]:
144
+ """
145
+ Professional food recognition using specialized models
146
+
147
+ Returns:
148
+ (food_name, confidence, alternatives)
149
+ """
150
+ try:
151
+ start_time = time.time()
152
+
153
+ # Convert image if needed
154
+ if image.mode != 'RGB':
155
+ image = image.convert('RGB')
156
+
157
+ # Use specialized food recognition pipeline
158
+ results = self.food_pipeline(image, top_k=5)
159
+
160
+ if not results:
161
+ logger.warning("No food predictions returned")
162
+ return "unknown food", 0.1, []
163
+
164
+ # Extract top prediction
165
+ top_result = results[0]
166
+ food_name = self._clean_food_label(top_result['label'])
167
+ confidence = top_result['score']
168
+
169
+ # Get alternatives
170
+ alternatives = []
171
+ for result in results[1:]:
172
+ alt_name = self._clean_food_label(result['label'])
173
+ if alt_name != food_name: # Avoid duplicates
174
+ alternatives.append(alt_name)
175
+
176
+ elapsed = time.time() - start_time
177
+ logger.info(f"🎯 Professional food recognition in {elapsed:.2f}s: {food_name} ({confidence:.3f})")
178
+
179
+ return food_name, confidence, alternatives[:4] # Return top 4 alternatives
180
+
181
+ except Exception as e:
182
+ logger.error(f"Food recognition failed: {e}")
183
+ return "unknown food", 0.1, []
184
+
185
+ def _clean_food_label(self, label: str) -> str:
186
+ """Clean food label from model output"""
187
+ # Remove common prefixes/suffixes from model labels
188
+ cleaned = label.lower().strip()
189
+
190
+ # Remove model-specific prefixes
191
+ prefixes_to_remove = ['food_', 'dish_', 'meal_']
192
+ for prefix in prefixes_to_remove:
193
+ if cleaned.startswith(prefix):
194
+ cleaned = cleaned[len(prefix):]
195
+
196
+ # Replace underscores with spaces
197
+ cleaned = cleaned.replace('_', ' ')
198
+
199
+ # Remove extra spaces
200
+ cleaned = ' '.join(cleaned.split())
201
+
202
+ return cleaned
203
+
204
+ # Optimized Open Food Facts Client
205
+ class FastNutritionClient:
206
+ """Fast nutrition data client with better error handling"""
207
+
208
+ def __init__(self):
209
+ self.session = None
210
+ self.timeout = aiohttp.ClientTimeout(total=5, connect=2) # Very fast timeout
211
+
212
+ async def __aenter__(self):
213
+ self.session = aiohttp.ClientSession(timeout=self.timeout)
214
+ return self
215
+
216
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
217
+ if self.session:
218
+ await self.session.close()
219
+
220
+ async def get_nutrition(self, food_name: str) -> Optional[Tuple[NutritionInfo, str, Optional[str]]]:
221
+ """
222
+ Nutrition data lookup from Open Food Facts only - NO FALLBACKS
223
+
224
+ Returns:
225
+ (nutrition_info, source, product_id) or None if not found in OFF
226
+ """
227
+ try:
228
+ # Try multiple search strategies for better results
229
+ search_terms = self._generate_search_terms(food_name)
230
+
231
+ for search_term in search_terms:
232
+ try:
233
+ result = await asyncio.wait_for(self._search_off(search_term), timeout=2.0)
234
+ if result:
235
+ return result
236
+ except asyncio.TimeoutError:
237
+ logger.debug(f"Timeout searching for '{search_term}'")
238
+ continue
239
+ except Exception as e:
240
+ logger.debug(f"Error searching for '{search_term}': {e}")
241
+ continue
242
+
243
+ # NO FALLBACK - return None if not found in Open Food Facts
244
+ logger.warning(f"No nutrition data found in Open Food Facts for '{food_name}'")
245
+ return None
246
+
247
+ except Exception as e:
248
+ logger.warning(f"Nutrition lookup failed for '{food_name}': {e}")
249
+ return None
250
+
251
+ def _generate_search_terms(self, food_name: str) -> List[str]:
252
+ """Generate multiple search terms for better matching"""
253
+ terms = []
254
+
255
+ # Original term
256
+ terms.append(food_name.lower().strip())
257
+
258
+ # Remove descriptive words for broader search
259
+ clean_term = food_name.lower()
260
+ remove_words = ["american", "fluffy", "stack of", "with butter", "with syrup", "breakfast"]
261
+ for word in remove_words:
262
+ clean_term = clean_term.replace(word, "").strip()
263
+
264
+ if clean_term and clean_term != terms[0]:
265
+ terms.append(clean_term)
266
+
267
+ # Extract main food word (first meaningful word)
268
+ words = clean_term.split()
269
+ if words:
270
+ main_word = words[0] if len(words[0]) > 3 else (words[1] if len(words) > 1 else words[0])
271
+ if main_word not in terms:
272
+ terms.append(main_word)
273
+
274
+ return terms[:3] # Limit to 3 attempts
275
+
276
+ async def _search_off(self, search_term: str) -> Optional[Tuple[NutritionInfo, str, Optional[str]]]:
277
+ """Search Open Food Facts with single term"""
278
+ try:
279
+ params = {
280
+ "search_terms": search_term,
281
+ "search_simple": 1,
282
+ "action": "process",
283
+ "json": 1,
284
+ "page_size": 5,
285
+ "sort_by": "popularity"
286
+ }
287
+
288
+ headers = {"User-Agent": config.OFF_USER_AGENT}
289
+
290
+ # Use asyncio.wait_for for additional timeout protection
291
+ search_task = self.session.get(config.OFF_SEARCH_URL, params=params, headers=headers)
292
+
293
+ async with await asyncio.wait_for(search_task, timeout=3.0) as response:
294
+ if response.status != 200:
295
+ return None
296
+
297
+ data = await response.json()
298
+ products = data.get("products", [])
299
+
300
+ # Find best product with nutrition data
301
+ for product in products[:3]:
302
+ product_name = product.get("product_name", "unknown")
303
+ logger.debug(f"Checking product: {product_name}")
304
+ nutrition = self._extract_nutrition(product)
305
+ if nutrition:
306
+ logger.debug(f"Extracted nutrition: {nutrition.calories} kcal")
307
+ if self._validate_nutrition(nutrition):
308
+ product_id = product.get("code")
309
+ logger.info(f"✅ Found nutrition for '{search_term}': {nutrition.calories} kcal")
310
+ return nutrition, "Open Food Facts", product_id
311
+ else:
312
+ logger.debug(f"❌ Nutrition validation failed for {product_name}")
313
+ else:
314
+ logger.debug(f"❌ Could not extract nutrition from {product_name}")
315
+
316
+ except asyncio.TimeoutError:
317
+ logger.debug(f"OFF search timed out for '{search_term}'")
318
+ except Exception as e:
319
+ logger.debug(f"OFF search failed for '{search_term}': {e}")
320
+
321
+ return None
322
+
323
+ def _safe_float(self, value) -> float:
324
+ """Safely convert value to float"""
325
+ if not value:
326
+ return 0.0
327
+ try:
328
+ if isinstance(value, str):
329
+ cleaned = value.replace(',', '.')
330
+ # Handle duplicated decimals like "0.120.12"
331
+ if cleaned.count('.') > 1:
332
+ parts = cleaned.split('.')
333
+ cleaned = f"{parts[0]}.{parts[1][:2]}" # Take first 2 decimal places
334
+ return float(cleaned)
335
+ return float(value)
336
+ except (ValueError, TypeError):
337
+ return 0.0
338
+
339
+ def _extract_nutrition(self, product: Dict) -> Optional[NutritionInfo]:
340
+ """Extract nutrition with improved validation"""
341
+ try:
342
+ nutriments = product.get("nutriments", {})
343
+
344
+ # Get calories from multiple possible fields
345
+ calories = 0
346
+ for key in ["energy-kcal_100g", "energy_100g"]:
347
+ value = nutriments.get(key)
348
+ if value:
349
+ if key == "energy_100g": # kJ to kcal
350
+ calories = self._safe_float(value) / 4.184
351
+ else:
352
+ calories = self._safe_float(value)
353
+ break
354
+
355
+ protein = self._safe_float(nutriments.get("proteins_100g", 0))
356
+ fat = self._safe_float(nutriments.get("fat_100g", 0))
357
+ carbs = self._safe_float(nutriments.get("carbohydrates_100g", 0))
358
+
359
+ # Basic validation
360
+ if calories <= 0 or calories > 3000:
361
+ return None
362
+
363
+ # Optional nutrients
364
+ fiber = self._safe_float(nutriments.get("fiber_100g")) or None
365
+ sugar = self._safe_float(nutriments.get("sugars_100g")) or None
366
+ sodium = self._safe_float(nutriments.get("sodium_100g")) or None
367
+
368
+ # Convert sodium g to mg
369
+ if sodium and sodium > 0:
370
+ sodium = sodium * 1000 if sodium < 50 else sodium
371
+
372
+ return NutritionInfo(
373
+ calories=calories,
374
+ protein=protein,
375
+ fat=fat,
376
+ carbs=carbs,
377
+ fiber=fiber,
378
+ sugar=sugar,
379
+ sodium=sodium
380
+ )
381
+
382
+ except Exception as e:
383
+ logger.debug(f"Nutrition extraction failed: {e}")
384
+ return None
385
+
386
+ def _validate_nutrition(self, nutrition: NutritionInfo) -> bool:
387
+ """Validate nutrition data makes sense"""
388
+ return (50 <= nutrition.calories <= 2000 and
389
+ 0 <= nutrition.protein <= 100 and
390
+ 0 <= nutrition.fat <= 100 and
391
+ 0 <= nutrition.carbs <= 100)
392
+
393
+ # Global model instances
394
+ food_model = None
395
+
396
+ @asynccontextmanager
397
+ async def lifespan(app: FastAPI):
398
+ """Initialize models on startup"""
399
+ global food_model
400
+ logger.info("🚀 Starting Fast Food Recognition Backend...")
401
+
402
+ # Load optimized food recognition model
403
+ food_model = FoodRecognitionModel()
404
+
405
+ logger.info("✅ Backend ready for fast food recognition!")
406
+ yield
407
+
408
+ # Cleanup on shutdown
409
+ logger.info("🛑 Shutting down backend...")
410
+
411
+ # FastAPI app
412
+ app = FastAPI(
413
+ title="Fast Food Recognition Backend",
414
+ description="Optimized CLIP-based food identification with Open Food Facts nutrition",
415
+ version="3.0.0",
416
+ lifespan=lifespan
417
+ )
418
+
419
+ # CORS middleware
420
+ app.add_middleware(
421
+ CORSMiddleware,
422
+ allow_origins=["*"],
423
+ allow_credentials=True,
424
+ allow_methods=["*"],
425
+ allow_headers=["*"],
426
+ )
427
+
428
+ # Utility functions
429
+ def validate_image(file: UploadFile) -> Image.Image:
430
+ """Validate and load uploaded image"""
431
+ try:
432
+ image_data = file.file.read()
433
+ image = Image.open(BytesIO(image_data))
434
+
435
+ # Convert to RGB if needed
436
+ if image.mode != 'RGB':
437
+ image = image.convert('RGB')
438
+
439
+ return image
440
+
441
+ except Exception as e:
442
+ raise HTTPException(
443
+ status_code=400,
444
+ detail=f"Invalid image file: {str(e)}"
445
+ )
446
+
447
+ async def validate_image_from_url(image_url: str) -> Image.Image:
448
+ """Validate and load image from URL"""
449
+ try:
450
+ async with aiohttp.ClientSession() as session:
451
+ async with session.get(image_url) as response:
452
+ if response.status != 200:
453
+ raise HTTPException(status_code=400, detail="Could not fetch image from URL")
454
+
455
+ image_data = await response.read()
456
+ image = Image.open(BytesIO(image_data))
457
+
458
+ if image.mode != 'RGB':
459
+ image = image.convert('RGB')
460
+
461
+ return image
462
+
463
+ except Exception as e:
464
+ raise HTTPException(
465
+ status_code=400,
466
+ detail=f"Invalid image URL: {str(e)}"
467
+ )
468
+
469
+ # API Endpoints
470
+
471
+ @app.get("/")
472
+ async def root():
473
+ """Health check endpoint"""
474
+ return {
475
+ "status": "healthy",
476
+ "message": "Fast Food Recognition Backend",
477
+ "version": "3.0.0",
478
+ "device": str(config.DEVICE)
479
+ }
480
+
481
+ @app.get("/health")
482
+ async def health_check():
483
+ """Detailed health check"""
484
+ return {
485
+ "status": "healthy",
486
+ "model_loaded": food_model is not None,
487
+ "device": config.DEVICE,
488
+ "food_pipeline_loaded": food_model.food_pipeline is not None if food_model else False,
489
+ "model_type": "Professional Food Recognition Models"
490
+ }
491
+
492
+ @app.post("/analyze", response_model=FoodAnalysisResponse)
493
+ async def analyze_food_image(
494
+ file: UploadFile = File(..., description="Food image to analyze"),
495
+ top_alternatives: int = Query(3, ge=1, le=5, description="Number of alternative predictions")
496
+ ):
497
+ """
498
+ Fast food image analysis with optimized CLIP recognition
499
+ """
500
+ try:
501
+ start_time = time.time()
502
+
503
+ # Validate and load image
504
+ image = validate_image(file)
505
+ logger.info(f"Image loaded in {time.time() - start_time:.2f}s")
506
+
507
+ # Fast food recognition - always returns high confidence results
508
+ food_name, confidence, alternatives = food_model.recognize_food(image)
509
+
510
+ # Get nutrition data
511
+ nutrition_start = time.time()
512
+ async with FastNutritionClient() as nutrition_client:
513
+ nutrition_result = await nutrition_client.get_nutrition(food_name)
514
+
515
+ if not nutrition_result:
516
+ raise HTTPException(
517
+ status_code=422,
518
+ detail=f"No nutrition data found for '{food_name}'. Try a different image or food type."
519
+ )
520
+
521
+ nutrition, source, product_id = nutrition_result
522
+ logger.info(f"Nutrition lookup completed in {time.time() - nutrition_start:.2f}s")
523
+
524
+ total_time = time.time() - start_time
525
+ logger.info(f"🎯 Complete analysis in {total_time:.2f}s: {food_name} ({confidence:.3f})")
526
+
527
+ return FoodAnalysisResponse(
528
+ label=food_name,
529
+ confidence=confidence,
530
+ nutrition=nutrition,
531
+ alternatives=alternatives[:top_alternatives],
532
+ source=source,
533
+ off_product_id=product_id
534
+ )
535
+
536
+ except HTTPException:
537
+ raise
538
+ except Exception as e:
539
+ logger.error(f"Analysis failed: {e}")
540
+ raise HTTPException(status_code=500, detail=f"Internal analysis error: {str(e)}")
541
+
542
+ @app.post("/analyze-url", response_model=FoodAnalysisResponse)
543
+ async def analyze_food_image_from_url(
544
+ image_url: str = Query(..., description="URL of food image to analyze"),
545
+ top_alternatives: int = Query(3, ge=1, le=5, description="Number of alternative predictions")
546
+ ):
547
+ """
548
+ Fast food image analysis from URL
549
+ """
550
+ try:
551
+ start_time = time.time()
552
+
553
+ # Load image from URL
554
+ image = await validate_image_from_url(image_url)
555
+
556
+ # Fast food recognition - always returns high confidence results
557
+ food_name, confidence, alternatives = food_model.recognize_food(image)
558
+
559
+ # Get nutrition data
560
+ async with FastNutritionClient() as nutrition_client:
561
+ nutrition_result = await nutrition_client.get_nutrition(food_name)
562
+
563
+ if not nutrition_result:
564
+ raise HTTPException(
565
+ status_code=422,
566
+ detail=f"No nutrition data found for '{food_name}'"
567
+ )
568
+
569
+ nutrition, source, product_id = nutrition_result
570
+
571
+ total_time = time.time() - start_time
572
+ logger.info(f"🎯 URL analysis completed in {total_time:.2f}s: {food_name}")
573
+
574
+ return FoodAnalysisResponse(
575
+ label=food_name,
576
+ confidence=confidence,
577
+ nutrition=nutrition,
578
+ alternatives=alternatives[:top_alternatives],
579
+ source=source,
580
+ off_product_id=product_id
581
+ )
582
+
583
+ except HTTPException:
584
+ raise
585
+ except Exception as e:
586
+ logger.error(f"URL analysis failed: {e}")
587
+ raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}")
588
+
589
+ @app.get("/search-nutrition/{food_name}")
590
+ async def search_nutrition_data(food_name: str):
591
+ """
592
+ Search for nutrition information for a specific food item
593
+ """
594
+ try:
595
+ async with FastNutritionClient() as nutrition_client:
596
+ nutrition_result = await nutrition_client.get_nutrition(food_name)
597
+
598
+ if not nutrition_result:
599
+ raise HTTPException(
600
+ status_code=404,
601
+ detail=f"No nutrition data found for '{food_name}'"
602
+ )
603
+
604
+ nutrition, source, product_id = nutrition_result
605
+
606
+ return {
607
+ "food_name": food_name,
608
+ "nutrition": nutrition,
609
+ "source": source,
610
+ "off_product_id": product_id
611
+ }
612
+
613
+ except HTTPException:
614
+ raise
615
+ except Exception as e:
616
+ logger.error(f"Nutrition search failed: {e}")
617
+ raise HTTPException(status_code=500, detail=f"Search failed: {str(e)}")
618
+
619
+ # Global exception handler
620
+ @app.exception_handler(Exception)
621
+ async def global_exception_handler(request, exc):
622
+ logger.error(f"Global exception: {exc}")
623
+ return JSONResponse(
624
+ status_code=500,
625
+ content={
626
+ "error": "Internal server error",
627
+ "detail": "An unexpected error occurred"
628
+ }
629
+ )
630
+
631
+ if __name__ == "__main__":
632
+ # Create backend directory if it doesn't exist
633
+ backend_dir = Path(__file__).parent
634
+ backend_dir.mkdir(exist_ok=True)
635
+
636
+ # Run the server with configuration from Config
637
+ uvicorn.run(
638
+ "app:app",
639
+ host=config.HOST,
640
+ port=config.PORT,
641
+ reload=True,
642
+ log_level="info"
643
+ )
requirements.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core FastAPI and server requirements
2
+ fastapi==0.104.1
3
+ uvicorn[standard]==0.24.0
4
+
5
+ # Machine Learning and Image Processing (optimized)
6
+ torch>=2.0.0
7
+ torchvision>=0.15.0
8
+ Pillow>=10.0.0
9
+
10
+ # Lightweight transformers for CLIP only
11
+ transformers>=4.30.0
12
+
13
+ # HTTP and API client
14
+ aiohttp>=3.8.0
15
+
16
+ # Data validation and serialization
17
+ pydantic>=2.0.0
18
+
19
+ # Utilities
20
+ python-multipart>=0.0.6