""" 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)