smartplate / app.py
Gianone's picture
fix(deploy): aggressive monkey-patch for Gradio API info schema
3063ef6
Raw
History Blame Contribute Delete
6.73 kB
"""
SmartPlate Gradio application β€” inference only, no training code here.
Run locally:
python app.py
On Hugging Face Spaces, this file is loaded automatically.
"""
from __future__ import annotations
import logging
import os
from pathlib import Path
from typing import Optional, Tuple
from dotenv import load_dotenv
load_dotenv()
import gradio as gr
# Aggressive patch: bypass Gradio's API info schema parser entirely
# Fixes both "bool not iterable" AND "Cannot parse schema True"
import gradio_client.utils as _gcu
_original_json_to_type = _gcu._json_schema_to_python_type
def _safe_json_to_type(schema, defs=None):
if not isinstance(schema, dict):
return "Any"
try:
return _original_json_to_type(schema, defs)
except Exception:
return "Any"
_gcu._json_schema_to_python_type = _safe_json_to_type
def _safe_top_level(schema):
if not isinstance(schema, dict):
return "Any"
try:
return _safe_json_to_type(schema, schema.get("$defs"))
except Exception:
return "Any"
_gcu.json_schema_to_python_type = _safe_top_level
_original_get_type = _gcu.get_type
def _patched_get_type(schema):
if not isinstance(schema, dict):
return "Any"
try:
return _original_get_type(schema)
except Exception:
return "Any"
_gcu.get_type = _patched_get_type
from PIL import Image
from src.pipeline import SmartPlatePipeline
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
_pipeline: Optional[SmartPlatePipeline] = None
def get_pipeline() -> SmartPlatePipeline:
global _pipeline
if _pipeline is None:
_pipeline = SmartPlatePipeline()
return _pipeline
def analyze_meal(
image: Optional[Image.Image],
user_question: str,
) -> Tuple[str, str, str]:
"""Gradio callback: run the full pipeline and return formatted outputs.
Returns:
Tuple of (cv_output, ml_output, nlp_output) as Markdown strings.
"""
if image is None:
return "Please upload a meal photo to get started.", "", ""
question = user_question.strip() if user_question else None
try:
result = get_pipeline().process(image, user_question=question)
cv = result["cv_result"]
ml = result["ml_result"]
nlp = result["nlp_result"]
# --- CV output ---
food_name = cv["class"].replace("_", " ").title()
cv_text = f"**{food_name}**\n\nConfidence: {cv['confidence']:.0%}"
if cv.get("top_5") and len(cv["top_5"]) > 1:
top5_lines = "\n".join(
f"- {r['class'].replace('_', ' ').title()}: {r['confidence']:.0%}"
for r in cv["top_5"]
)
cv_text += f"\n\n**Top 5 predictions:**\n{top5_lines}"
# --- ML output ---
n = ml["nutrition"]
health = ml["health_label"].upper()
health_emoji = {"HEALTHY": "🟒", "MEDIUM": "🟑", "UNHEALTHY": "πŸ”΄"}.get(
health, "βšͺ"
)
proba = ml.get("probabilities", {})
proba_str = " ".join(
f"{k}: {v:.0%}" for k, v in proba.items()
)
ml_text = (
f"### Nutritional Values (per 100 g)\n\n"
f"| Nutrient | Amount |\n"
f"|---|---|\n"
f"| Energy | {n['kcal']:.0f} kcal |\n"
f"| Fat | {n['fat']:.1f} g |\n"
f"| β€” Saturated fat | {n['sat_fat']:.1f} g |\n"
f"| Carbohydrates | {n['carbs']:.1f} g |\n"
f"| β€” Sugars | {n['sugar']:.1f} g |\n"
f"| Fiber | {n['fiber']:.1f} g |\n"
f"| Protein | {n['protein']:.1f} g |\n"
f"| Salt | {n['salt']:.1f} g |\n\n"
f"**Health Category:** {health_emoji} {health}\n\n"
f"*Confidence: {proba_str}*"
)
# --- NLP output ---
sources = nlp.get("sources", [])
sources_str = " Β· ".join(dict.fromkeys(sources)) if sources else "WHO Β· DGE Β· Harvard"
nlp_text = f"{nlp['answer']}\n\n*Sources: {sources_str}*"
return cv_text, ml_text, nlp_text
except EnvironmentError as exc:
logger.error("Environment error: %s", exc)
return (
"⚠️ Configuration error.",
str(exc),
"Please set OPENAI_API_KEY in your .env file.",
)
except Exception as exc:
logger.error("Pipeline error: %s", exc, exc_info=True)
return f"⚠️ Error: {exc}", "", "Please try again or check the logs."
# ── Gradio layout ──────────────────────────────────────────────────────────────
_EXAMPLES_DIR = Path("assets/examples")
def _find_examples() -> list:
"""Return example image paths if the directory exists."""
if not _EXAMPLES_DIR.exists():
return []
paths = sorted(
list(_EXAMPLES_DIR.glob("*.jpg"))
+ list(_EXAMPLES_DIR.glob("*.jpeg"))
+ list(_EXAMPLES_DIR.glob("*.png"))
)
return [[str(p), ""] for p in paths[:5]]
with gr.Blocks(
title="SmartPlate – AI Nutrition Assistant",
theme=gr.themes.Soft(),
) as demo:
gr.Markdown(
"# SmartPlate – AI Nutrition Assistant 🍽️\n"
"Photograph your meal and get instant nutritional analysis with "
"evidence-based health advice."
)
with gr.Row():
with gr.Column(scale=1):
img_input = gr.Image(type="pil", label="Upload a meal photo")
question_input = gr.Textbox(
label="Ask a question (optional)",
placeholder="e.g. Can I eat this on a diet?",
lines=2,
)
submit_btn = gr.Button("Analyze πŸ”", variant="primary")
with gr.Column(scale=2):
cv_output = gr.Markdown(label="Dish Recognition")
ml_output = gr.Markdown(label="Nutritional Analysis")
nlp_output = gr.Markdown(label="Health Advice")
examples = _find_examples()
if examples:
gr.Examples(
examples=examples,
inputs=[img_input, question_input],
label="Try an example",
)
submit_btn.click(
fn=analyze_meal,
inputs=[img_input, question_input],
outputs=[cv_output, ml_output, nlp_output],
)
gr.Markdown(
"---\n"
"**Sources:** WHO Β· DGE (Deutsche Gesellschaft fΓΌr ErnΓ€hrung) Β· Harvard Nutrition\n\n"
"*For educational use only β€” not medical advice. "
"ZHAW KI-Anwendungen FS 2026.*"
)
if __name__ == "__main__":
share = os.getenv("GRADIO_SHARE", "false").lower() == "true"
demo.launch(show_api=False)