Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -40,6 +40,8 @@ from fastapi import FastAPI, HTTPException, Header, Depends
|
|
| 40 |
from fastapi.middleware.cors import CORSMiddleware
|
| 41 |
from pydantic import BaseModel
|
| 42 |
from PIL import Image
|
|
|
|
|
|
|
| 43 |
|
| 44 |
# ββ Lazy imports for heavy ML deps ββββββββββββββββββββββββββββββββββββββββββ
|
| 45 |
# Imported inside lifespan so the Space starts quickly and fails clearly
|
|
@@ -208,10 +210,50 @@ def _run_inference(image: Image.Image, max_new_tokens: int) -> str:
|
|
| 208 |
return _processor.batch_decode(new_tokens, skip_special_tokens=True)[0].strip()
|
| 209 |
|
| 210 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
def _parse_response(raw: str) -> dict:
|
| 212 |
result = {"ingredients": "", "portion_notes": "", "raw_text": raw}
|
| 213 |
|
| 214 |
-
# Try
|
| 215 |
if "Ingredients detected:" in raw:
|
| 216 |
ing_start = raw.index("Ingredients detected:") + len("Ingredients detected:")
|
| 217 |
ing_end = raw.index(".", ing_start) if "." in raw[ing_start:] else len(raw)
|
|
@@ -223,8 +265,8 @@ def _parse_response(raw: str) -> dict:
|
|
| 223 |
result["portion_notes"] = raw[pa_start:pa_end].strip()
|
| 224 |
|
| 225 |
if "JSON Summary:" in raw:
|
| 226 |
-
json_start
|
| 227 |
-
json_str
|
| 228 |
brace_start = json_str.find("{")
|
| 229 |
brace_end = json_str.rfind("}") + 1
|
| 230 |
if brace_start != -1 and brace_end > brace_start:
|
|
@@ -240,9 +282,21 @@ def _parse_response(raw: str) -> dict:
|
|
| 240 |
except json.JSONDecodeError:
|
| 241 |
pass
|
| 242 |
|
| 243 |
-
# ββ
|
| 244 |
-
if not result["ingredients"]
|
| 245 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
|
| 247 |
return result
|
| 248 |
|
|
|
|
| 40 |
from fastapi.middleware.cors import CORSMiddleware
|
| 41 |
from pydantic import BaseModel
|
| 42 |
from PIL import Image
|
| 43 |
+
import re
|
| 44 |
+
import requests as req_lib
|
| 45 |
|
| 46 |
# ββ Lazy imports for heavy ML deps ββββββββββββββββββββββββββββββββββββββββββ
|
| 47 |
# Imported inside lifespan so the Space starts quickly and fails clearly
|
|
|
|
| 210 |
return _processor.batch_decode(new_tokens, skip_special_tokens=True)[0].strip()
|
| 211 |
|
| 212 |
|
| 213 |
+
import re
|
| 214 |
+
import requests as req_lib
|
| 215 |
+
|
| 216 |
+
def _get_nutrition_from_api(ingredients_text: str) -> dict:
|
| 217 |
+
"""Use Open Food Facts search β no API key needed."""
|
| 218 |
+
try:
|
| 219 |
+
# Take the first identified food item
|
| 220 |
+
food_query = ingredients_text.split(",")[0].strip()
|
| 221 |
+
|
| 222 |
+
response = req_lib.get(
|
| 223 |
+
"https://world.openfoodfacts.org/cgi/search.pl",
|
| 224 |
+
params={
|
| 225 |
+
"search_terms": food_query,
|
| 226 |
+
"search_simple": 1,
|
| 227 |
+
"action": "process",
|
| 228 |
+
"json": 1,
|
| 229 |
+
"page_size": 1,
|
| 230 |
+
},
|
| 231 |
+
timeout=10,
|
| 232 |
+
)
|
| 233 |
+
response.raise_for_status()
|
| 234 |
+
data = response.json()
|
| 235 |
+
products = data.get("products", [])
|
| 236 |
+
|
| 237 |
+
if not products:
|
| 238 |
+
return {}
|
| 239 |
+
|
| 240 |
+
nutriments = products[0].get("nutriments", {})
|
| 241 |
+
return {
|
| 242 |
+
"calories": round(nutriments.get("energy-kcal_100g", 0) or 0, 1),
|
| 243 |
+
"protein_g": round(nutriments.get("proteins_100g", 0) or 0, 1),
|
| 244 |
+
"carbs_g": round(nutriments.get("carbohydrates_100g", 0) or 0, 1),
|
| 245 |
+
"fat_g": round(nutriments.get("fat_100g", 0) or 0, 1),
|
| 246 |
+
"fibre_g": round(nutriments.get("fiber_100g", 0) or 0, 1),
|
| 247 |
+
}
|
| 248 |
+
except Exception as e:
|
| 249 |
+
logger.warning(f"Open Food Facts API failed: {e}")
|
| 250 |
+
return {}
|
| 251 |
+
|
| 252 |
+
|
| 253 |
def _parse_response(raw: str) -> dict:
|
| 254 |
result = {"ingredients": "", "portion_notes": "", "raw_text": raw}
|
| 255 |
|
| 256 |
+
# ββ Try structured CaLoRAify format first βββββββββββββββββββββββββββββ
|
| 257 |
if "Ingredients detected:" in raw:
|
| 258 |
ing_start = raw.index("Ingredients detected:") + len("Ingredients detected:")
|
| 259 |
ing_end = raw.index(".", ing_start) if "." in raw[ing_start:] else len(raw)
|
|
|
|
| 265 |
result["portion_notes"] = raw[pa_start:pa_end].strip()
|
| 266 |
|
| 267 |
if "JSON Summary:" in raw:
|
| 268 |
+
json_start = raw.index("JSON Summary:") + len("JSON Summary:")
|
| 269 |
+
json_str = raw[json_start:].strip()
|
| 270 |
brace_start = json_str.find("{")
|
| 271 |
brace_end = json_str.rfind("}") + 1
|
| 272 |
if brace_start != -1 and brace_end > brace_start:
|
|
|
|
| 282 |
except json.JSONDecodeError:
|
| 283 |
pass
|
| 284 |
|
| 285 |
+
# ββ Fallback: model gave plain description βββββββββββββββββββββββββββββ
|
| 286 |
+
if not result["ingredients"]:
|
| 287 |
+
# Extract food nouns from the raw description
|
| 288 |
+
result["ingredients"] = raw.strip()[:200]
|
| 289 |
+
result["portion_notes"] = "Portion estimated from image."
|
| 290 |
+
|
| 291 |
+
# ββ If we still have no calories, call Nutritionix ββββββββββββββββββββ
|
| 292 |
+
if result["calories"] is None and result["ingredients"]:
|
| 293 |
+
logger.info(f"Calling Nutritionix for: {result['ingredients'][:80]}")
|
| 294 |
+
nutrition = _get_nutrition_from_api(result["ingredients"])
|
| 295 |
+
if nutrition:
|
| 296 |
+
result.update(nutrition)
|
| 297 |
+
logger.info(f"Nutritionix returned: {nutrition}")
|
| 298 |
+
else:
|
| 299 |
+
logger.warning("Nutritionix returned nothing β calories will be None")
|
| 300 |
|
| 301 |
return result
|
| 302 |
|