Spaces:
Sleeping
Sleeping
har1zarD
commited on
Commit
·
2936e62
1
Parent(s):
4ea8ae0
Remove app_ultimate.py and requirements_ultimate.txt files
Browse files- app.py +243 -386
- requirements.txt +11 -20
app.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import Optional, Dict, Any, List
|
|
| 6 |
import base64
|
| 7 |
import re
|
| 8 |
import requests
|
|
|
|
| 9 |
|
| 10 |
import uvicorn
|
| 11 |
from fastapi import FastAPI, File, UploadFile, HTTPException, Query
|
|
@@ -13,55 +14,93 @@ from fastapi.responses import JSONResponse
|
|
| 13 |
from fastapi.middleware.cors import CORSMiddleware
|
| 14 |
from PIL import Image
|
| 15 |
import torch
|
| 16 |
-
|
|
|
|
| 17 |
|
| 18 |
# --- Configuration ---
|
| 19 |
-
#
|
| 20 |
-
#
|
| 21 |
-
MODEL_NAME = "
|
| 22 |
-
# Alternative opcije (sve izvrsne):
|
| 23 |
-
# - "llava-hf/llava-v1.6-vicuna-7b-hf" - Također odličan
|
| 24 |
-
# - "llava-hf/llava-v1.6-vicuna-13b-hf" - Za maksimalnu preciznost (sporiji)
|
| 25 |
|
| 26 |
# --- Helper Functions ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
def load_model():
|
| 28 |
"""
|
| 29 |
-
Učitava
|
| 30 |
-
|
| 31 |
-
LLaVA-NeXT je trenutno NAJBOLJI open-source multimodal model sa:
|
| 32 |
-
- Superiornom vizuelnom razumijevanju
|
| 33 |
-
- Odličnim performansama na food recognition taskovima
|
| 34 |
-
- 100% stabilnim API-jem
|
| 35 |
-
- Brzom inferencom
|
| 36 |
"""
|
| 37 |
try:
|
| 38 |
-
print(f"Loading
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
device
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
MODEL_NAME
|
| 47 |
-
)
|
| 48 |
-
|
| 49 |
-
# Učitaj model sa optimizacijama (FIXOVANO: bez trust_remote_code)
|
| 50 |
-
model = LlavaNextForConditionalGeneration.from_pretrained(
|
| 51 |
-
MODEL_NAME,
|
| 52 |
-
torch_dtype=torch.float16 if device == "cuda" else torch.float32,
|
| 53 |
-
device_map="auto" if device == "cuda" else None
|
| 54 |
-
)
|
| 55 |
-
|
| 56 |
-
if device == "cpu":
|
| 57 |
-
model.to(device)
|
| 58 |
-
|
| 59 |
model.eval()
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
except Exception as e:
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
raise
|
| 66 |
|
| 67 |
def is_image_file(file: UploadFile):
|
|
@@ -229,231 +268,128 @@ def get_estimated_nutrition(food_name: str) -> Dict[str, Any]:
|
|
| 229 |
"note": "Nutritivne vrijednosti su procijenjene na osnovu kategorije hrane"
|
| 230 |
}
|
| 231 |
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
device: Device za izvršavanje (cuda/cpu)
|
| 257 |
-
|
| 258 |
-
Returns:
|
| 259 |
-
Dictionary sa sveobuhvatnim rezultatima analize
|
| 260 |
-
"""
|
| 261 |
-
results = {}
|
| 262 |
-
|
| 263 |
-
# Task 1: Sveobuhvatna Food Analiza
|
| 264 |
-
try:
|
| 265 |
-
prompt = """[INST] <image>
|
| 266 |
-
Analiziraj ovu sliku detaljno. Opiši koju hranu ili objekte vidiš, njihove približne porcije,
|
| 267 |
-
sastojke koje možeš identificirati, i bilo koji vidljivi tekst. Budi veoma specifičan i detaljan. [/INST]"""
|
| 268 |
-
|
| 269 |
-
inputs = processor(prompt, image, return_tensors="pt").to(device)
|
| 270 |
-
|
| 271 |
-
with torch.no_grad():
|
| 272 |
-
output = model.generate(
|
| 273 |
-
**inputs,
|
| 274 |
-
max_new_tokens=512,
|
| 275 |
-
temperature=0.1,
|
| 276 |
-
top_p=0.9,
|
| 277 |
-
do_sample=False
|
| 278 |
-
)
|
| 279 |
-
|
| 280 |
-
response = processor.decode(output[0], skip_special_tokens=True)
|
| 281 |
-
# Izvuci samo odgovor (skip prompt)
|
| 282 |
-
response = response.split("[/INST]")[-1].strip()
|
| 283 |
-
|
| 284 |
-
results["detailed_analysis"] = response
|
| 285 |
-
except Exception as e:
|
| 286 |
-
print(f"Greška u detaljnoj analizi: {e}")
|
| 287 |
-
results["detailed_analysis"] = ""
|
| 288 |
-
|
| 289 |
-
# Task 2: Specifična Identifikacija Hrane
|
| 290 |
-
try:
|
| 291 |
-
prompt = """[INST] <image>
|
| 292 |
-
Nabroj sve namirnice koje možeš identificirati na ovoj slici. Za svaku stavku navedite:
|
| 293 |
-
1) Naziv
|
| 294 |
-
2) Procijenjena porcija/količina
|
| 295 |
-
3) Glavni sastojci ako su vidljivi
|
| 296 |
-
|
| 297 |
-
Formatiraj kao numerisanu listu. [/INST]"""
|
| 298 |
-
|
| 299 |
-
inputs = processor(prompt, image, return_tensors="pt").to(device)
|
| 300 |
-
|
| 301 |
-
with torch.no_grad():
|
| 302 |
-
output = model.generate(
|
| 303 |
-
**inputs,
|
| 304 |
-
max_new_tokens=512,
|
| 305 |
-
temperature=0.1,
|
| 306 |
-
top_p=0.9,
|
| 307 |
-
do_sample=False
|
| 308 |
-
)
|
| 309 |
-
|
| 310 |
-
response = processor.decode(output[0], skip_special_tokens=True)
|
| 311 |
-
response = response.split("[/INST]")[-1].strip()
|
| 312 |
-
|
| 313 |
-
results["food_items"] = response
|
| 314 |
-
except Exception as e:
|
| 315 |
-
print(f"Greška u identifikaciji hrane: {e}")
|
| 316 |
-
results["food_items"] = ""
|
| 317 |
-
|
| 318 |
-
# Task 3: Nutritivni Kontekst
|
| 319 |
-
try:
|
| 320 |
-
prompt = """[INST] <image>
|
| 321 |
-
Na osnovu onoga što vidiš, daj kratak nutritivni pregled: Da li je ovaj obrok bogat proteinima,
|
| 322 |
-
ugljenim hidratima ili mastima? Da li je to zdrav izbor? Bilo kakve dijetetske napomene? [/INST]"""
|
| 323 |
-
|
| 324 |
-
inputs = processor(prompt, image, return_tensors="pt").to(device)
|
| 325 |
-
|
| 326 |
-
with torch.no_grad():
|
| 327 |
-
output = model.generate(
|
| 328 |
-
**inputs,
|
| 329 |
-
max_new_tokens=256,
|
| 330 |
-
temperature=0.1,
|
| 331 |
-
top_p=0.9,
|
| 332 |
-
do_sample=False
|
| 333 |
-
)
|
| 334 |
-
|
| 335 |
-
response = processor.decode(output[0], skip_special_tokens=True)
|
| 336 |
-
response = response.split("[/INST]")[-1].strip()
|
| 337 |
-
|
| 338 |
-
results["nutritional_context"] = response
|
| 339 |
-
except Exception as e:
|
| 340 |
-
print(f"Greška u nutritivnoj analizi: {e}")
|
| 341 |
-
results["nutritional_context"] = ""
|
| 342 |
-
|
| 343 |
-
# Task 4: OCR - Izvuci vidljivi tekst
|
| 344 |
-
try:
|
| 345 |
-
prompt = """[INST] <image>
|
| 346 |
-
Izvuci bilo koji vidljivi tekst na ovoj slici (etikete, nutritivne informacije, menije, znakove, itd.).
|
| 347 |
-
Ako nema teksta, reci 'Tekst nije detektovan'. [/INST]"""
|
| 348 |
-
|
| 349 |
-
inputs = processor(prompt, image, return_tensors="pt").to(device)
|
| 350 |
-
|
| 351 |
-
with torch.no_grad():
|
| 352 |
-
output = model.generate(
|
| 353 |
-
**inputs,
|
| 354 |
-
max_new_tokens=256,
|
| 355 |
-
temperature=0.1,
|
| 356 |
-
top_p=0.9,
|
| 357 |
-
do_sample=False
|
| 358 |
-
)
|
| 359 |
-
|
| 360 |
-
response = processor.decode(output[0], skip_special_tokens=True)
|
| 361 |
-
response = response.split("[/INST]")[-1].strip()
|
| 362 |
-
|
| 363 |
-
results["ocr_text"] = response
|
| 364 |
-
except Exception as e:
|
| 365 |
-
print(f"Greška u OCR-u: {e}")
|
| 366 |
-
results["ocr_text"] = ""
|
| 367 |
-
|
| 368 |
-
return results
|
| 369 |
-
|
| 370 |
-
def extract_food_info(analysis_results: Dict[str, Any]) -> Dict[str, Any]:
|
| 371 |
-
"""
|
| 372 |
-
Izvlači strukturirane food informacije iz LLaVA rezultata analize.
|
| 373 |
-
|
| 374 |
-
Args:
|
| 375 |
-
analysis_results: Sirovi rezultati iz LLaVA analize
|
| 376 |
-
|
| 377 |
-
Returns:
|
| 378 |
-
Formatirane food informacije
|
| 379 |
-
"""
|
| 380 |
-
detailed_analysis = analysis_results.get("detailed_analysis", "").lower()
|
| 381 |
-
food_items = analysis_results.get("food_items", "")
|
| 382 |
-
|
| 383 |
-
# Provjeri da li je prisutna hrana
|
| 384 |
-
food_keywords = [
|
| 385 |
-
"food", "meal", "dish", "plate", "bowl", "fruit", "vegetable", "hrana", "jelo",
|
| 386 |
-
"meat", "chicken", "beef", "fish", "pasta", "rice", "bread", "meso", "piletina",
|
| 387 |
-
"salad", "sandwich", "pizza", "burger", "dessert", "cake", "salata", "sendvič",
|
| 388 |
-
"cookie", "snack", "breakfast", "lunch", "dinner", "drink", "doručak", "ručak",
|
| 389 |
-
"beverage", "coffee", "tea", "juice", "kafa", "čaj", "sok"
|
| 390 |
]
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
return {
|
| 421 |
"primary_label": primary_label.title(),
|
| 422 |
-
"
|
| 423 |
-
"
|
| 424 |
-
"
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
}
|
| 430 |
|
| 431 |
# --- Učitaj Model pri Pokretanju Aplikacije ---
|
| 432 |
-
print("🚀 Pokrećem
|
| 433 |
-
processor, model, device = load_model()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
|
| 435 |
# --- FastAPI Aplikacija ---
|
| 436 |
app = FastAPI(
|
| 437 |
-
title="🍎
|
| 438 |
description="""
|
| 439 |
-
**🏆
|
| 440 |
|
| 441 |
-
|
| 442 |
|
| 443 |
### 🌟 Glavne Mogućnosti:
|
| 444 |
-
- 🍕 **AI Food Recognition** -
|
| 445 |
- 📊 **REALNI Nutritivni Podaci** - Automatski vraća kalorije, makroe, mikronutrijente
|
| 446 |
- 🔍 **Open Food Facts Integracija** - 700,000+ proizvoda u bazi
|
| 447 |
- 🤖 **AI Fallback Estimation** - Inteligentna procjena za nepoznatu hranu
|
| 448 |
- 🔎 **Manual Nutrition Lookup** - Pretraži nutrition po imenu hrane
|
| 449 |
- 📝 **Analiza Sastojaka** - Identificira vidljive sastojke i komponente
|
| 450 |
-
- 📄 **OCR
|
| 451 |
-
- 🎯 **Visual Question Answering** - Postavi bilo koje pitanje o slici
|
| 452 |
-
- 🌍 **Višejezična Podrška** - Radi sa tekstom na više jezika
|
| 453 |
|
| 454 |
### 🎯 Kako Radi:
|
| 455 |
1. **Upload** - Pošalji sliku hrane na `/analyze` endpoint
|
| 456 |
-
2. **AI Detection** -
|
| 457 |
3. **Nutrition Lookup** - Automatski pretraži Open Food Facts bazu
|
| 458 |
4. **Response** - Primiš naziv hrane + kompletan nutrition breakdown
|
| 459 |
|
|
@@ -476,7 +412,7 @@ app = FastAPI(
|
|
| 476 |
- 🤖 Inteligentna procjena za nepoznatu hranu
|
| 477 |
- ✅ Production-ready i stabilan
|
| 478 |
""",
|
| 479 |
-
version="
|
| 480 |
)
|
| 481 |
|
| 482 |
# Dodaj CORS middleware za web aplikacije
|
|
@@ -490,8 +426,8 @@ app.add_middleware(
|
|
| 490 |
|
| 491 |
@app.post("/analyze",
|
| 492 |
summary="Analiziraj Food Sliku",
|
| 493 |
-
description="Upload-uj sliku da dobiješ
|
| 494 |
-
response_description="
|
| 495 |
)
|
| 496 |
async def analyze(file: UploadFile = File(...)):
|
| 497 |
"""
|
|
@@ -528,13 +464,10 @@ async def analyze(file: UploadFile = File(...)):
|
|
| 528 |
raise HTTPException(status_code=500, detail=f"Greška pri čitanju slike: {e}")
|
| 529 |
|
| 530 |
try:
|
| 531 |
-
#
|
| 532 |
-
print("🔍 Analiziram sliku sa
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
# Izvuci strukturirane food informacije
|
| 536 |
-
food_info = extract_food_info(analysis_results)
|
| 537 |
-
|
| 538 |
except Exception as e:
|
| 539 |
print(f"Greška tokom analize: {e}")
|
| 540 |
raise HTTPException(status_code=500, detail=f"Greška tokom analize: {e}")
|
|
@@ -567,12 +500,12 @@ async def analyze(file: UploadFile = File(...)):
|
|
| 567 |
# Alternative
|
| 568 |
"alternatives": food_info["alternative_labels"],
|
| 569 |
|
| 570 |
-
|
| 571 |
-
|
| 572 |
"detailed_description": food_info["detailed_analysis"],
|
| 573 |
"food_items": food_info["food_items"],
|
| 574 |
-
"nutritional_context":
|
| 575 |
-
"ocr_text":
|
| 576 |
},
|
| 577 |
|
| 578 |
"image_info": {
|
|
@@ -584,14 +517,10 @@ async def analyze(file: UploadFile = File(...)):
|
|
| 584 |
"model_info": {
|
| 585 |
"vision_model": MODEL_NAME,
|
| 586 |
"nutrition_source": nutrition_data["source"],
|
| 587 |
-
"type": "
|
| 588 |
"capabilities": [
|
| 589 |
-
"Food Recognition",
|
| 590 |
-
"Nutrition Data Lookup"
|
| 591 |
-
"Ingredient Analysis",
|
| 592 |
-
"Portion Estimation",
|
| 593 |
-
"Multi-Object Detection",
|
| 594 |
-
"OCR & Text Understanding"
|
| 595 |
]
|
| 596 |
}
|
| 597 |
}
|
|
@@ -599,66 +528,14 @@ async def analyze(file: UploadFile = File(...)):
|
|
| 599 |
return JSONResponse(content=final_response)
|
| 600 |
|
| 601 |
@app.post("/ask",
|
| 602 |
-
summary="Postavi Pitanje o Slici",
|
| 603 |
-
description="
|
| 604 |
)
|
| 605 |
async def ask_about_image(
|
| 606 |
file: UploadFile = File(...),
|
| 607 |
question: str = Query(..., description="Tvoje pitanje o slici")
|
| 608 |
):
|
| 609 |
-
""
|
| 610 |
-
**Visual Question Answering Endpoint**
|
| 611 |
-
|
| 612 |
-
Upload-uj sliku i postavi BILO KOJE pitanje o njoj:
|
| 613 |
-
- "Koje sastojke vidiš?"
|
| 614 |
-
- "Da li je ovo zdrav obrok?"
|
| 615 |
-
- "Koliko približno kalorija?"
|
| 616 |
-
- "Koja je ovo kuhinja?"
|
| 617 |
-
- "Može li vegetarijanac ovo jesti?"
|
| 618 |
-
"""
|
| 619 |
-
if not file:
|
| 620 |
-
raise HTTPException(status_code=400, detail="Slika nije poslata.")
|
| 621 |
-
|
| 622 |
-
if not is_image_file(file):
|
| 623 |
-
raise HTTPException(
|
| 624 |
-
status_code=400,
|
| 625 |
-
detail="Nepodržan format slike. Koristi JPEG, PNG ili WebP."
|
| 626 |
-
)
|
| 627 |
-
|
| 628 |
-
try:
|
| 629 |
-
contents = await file.read()
|
| 630 |
-
image = Image.open(BytesIO(contents))
|
| 631 |
-
|
| 632 |
-
if image.mode != "RGB":
|
| 633 |
-
image = image.convert("RGB")
|
| 634 |
-
|
| 635 |
-
# Pripremi VQA prompt
|
| 636 |
-
prompt = f"[INST] <image>\n{question} [/INST]"
|
| 637 |
-
|
| 638 |
-
inputs = processor(prompt, image, return_tensors="pt").to(device)
|
| 639 |
-
|
| 640 |
-
with torch.no_grad():
|
| 641 |
-
output = model.generate(
|
| 642 |
-
**inputs,
|
| 643 |
-
max_new_tokens=512,
|
| 644 |
-
temperature=0.2,
|
| 645 |
-
top_p=0.9,
|
| 646 |
-
do_sample=True
|
| 647 |
-
)
|
| 648 |
-
|
| 649 |
-
response = processor.decode(output[0], skip_special_tokens=True)
|
| 650 |
-
answer = response.split("[/INST]")[-1].strip()
|
| 651 |
-
|
| 652 |
-
return JSONResponse(content={
|
| 653 |
-
"success": True,
|
| 654 |
-
"question": question,
|
| 655 |
-
"answer": answer,
|
| 656 |
-
"model": MODEL_NAME
|
| 657 |
-
})
|
| 658 |
-
|
| 659 |
-
except Exception as e:
|
| 660 |
-
print(f"Greška tokom VQA: {e}")
|
| 661 |
-
raise HTTPException(status_code=500, detail=f"Greška tokom analize: {e}")
|
| 662 |
|
| 663 |
@app.get("/search-nutrition/{food_name}",
|
| 664 |
summary="Pretraži Nutritivne Podatke",
|
|
@@ -714,33 +591,33 @@ async def search_nutrition(food_name: str):
|
|
| 714 |
def root():
|
| 715 |
"""Root endpoint sa API informacijama."""
|
| 716 |
return {
|
| 717 |
-
"message": "🍎
|
| 718 |
"status": "🟢 Online",
|
| 719 |
"tagline": "🏆 Najbolji Self-Hosted Food Recognition + Nutrition API",
|
| 720 |
"model": {
|
| 721 |
"vision_model": MODEL_NAME,
|
| 722 |
"nutrition_source": "Open Food Facts + AI Estimation",
|
| 723 |
-
"type": "
|
| 724 |
-
"provider": "
|
| 725 |
-
"generation": "
|
| 726 |
"device": device.upper(),
|
| 727 |
-
"rank": "🥇
|
| 728 |
},
|
| 729 |
"capabilities": {
|
| 730 |
-
"food_recognition": "✅
|
| 731 |
"nutrition_data": "✅ Realne Nutritivne Vrijednosti",
|
| 732 |
"nutrition_lookup": "✅ Manual Search po Imenu",
|
| 733 |
-
"ingredient_analysis": "
|
| 734 |
-
"portion_estimation": "
|
| 735 |
-
"multi_object_detection": "
|
| 736 |
-
"ocr": "
|
| 737 |
-
"visual_qa": "
|
| 738 |
-
"offline_mode": "✅
|
| 739 |
"database": "✅ Open Food Facts (700K+ proizvoda)"
|
| 740 |
},
|
| 741 |
"endpoints": {
|
| 742 |
"POST /analyze": "🍕 Upload food sliku - AI prepozna + vrati nutritivne podatke",
|
| 743 |
-
"POST /ask": "
|
| 744 |
"GET /search-nutrition/{food_name}": "🔍 Pretraži nutritivne podatke po imenu hrane",
|
| 745 |
"GET /health": "💚 Provjeri API i model health status",
|
| 746 |
"GET /capabilities": "📋 Lista svih mogućnosti modela",
|
|
@@ -750,11 +627,11 @@ def root():
|
|
| 750 |
"advantages": {
|
| 751 |
"cost": "💰 100% Besplatno - Nikad nema API troškova",
|
| 752 |
"privacy": "🔒 Self-hosted - Tvoji podaci ostaju privatni",
|
| 753 |
-
"performance": "⚡
|
| 754 |
"nutrition_accuracy": "📊 Realni podaci iz Open Food Facts baze",
|
| 755 |
"fallback": "🤖 AI procjena ako hrana nije u bazi",
|
| 756 |
-
"offline": "📡
|
| 757 |
-
"stability": "✅
|
| 758 |
"updates": "🔄 Open-source - Uvijek se poboljšava"
|
| 759 |
},
|
| 760 |
"documentation": "Posjeti /docs za interaktivno API testiranje"
|
|
@@ -781,12 +658,12 @@ def health_check():
|
|
| 781 |
"model_loaded": model_status,
|
| 782 |
"vision_model": MODEL_NAME,
|
| 783 |
"nutrition_api": nutrition_api_status,
|
| 784 |
-
"model_type": "
|
| 785 |
"device": device,
|
| 786 |
"device_available": torch.cuda.is_available() if device == "cuda" else True,
|
| 787 |
-
"version": "
|
| 788 |
"timestamp": "2025-10-08",
|
| 789 |
-
"ranking": "🥇
|
| 790 |
}
|
| 791 |
|
| 792 |
@app.get("/capabilities",
|
|
@@ -798,13 +675,13 @@ def get_capabilities():
|
|
| 798 |
return {
|
| 799 |
"vision_model": MODEL_NAME,
|
| 800 |
"nutrition_source": "Open Food Facts",
|
| 801 |
-
"generation": "
|
| 802 |
-
"release": "2024 (
|
| 803 |
"vision_tasks": {
|
| 804 |
"food_recognition": {
|
| 805 |
-
"description": "
|
| 806 |
-
"accuracy": "
|
| 807 |
-
"features": ["
|
| 808 |
},
|
| 809 |
"nutrition_data": {
|
| 810 |
"description": "Vraća REALNE nutritivne vrijednosti iz baze podataka",
|
|
@@ -813,30 +690,10 @@ def get_capabilities():
|
|
| 813 |
"data_includes": ["Kalorije", "Proteini", "Ugljeni hidrati", "Masti", "Vlakna", "Šećeri", "Natrijum"],
|
| 814 |
"per_serving": "100g (standardno)"
|
| 815 |
},
|
| 816 |
-
"nutritional_analysis": {
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
}
|
| 820 |
-
"visual_understanding": {
|
| 821 |
-
"description": "Sveobuhvatno razumijevanje i opis slike",
|
| 822 |
-
"output": "Detaljni opisi na prirodnom jeziku",
|
| 823 |
-
"depth": "Ultra-detaljno sa kontekstom"
|
| 824 |
-
},
|
| 825 |
-
"ocr": {
|
| 826 |
-
"description": "Izvlači i razumije tekst sa slika",
|
| 827 |
-
"languages": "Višejezično (100+ jezika)",
|
| 828 |
-
"applications": ["Nutrition labels", "Menus", "Recipes", "Signs"]
|
| 829 |
-
},
|
| 830 |
-
"visual_qa": {
|
| 831 |
-
"description": "Odgovara na bilo koje pitanje o slici",
|
| 832 |
-
"interaction": "Konverzacijski",
|
| 833 |
-
"examples": [
|
| 834 |
-
"Koje sastojke vidiš?",
|
| 835 |
-
"Da li je ovo zdrav obrok?",
|
| 836 |
-
"Koliko približno kalorija?",
|
| 837 |
-
"Koja je ovo kuhinja?"
|
| 838 |
-
]
|
| 839 |
-
}
|
| 840 |
},
|
| 841 |
"use_cases": [
|
| 842 |
"Profesionalno nutrition tracking sa realnim podacima",
|
|
@@ -846,32 +703,31 @@ def get_capabilities():
|
|
| 846 |
"Sistemi za dijetetske preporuke",
|
| 847 |
"Food delivery aplikacije sa nutrition labels",
|
| 848 |
"Health i fitness platforme",
|
| 849 |
-
"Analiza recepata
|
| 850 |
-
"
|
| 851 |
-
"Kontrola porcija i kalorija",
|
| 852 |
"Edukativne food i nutrition aplikacije",
|
| 853 |
"Medical i healthcare nutrition tracking"
|
| 854 |
],
|
| 855 |
"advantages": [
|
| 856 |
-
"🏆
|
| 857 |
"📊 REALNI nutritivni podaci iz Open Food Facts",
|
| 858 |
-
"💯
|
| 859 |
"🆓 Potpuno besplatno za korištenje",
|
| 860 |
"🔒 Self-hostable za privatnost",
|
| 861 |
"⚡ Brza inferenca",
|
| 862 |
"🤖 AI fallback estimation za nepoznatu hranu",
|
| 863 |
"📡 Vision model radi offline",
|
| 864 |
"🌍 Višejezična podrška",
|
| 865 |
-
"🎯
|
| 866 |
"💪 Robustan i pouzdan",
|
| 867 |
"🔄 Aktivno održavan",
|
| 868 |
-
"✅
|
| 869 |
"🔬 700,000+ proizvoda u bazi"
|
| 870 |
],
|
| 871 |
"technical_specs": {
|
| 872 |
-
"parameters": "
|
| 873 |
-
"architecture": "
|
| 874 |
-
"training_data": "
|
| 875 |
"supported_formats": ["JPEG", "PNG", "WebP"],
|
| 876 |
"max_resolution": "Podrška za visoke rezolucije",
|
| 877 |
"batch_processing": "Podržano",
|
|
@@ -885,28 +741,29 @@ def get_capabilities():
|
|
| 885 |
# --- Pokreni API ---
|
| 886 |
if __name__ == "__main__":
|
| 887 |
print("=" * 80)
|
| 888 |
-
print("🍎
|
| 889 |
print("=" * 80)
|
| 890 |
print(f"🤖 Vision Model: {MODEL_NAME}")
|
| 891 |
print(f"📊 Nutrition Source: Open Food Facts + AI Estimation")
|
| 892 |
-
print(f"🏢 Provider:
|
| 893 |
-
print(f"🔧 Type:
|
| 894 |
print(f"💻 Device: {device.upper()}")
|
| 895 |
-
print(f"🎯 Rank:
|
| 896 |
print(f"✨ Status: Production Ready - NUTRITION EDITION")
|
| 897 |
print(f"💰 Cost: $0 - 100% Besplatno Self-Hosted")
|
| 898 |
print("=" * 80)
|
| 899 |
print("🌟 NOVE MOGUĆNOSTI:")
|
| 900 |
-
print(" ✅
|
| 901 |
print(" ✅ Automatsko vraćanje nutritivnih vrijednosti")
|
| 902 |
print(" ✅ 700,000+ proizvoda u Open Food Facts bazi")
|
| 903 |
print(" ✅ AI procjena za nepoznatu hranu")
|
| 904 |
print(" ✅ Manual nutrition lookup po imenu")
|
| 905 |
print("=" * 80)
|
| 906 |
-
|
| 907 |
-
print("
|
| 908 |
-
print("
|
|
|
|
| 909 |
print("=" * 80)
|
| 910 |
-
uvicorn.run(app, host="0.0.0.0", port=
|
| 911 |
|
| 912 |
|
|
|
|
| 6 |
import base64
|
| 7 |
import re
|
| 8 |
import requests
|
| 9 |
+
import contextlib
|
| 10 |
|
| 11 |
import uvicorn
|
| 12 |
from fastapi import FastAPI, File, UploadFile, HTTPException, Query
|
|
|
|
| 14 |
from fastapi.middleware.cors import CORSMiddleware
|
| 15 |
from PIL import Image
|
| 16 |
import torch
|
| 17 |
+
import torch.nn.functional as F
|
| 18 |
+
from transformers import CLIPProcessor, CLIPModel
|
| 19 |
|
| 20 |
# --- Configuration ---
|
| 21 |
+
# LITE varijanta: CLIP zero-shot klasifikacija nad Food-101 labelama (CPU-friendly)
|
| 22 |
+
# Zadano koristi ViT-L/14 model; može se promijeniti preko env varijable MODEL_NAME
|
| 23 |
+
MODEL_NAME = os.environ.get("MODEL_NAME", "openai/clip-vit-large-patch14")
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
# --- Helper Functions ---
|
| 26 |
+
def select_device() -> str:
|
| 27 |
+
"""Odabire najbolji dostupni uređaj: CUDA > MPS (Apple) > CPU."""
|
| 28 |
+
if torch.cuda.is_available():
|
| 29 |
+
return "cuda"
|
| 30 |
+
# MPS (Apple Silicon)
|
| 31 |
+
try:
|
| 32 |
+
if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
| 33 |
+
return "mps"
|
| 34 |
+
except Exception:
|
| 35 |
+
pass
|
| 36 |
+
return "cpu"
|
| 37 |
+
|
| 38 |
+
def select_dtype(device: str):
|
| 39 |
+
"""Odabire optimalni dtype za dati uređaj (za manju memoriju i bržu inferencu)."""
|
| 40 |
+
if device == "cuda":
|
| 41 |
+
return torch.float16
|
| 42 |
+
# MPS je najstabilniji sa float16 za CLIP u praksi
|
| 43 |
+
if device == "mps":
|
| 44 |
+
return torch.float16
|
| 45 |
+
return torch.float32
|
| 46 |
+
|
| 47 |
+
def autocast_context(device: str, dtype):
|
| 48 |
+
"""Vraća odgovarajući autocast kontekst za dati uređaj i dtype (ili no-op)."""
|
| 49 |
+
if device in ("cuda", "cpu", "mps"):
|
| 50 |
+
try:
|
| 51 |
+
return torch.autocast(device_type=device, dtype=dtype)
|
| 52 |
+
except Exception:
|
| 53 |
+
return contextlib.nullcontext()
|
| 54 |
+
return contextlib.nullcontext()
|
| 55 |
+
|
| 56 |
def load_model():
|
| 57 |
"""
|
| 58 |
+
Učitava lagani CLIP model i processor za zero-shot klasifikaciju.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
"""
|
| 60 |
try:
|
| 61 |
+
print(f"Loading LITE model: {MODEL_NAME}...")
|
| 62 |
+
device = select_device()
|
| 63 |
+
dtype = select_dtype(device)
|
| 64 |
+
print(f"Using device: {device} | dtype: {dtype}")
|
| 65 |
+
processor = CLIPProcessor.from_pretrained(MODEL_NAME)
|
| 66 |
+
# Preferiraj sigurnije safetensors težine + učitaj direktno u niži dtype
|
| 67 |
+
model = CLIPModel.from_pretrained(MODEL_NAME, use_safetensors=True, torch_dtype=dtype)
|
| 68 |
+
model.to(device)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
model.eval()
|
| 70 |
+
# Opcionalni compile za dodatni throughput na CUDA
|
| 71 |
+
if device == "cuda" and os.environ.get("CLIP_COMPILE", "1") == "1" and hasattr(torch, "compile"):
|
| 72 |
+
try:
|
| 73 |
+
model = torch.compile(model, mode="reduce-overhead", fullgraph=False)
|
| 74 |
+
print("⚡ torch.compile omogućen (reduce-overhead)")
|
| 75 |
+
except Exception as _e:
|
| 76 |
+
print(f"ℹ️ torch.compile nije omogućen: {_e}")
|
| 77 |
+
print("✅ LITE CLIP model učitan uspješno!")
|
| 78 |
+
return processor, model, device, dtype
|
| 79 |
+
except ValueError as e:
|
| 80 |
+
# Jasnija poruka za CVE i torch>=2.6 zahtjev
|
| 81 |
+
if "upgrade torch to at least v2.6" in str(e) or "torch.load" in str(e):
|
| 82 |
+
msg = (
|
| 83 |
+
"Zbog CVE-2025-32434 potrebno je koristiti torch>=2.6. "
|
| 84 |
+
"Ažuriraj okruženje: pip install --upgrade 'torch>=2.6' 'safetensors>=0.4.3'\n"
|
| 85 |
+
"Ako radiš lokalno: pip install -r requirements.txt"
|
| 86 |
+
)
|
| 87 |
+
print(f"❌ {msg}")
|
| 88 |
+
raise
|
| 89 |
except Exception as e:
|
| 90 |
+
# Pokušaj fallback bez safetensors (ako je dostupno i okruženje je sigurno)
|
| 91 |
+
print(f"⚠️ Primarni load sa safetensors nije uspio: {e}. Pokušavam fallback...")
|
| 92 |
+
try:
|
| 93 |
+
device = select_device()
|
| 94 |
+
dtype = select_dtype(device)
|
| 95 |
+
processor = CLIPProcessor.from_pretrained(MODEL_NAME)
|
| 96 |
+
model = CLIPModel.from_pretrained(MODEL_NAME, use_safetensors=False, torch_dtype=dtype)
|
| 97 |
+
model.to(device)
|
| 98 |
+
model.eval()
|
| 99 |
+
print("✅ LITE CLIP model učitan uspješno (fallback način)!")
|
| 100 |
+
return processor, model, device, dtype
|
| 101 |
+
except Exception as e2:
|
| 102 |
+
print(f"❌ Greška pri učitavanju CLIP modela (fallback): {e2}")
|
| 103 |
+
print(f"❌ Greška pri učitavanju CLIP modela: {e}")
|
| 104 |
raise
|
| 105 |
|
| 106 |
def is_image_file(file: UploadFile):
|
|
|
|
| 268 |
"note": "Nutritivne vrijednosti su procijenjene na osnovu kategorije hrane"
|
| 269 |
}
|
| 270 |
|
| 271 |
+
def get_food101_labels() -> List[str]:
|
| 272 |
+
"""Vraća listu Food-101 klasa (formatirano sa razmacima)."""
|
| 273 |
+
raw_labels = [
|
| 274 |
+
"apple_pie", "baby_back_ribs", "baklava", "beef_carpaccio", "beef_tartare",
|
| 275 |
+
"beet_salad", "beignets", "bibimbap", "bread_pudding", "breakfast_burrito",
|
| 276 |
+
"bruschetta", "caesar_salad", "cannoli", "caprese_salad", "carrot_cake",
|
| 277 |
+
"ceviche", "cheesecake", "cheese_plate", "chicken_curry", "chicken_quesadilla",
|
| 278 |
+
"chicken_wings", "chocolate_cake", "chocolate_mousse", "churros", "clam_chowder",
|
| 279 |
+
"club_sandwich", "crab_cakes", "creme_brulee", "croque_madame", "cup_cakes",
|
| 280 |
+
"deviled_eggs", "donuts", "dumplings", "edamame", "eggs_benedict",
|
| 281 |
+
"escargots", "falafel", "filet_mignon", "fish_and_chips", "foie_gras",
|
| 282 |
+
"french_fries", "french_onion_soup", "french_toast", "fried_calamari", "fried_rice",
|
| 283 |
+
"frozen_yogurt", "garlic_bread", "gnocchi", "greek_salad", "grilled_cheese_sandwich",
|
| 284 |
+
"grilled_salmon", "guacamole", "gyoza", "hamburger", "hot_and_sour_soup",
|
| 285 |
+
"hot_dog", "huevos_rancheros", "hummus", "ice_cream", "lasagna",
|
| 286 |
+
"lobster_bisque", "lobster_roll_sandwich", "macaroni_and_cheese", "macarons", "miso_soup",
|
| 287 |
+
"mussels", "nachos", "omelette", "onion_rings", "oysters",
|
| 288 |
+
"pad_thai", "paella", "pancakes", "panna_cotta", "peking_duck",
|
| 289 |
+
"pho", "pizza", "pork_chop", "poutine", "prime_rib",
|
| 290 |
+
"pulled_pork_sandwich", "ramen", "ravioli", "red_velvet_cake", "risotto",
|
| 291 |
+
"samosa", "sashimi", "scallops", "seaweed_salad", "shrimp_and_grits",
|
| 292 |
+
"spaghetti_bolognese", "spaghetti_carbonara", "spring_rolls", "steak", "strawberry_shortcake",
|
| 293 |
+
"sushi", "tacos", "takoyaki", "tiramisu", "tuna_tartare",
|
| 294 |
+
"waffles"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
]
|
| 296 |
+
return [label.replace("_", " ") for label in raw_labels]
|
| 297 |
+
|
| 298 |
+
def build_text_cache(labels: List[str], processor: CLIPProcessor, model: CLIPModel, device: str, dtype) -> torch.Tensor:
|
| 299 |
+
"""Prekompajlira i kešira CLIP tekstualne embeddinge za Food-101 labele (L2-normalizovane)."""
|
| 300 |
+
with torch.no_grad():
|
| 301 |
+
text_inputs = processor(text=labels, return_tensors="pt", padding=True, truncation=True)
|
| 302 |
+
text_inputs = {k: v.to(device) for k, v in text_inputs.items()}
|
| 303 |
+
with autocast_context(device, dtype):
|
| 304 |
+
text_features = model.get_text_features(**text_inputs)
|
| 305 |
+
text_features = text_features / text_features.norm(dim=-1, keepdim=True)
|
| 306 |
+
return text_features
|
| 307 |
+
|
| 308 |
+
def warmup_model(processor: CLIPProcessor, model: CLIPModel, device: str, dtype):
|
| 309 |
+
"""Kratki warmup da se popune keševi i stabilizuje latency (posebno uz torch.compile)."""
|
| 310 |
+
try:
|
| 311 |
+
img = Image.new("RGB", (224, 224), color=(127, 127, 127))
|
| 312 |
+
img_inputs = processor(images=img, return_tensors="pt")
|
| 313 |
+
img_inputs = {k: v.to(device) for k, v in img_inputs.items()}
|
| 314 |
+
with torch.no_grad(), autocast_context(device, dtype):
|
| 315 |
+
_ = model.get_image_features(**img_inputs)
|
| 316 |
+
if device == "cuda":
|
| 317 |
+
torch.cuda.synchronize()
|
| 318 |
+
print("🔥 Warmup završen")
|
| 319 |
+
except Exception as _e:
|
| 320 |
+
print(f"ℹ️ Warmup preskočen: {_e}")
|
| 321 |
+
|
| 322 |
+
def classify_image_with_clip(image: Image.Image, processor: CLIPProcessor, model: CLIPModel, device: str) -> Dict[str, Any]:
|
| 323 |
+
"""Zero-shot klasifikacija slike nad Food-101 labelama koristeći CLIP sa keširanim tekst embedding-ima."""
|
| 324 |
+
global TEXT_FEATURES, TEXT_LABELS, CURRENT_DTYPE
|
| 325 |
+
labels = TEXT_LABELS
|
| 326 |
+
img_inputs = processor(images=image, return_tensors="pt")
|
| 327 |
+
img_inputs = {k: v.to(device) for k, v in img_inputs.items()}
|
| 328 |
+
with torch.no_grad(), autocast_context(device, CURRENT_DTYPE):
|
| 329 |
+
image_features = model.get_image_features(**img_inputs)
|
| 330 |
+
image_features = image_features / image_features.norm(dim=-1, keepdim=True)
|
| 331 |
+
logits = (image_features @ TEXT_FEATURES.t()) * 100.0
|
| 332 |
+
probs = F.softmax(logits, dim=1).cpu().numpy()[0]
|
| 333 |
+
# Top-5
|
| 334 |
+
top_indices = probs.argsort()[-5:][::-1]
|
| 335 |
+
top_labels = [labels[i] for i in top_indices]
|
| 336 |
+
top_probs = [float(probs[i]) for i in top_indices]
|
| 337 |
+
primary_label = top_labels[0]
|
| 338 |
return {
|
| 339 |
"primary_label": primary_label.title(),
|
| 340 |
+
"alternatives": [l.title() for l in top_labels[1:]],
|
| 341 |
+
"confidence": top_probs[0],
|
| 342 |
+
"top5": list(zip(top_labels, top_probs))
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
def extract_clip_food_info(classification: Dict[str, Any]) -> Dict[str, Any]:
|
| 346 |
+
"""Formatira rezultat CLIP klasifikacije u zajedničku strukturu."""
|
| 347 |
+
primary = classification["primary_label"]
|
| 348 |
+
alts = classification["alternatives"]
|
| 349 |
+
conf = classification["confidence"]
|
| 350 |
+
# Jednostavan tekstualni rezime umjesto LLaVA eseja
|
| 351 |
+
detailed = f"Detektovano: {primary} (povjerenje {conf:.2f}). Top-5: " + \
|
| 352 |
+
", ".join([f"{l.title()} ({p:.2f})" for l, p in classification["top5"]])
|
| 353 |
+
items = f"1) {primary}"
|
| 354 |
+
return {
|
| 355 |
+
"primary_label": primary,
|
| 356 |
+
"alternative_labels": alts,
|
| 357 |
+
"detailed_analysis": detailed,
|
| 358 |
+
"food_items": items,
|
| 359 |
+
"nutritional_context": "",
|
| 360 |
+
"ocr_text": "",
|
| 361 |
+
"has_food": True,
|
| 362 |
+
"confidence": conf
|
| 363 |
}
|
| 364 |
|
| 365 |
# --- Učitaj Model pri Pokretanju Aplikacije ---
|
| 366 |
+
print("🚀 Pokrećem LITE Food Scanner API (CLIP)...")
|
| 367 |
+
processor, model, device, dtype = load_model()
|
| 368 |
+
CURRENT_DTYPE = dtype
|
| 369 |
+
TEXT_LABELS = get_food101_labels()
|
| 370 |
+
TEXT_FEATURES = build_text_cache(TEXT_LABELS, processor, model, device, dtype)
|
| 371 |
+
warmup_model(processor, model, device, dtype)
|
| 372 |
|
| 373 |
# --- FastAPI Aplikacija ---
|
| 374 |
app = FastAPI(
|
| 375 |
+
title="🍎 LITE Food Scanner API - Nutrition Edition (CLIP)",
|
| 376 |
description="""
|
| 377 |
+
**🏆 Lako i brzo prepoznavanje hrane + Nutrition Lookup (CPU-friendly)**
|
| 378 |
|
| 379 |
+
Koristi CLIP zero-shot klasifikaciju nad Food-101 klasama i Open Food Facts bazu.
|
| 380 |
|
| 381 |
### 🌟 Glavne Mogućnosti:
|
| 382 |
+
- 🍕 **AI Food Recognition** - CLIP zero-shot prepoznaje hranu iz slike
|
| 383 |
- 📊 **REALNI Nutritivni Podaci** - Automatski vraća kalorije, makroe, mikronutrijente
|
| 384 |
- 🔍 **Open Food Facts Integracija** - 700,000+ proizvoda u bazi
|
| 385 |
- 🤖 **AI Fallback Estimation** - Inteligentna procjena za nepoznatu hranu
|
| 386 |
- 🔎 **Manual Nutrition Lookup** - Pretraži nutrition po imenu hrane
|
| 387 |
- 📝 **Analiza Sastojaka** - Identificira vidljive sastojke i komponente
|
| 388 |
+
- 📄 **OCR / VQA** - Onemogućeno u LITE modu radi uštede memorije
|
|
|
|
|
|
|
| 389 |
|
| 390 |
### 🎯 Kako Radi:
|
| 391 |
1. **Upload** - Pošalji sliku hrane na `/analyze` endpoint
|
| 392 |
+
2. **AI Detection** - CLIP model identificira koja je hrana na slici
|
| 393 |
3. **Nutrition Lookup** - Automatski pretraži Open Food Facts bazu
|
| 394 |
4. **Response** - Primiš naziv hrane + kompletan nutrition breakdown
|
| 395 |
|
|
|
|
| 412 |
- 🤖 Inteligentna procjena za nepoznatu hranu
|
| 413 |
- ✅ Production-ready i stabilan
|
| 414 |
""",
|
| 415 |
+
version="9.0.0 - LITE (CLIP)"
|
| 416 |
)
|
| 417 |
|
| 418 |
# Dodaj CORS middleware za web aplikacije
|
|
|
|
| 426 |
|
| 427 |
@app.post("/analyze",
|
| 428 |
summary="Analiziraj Food Sliku",
|
| 429 |
+
description="Upload-uj sliku da dobiješ food label + nutritivne podatke (CLIP LITE)",
|
| 430 |
+
response_description="Rezultati food recognition i nutritivnih podataka"
|
| 431 |
)
|
| 432 |
async def analyze(file: UploadFile = File(...)):
|
| 433 |
"""
|
|
|
|
| 464 |
raise HTTPException(status_code=500, detail=f"Greška pri čitanju slike: {e}")
|
| 465 |
|
| 466 |
try:
|
| 467 |
+
# Zero-shot klasifikacija sa CLIP-om
|
| 468 |
+
print("🔍 Analiziram sliku sa CLIP (zero-shot Food-101)...")
|
| 469 |
+
classification = classify_image_with_clip(image, processor, model, device)
|
| 470 |
+
food_info = extract_clip_food_info(classification)
|
|
|
|
|
|
|
|
|
|
| 471 |
except Exception as e:
|
| 472 |
print(f"Greška tokom analize: {e}")
|
| 473 |
raise HTTPException(status_code=500, detail=f"Greška tokom analize: {e}")
|
|
|
|
| 500 |
# Alternative
|
| 501 |
"alternatives": food_info["alternative_labels"],
|
| 502 |
|
| 503 |
+
# Dodatne informacije (LITE)
|
| 504 |
+
"ai_analysis": {
|
| 505 |
"detailed_description": food_info["detailed_analysis"],
|
| 506 |
"food_items": food_info["food_items"],
|
| 507 |
+
"nutritional_context": "",
|
| 508 |
+
"ocr_text": ""
|
| 509 |
},
|
| 510 |
|
| 511 |
"image_info": {
|
|
|
|
| 517 |
"model_info": {
|
| 518 |
"vision_model": MODEL_NAME,
|
| 519 |
"nutrition_source": nutrition_data["source"],
|
| 520 |
+
"type": "CLIP Zero-shot Classifier + Nutrition Database",
|
| 521 |
"capabilities": [
|
| 522 |
+
"Food Recognition (Food-101)",
|
| 523 |
+
"Nutrition Data Lookup"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
]
|
| 525 |
}
|
| 526 |
}
|
|
|
|
| 528 |
return JSONResponse(content=final_response)
|
| 529 |
|
| 530 |
@app.post("/ask",
|
| 531 |
+
summary="Postavi Pitanje o Slici (LITE onemogućeno)",
|
| 532 |
+
description="U LITE modu VQA je onemogućeno radi uštede memorije"
|
| 533 |
)
|
| 534 |
async def ask_about_image(
|
| 535 |
file: UploadFile = File(...),
|
| 536 |
question: str = Query(..., description="Tvoje pitanje o slici")
|
| 537 |
):
|
| 538 |
+
raise HTTPException(status_code=501, detail="VQA je onemogućeno u LITE modu. Koristi /analyze za prepoznavanje hrane.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
|
| 540 |
@app.get("/search-nutrition/{food_name}",
|
| 541 |
summary="Pretraži Nutritivne Podatke",
|
|
|
|
| 591 |
def root():
|
| 592 |
"""Root endpoint sa API informacijama."""
|
| 593 |
return {
|
| 594 |
+
"message": "🍎 LITE Food Scanner API v9.0 - CLIP + Nutrition Edition",
|
| 595 |
"status": "🟢 Online",
|
| 596 |
"tagline": "🏆 Najbolji Self-Hosted Food Recognition + Nutrition API",
|
| 597 |
"model": {
|
| 598 |
"vision_model": MODEL_NAME,
|
| 599 |
"nutrition_source": "Open Food Facts + AI Estimation",
|
| 600 |
+
"type": "CLIP Zero-shot Classifier + Nutrition Database",
|
| 601 |
+
"provider": "OpenAI CLIP + Open Food Facts",
|
| 602 |
+
"generation": "CLIP (ViT-L/14)",
|
| 603 |
"device": device.upper(),
|
| 604 |
+
"rank": "🥇 LITE rješenje za Food Recognition"
|
| 605 |
},
|
| 606 |
"capabilities": {
|
| 607 |
+
"food_recognition": "✅ Food-101 Zero-shot (CLIP)",
|
| 608 |
"nutrition_data": "✅ Realne Nutritivne Vrijednosti",
|
| 609 |
"nutrition_lookup": "✅ Manual Search po Imenu",
|
| 610 |
+
"ingredient_analysis": "❌ (LITE)",
|
| 611 |
+
"portion_estimation": "❌ (LITE)",
|
| 612 |
+
"multi_object_detection": "❌ (LITE)",
|
| 613 |
+
"ocr": "❌ (LITE)",
|
| 614 |
+
"visual_qa": "❌ (LITE)",
|
| 615 |
+
"offline_mode": "✅",
|
| 616 |
"database": "✅ Open Food Facts (700K+ proizvoda)"
|
| 617 |
},
|
| 618 |
"endpoints": {
|
| 619 |
"POST /analyze": "🍕 Upload food sliku - AI prepozna + vrati nutritivne podatke",
|
| 620 |
+
"POST /ask": "❌ Onemogućeno u LITE modu",
|
| 621 |
"GET /search-nutrition/{food_name}": "🔍 Pretraži nutritivne podatke po imenu hrane",
|
| 622 |
"GET /health": "💚 Provjeri API i model health status",
|
| 623 |
"GET /capabilities": "📋 Lista svih mogućnosti modela",
|
|
|
|
| 627 |
"advantages": {
|
| 628 |
"cost": "💰 100% Besplatno - Nikad nema API troškova",
|
| 629 |
"privacy": "🔒 Self-hosted - Tvoji podaci ostaju privatni",
|
| 630 |
+
"performance": "⚡ Brza inferenca (CPU-friendly)",
|
| 631 |
"nutrition_accuracy": "📊 Realni podaci iz Open Food Facts baze",
|
| 632 |
"fallback": "🤖 AI procjena ako hrana nije u bazi",
|
| 633 |
+
"offline": "📡 Radi offline (model)",
|
| 634 |
+
"stability": "✅ Stabilno i production-ready",
|
| 635 |
"updates": "🔄 Open-source - Uvijek se poboljšava"
|
| 636 |
},
|
| 637 |
"documentation": "Posjeti /docs za interaktivno API testiranje"
|
|
|
|
| 658 |
"model_loaded": model_status,
|
| 659 |
"vision_model": MODEL_NAME,
|
| 660 |
"nutrition_api": nutrition_api_status,
|
| 661 |
+
"model_type": "CLIP Zero-shot Classifier + Nutrition Database",
|
| 662 |
"device": device,
|
| 663 |
"device_available": torch.cuda.is_available() if device == "cuda" else True,
|
| 664 |
+
"version": "9.0.0 - LITE (CLIP)",
|
| 665 |
"timestamp": "2025-10-08",
|
| 666 |
+
"ranking": "🥇 LITE Food Recognition + Nutrition Rješenje"
|
| 667 |
}
|
| 668 |
|
| 669 |
@app.get("/capabilities",
|
|
|
|
| 675 |
return {
|
| 676 |
"vision_model": MODEL_NAME,
|
| 677 |
"nutrition_source": "Open Food Facts",
|
| 678 |
+
"generation": "CLIP (ViT-L/14) + Nutrition Database",
|
| 679 |
+
"release": "2024 (Stable)",
|
| 680 |
"vision_tasks": {
|
| 681 |
"food_recognition": {
|
| 682 |
+
"description": "Zero-shot klasifikacija nad Food-101 listom klasa",
|
| 683 |
+
"accuracy": "Visoka (zavisno od scene)",
|
| 684 |
+
"features": ["Top-5 predlozi", "CPU-friendly"]
|
| 685 |
},
|
| 686 |
"nutrition_data": {
|
| 687 |
"description": "Vraća REALNE nutritivne vrijednosti iz baze podataka",
|
|
|
|
| 690 |
"data_includes": ["Kalorije", "Proteini", "Ugljeni hidrati", "Masti", "Vlakna", "Šećeri", "Natrijum"],
|
| 691 |
"per_serving": "100g (standardno)"
|
| 692 |
},
|
| 693 |
+
"nutritional_analysis": {"description": "(LITE) Onemogućeno"},
|
| 694 |
+
"visual_understanding": {"description": "(LITE) Onemogućeno"},
|
| 695 |
+
"ocr": {"description": "(LITE) Onemogućeno"},
|
| 696 |
+
"visual_qa": {"description": "(LITE) Onemogućeno"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
},
|
| 698 |
"use_cases": [
|
| 699 |
"Profesionalno nutrition tracking sa realnim podacima",
|
|
|
|
| 703 |
"Sistemi za dijetetske preporuke",
|
| 704 |
"Food delivery aplikacije sa nutrition labels",
|
| 705 |
"Health i fitness platforme",
|
| 706 |
+
"Analiza recepata (preko naziva)",
|
| 707 |
+
"Kontrola porcija (ručno)",
|
|
|
|
| 708 |
"Edukativne food i nutrition aplikacije",
|
| 709 |
"Medical i healthcare nutrition tracking"
|
| 710 |
],
|
| 711 |
"advantages": [
|
| 712 |
+
"🏆 Lagano i brzo rješenje",
|
| 713 |
"📊 REALNI nutritivni podaci iz Open Food Facts",
|
| 714 |
+
"💯 Dobra preciznost u food recognition (Food-101)",
|
| 715 |
"🆓 Potpuno besplatno za korištenje",
|
| 716 |
"🔒 Self-hostable za privatnost",
|
| 717 |
"⚡ Brza inferenca",
|
| 718 |
"🤖 AI fallback estimation za nepoznatu hranu",
|
| 719 |
"📡 Vision model radi offline",
|
| 720 |
"🌍 Višejezična podrška",
|
| 721 |
+
"🎯 Fokus na hranu + nutrition",
|
| 722 |
"💪 Robustan i pouzdan",
|
| 723 |
"🔄 Aktivno održavan",
|
| 724 |
+
"✅ Stabilan i production-ready",
|
| 725 |
"🔬 700,000+ proizvoda u bazi"
|
| 726 |
],
|
| 727 |
"technical_specs": {
|
| 728 |
+
"parameters": "~427M",
|
| 729 |
+
"architecture": "CLIP (ViT-L/14)",
|
| 730 |
+
"training_data": "WIT + zero-shot na Food-101",
|
| 731 |
"supported_formats": ["JPEG", "PNG", "WebP"],
|
| 732 |
"max_resolution": "Podrška za visoke rezolucije",
|
| 733 |
"batch_processing": "Podržano",
|
|
|
|
| 741 |
# --- Pokreni API ---
|
| 742 |
if __name__ == "__main__":
|
| 743 |
print("=" * 80)
|
| 744 |
+
print("🍎 LITE FOOD SCANNER API v9.0 - NUTRITION EDITION (CLIP)")
|
| 745 |
print("=" * 80)
|
| 746 |
print(f"🤖 Vision Model: {MODEL_NAME}")
|
| 747 |
print(f"📊 Nutrition Source: Open Food Facts + AI Estimation")
|
| 748 |
+
print(f"🏢 Provider: OpenAI CLIP + Open Food Facts")
|
| 749 |
+
print(f"🔧 Type: CLIP Zero-shot Classifier + Nutrition Database")
|
| 750 |
print(f"💻 Device: {device.upper()}")
|
| 751 |
+
print(f"🎯 Rank: LITE Food Recognition + Nutrition Rješenje")
|
| 752 |
print(f"✨ Status: Production Ready - NUTRITION EDITION")
|
| 753 |
print(f"💰 Cost: $0 - 100% Besplatno Self-Hosted")
|
| 754 |
print("=" * 80)
|
| 755 |
print("🌟 NOVE MOGUĆNOSTI:")
|
| 756 |
+
print(" ✅ Zero-shot prepoznavanje hrane (Food-101)")
|
| 757 |
print(" ✅ Automatsko vraćanje nutritivnih vrijednosti")
|
| 758 |
print(" ✅ 700,000+ proizvoda u Open Food Facts bazi")
|
| 759 |
print(" ✅ AI procjena za nepoznatu hranu")
|
| 760 |
print(" ✅ Manual nutrition lookup po imenu")
|
| 761 |
print("=" * 80)
|
| 762 |
+
run_port = int(os.environ.get("PORT", "8000"))
|
| 763 |
+
print(f"🌍 Pokrećem server na http://0.0.0.0:{run_port}")
|
| 764 |
+
print(f"📚 API Docs: http://0.0.0.0:{run_port}/docs")
|
| 765 |
+
print("🔥 Spreman za food recognition + nutrition analysis (LITE)!")
|
| 766 |
print("=" * 80)
|
| 767 |
+
uvicorn.run(app, host="0.0.0.0", port=run_port)
|
| 768 |
|
| 769 |
|
requirements.txt
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
#
|
| 2 |
-
#
|
| 3 |
|
| 4 |
# Core API Framework
|
| 5 |
fastapi==0.115.0
|
|
@@ -9,26 +9,17 @@ python-multipart==0.0.12
|
|
| 9 |
# Image Processing
|
| 10 |
pillow==11.0.0
|
| 11 |
|
| 12 |
-
# Deep Learning
|
| 13 |
-
torch>=2.
|
| 14 |
-
|
|
|
|
| 15 |
|
| 16 |
-
# Transformers
|
| 17 |
-
transformers>=4.
|
| 18 |
-
accelerate>=0.31.0
|
| 19 |
|
| 20 |
-
#
|
| 21 |
-
timm>=0.9.0
|
| 22 |
-
einops>=0.7.0
|
| 23 |
-
|
| 24 |
-
# Utilities
|
| 25 |
-
numpy>=1.24.0
|
| 26 |
-
sentencepiece>=0.2.0
|
| 27 |
-
protobuf>=4.25.0
|
| 28 |
requests>=2.32.0
|
| 29 |
-
httpx>=0.27.0
|
| 30 |
|
| 31 |
-
#
|
| 32 |
-
#
|
| 33 |
-
# Sve dependencies su standardne i provjerene za production
|
| 34 |
|
|
|
|
| 1 |
+
# LITE Food Scanner API - CLIP Edition
|
| 2 |
+
# Minimalni requirements za CPU-friendly food recognition
|
| 3 |
|
| 4 |
# Core API Framework
|
| 5 |
fastapi==0.115.0
|
|
|
|
| 9 |
# Image Processing
|
| 10 |
pillow==11.0.0
|
| 11 |
|
| 12 |
+
# Deep Learning / Transformers
|
| 13 |
+
# NOTE: Due to CVE-2025-32434, torch must be >=2.6 to allow torch.load() via transformers
|
| 14 |
+
torch>=2.6.0
|
| 15 |
+
safetensors>=0.4.3
|
| 16 |
|
| 17 |
+
# Transformers (CLIP)
|
| 18 |
+
transformers>=4.44.2
|
|
|
|
| 19 |
|
| 20 |
+
# HTTP util
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
requests>=2.32.0
|
|
|
|
| 22 |
|
| 23 |
+
# Napomena: LITE varijanta ne zahtijeva torchvision/timm/accelerate/einops
|
| 24 |
+
# CLIP radi preko transformers + torch
|
|
|
|
| 25 |
|