Gianone/smartplate
0
1"""2SmartPlate Gradio application β inference only, no training code here.3 4Run locally:5 python app.py6 7On Hugging Face Spaces, this file is loaded automatically.8"""9 10from __future__ import annotations11 12import logging13import os14from pathlib import Path15from typing import Optional, Tuple16 17from dotenv import load_dotenv18 19load_dotenv()20 21import gradio as gr22 23# Aggressive patch: bypass Gradio's API info schema parser entirely24# Fixes both "bool not iterable" AND "Cannot parse schema True"25import gradio_client.utils as _gcu26 27_original_json_to_type = _gcu._json_schema_to_python_type28 29def _safe_json_to_type(schema, defs=None):30 if not isinstance(schema, dict):31 return "Any"32 try:33 return _original_json_to_type(schema, defs)34 except Exception:35 return "Any"36 37_gcu._json_schema_to_python_type = _safe_json_to_type38 39def _safe_top_level(schema):40 if not isinstance(schema, dict):41 return "Any"42 try:43 return _safe_json_to_type(schema, schema.get("$defs"))44 except Exception:45 return "Any"46 47_gcu.json_schema_to_python_type = _safe_top_level48 49_original_get_type = _gcu.get_type50def _patched_get_type(schema):51 if not isinstance(schema, dict):52 return "Any"53 try:54 return _original_get_type(schema)55 except Exception:56 return "Any"57_gcu.get_type = _patched_get_type58 59from PIL import Image60 61from src.pipeline import SmartPlatePipeline62 63logging.basicConfig(level=logging.INFO)64logger = logging.getLogger(__name__)65 66_pipeline: Optional[SmartPlatePipeline] = None67 68 69def get_pipeline() -> SmartPlatePipeline:70 global _pipeline71 if _pipeline is None:72 _pipeline = SmartPlatePipeline()73 return _pipeline74 75 76def analyze_meal(77 image: Optional[Image.Image],78 user_question: str,79) -> Tuple[str, str, str]:80 """Gradio callback: run the full pipeline and return formatted outputs.81 82 Returns:83 Tuple of (cv_output, ml_output, nlp_output) as Markdown strings.84 """85 if image is None:86 return "Please upload a meal photo to get started.", "", ""87 88 question = user_question.strip() if user_question else None89 90 try:91 result = get_pipeline().process(image, user_question=question)92 93 cv = result["cv_result"]94 ml = result["ml_result"]95 nlp = result["nlp_result"]96 97 # --- CV output ---98 food_name = cv["class"].replace("_", " ").title()99 cv_text = f"**{food_name}**\n\nConfidence: {cv['confidence']:.0%}"100 if cv.get("top_5") and len(cv["top_5"]) > 1:101 top5_lines = "\n".join(102 f"- {r['class'].replace('_', ' ').title()}: {r['confidence']:.0%}"103 for r in cv["top_5"]104 )105 cv_text += f"\n\n**Top 5 predictions:**\n{top5_lines}"106 107 # --- ML output ---108 n = ml["nutrition"]109 health = ml["health_label"].upper()110 health_emoji = {"HEALTHY": "π’", "MEDIUM": "π‘", "UNHEALTHY": "π΄"}.get(111 health, "βͺ"112 )113 proba = ml.get("probabilities", {})114 proba_str = " ".join(115 f"{k}: {v:.0%}" for k, v in proba.items()116 )117 118 ml_text = (119 f"### Nutritional Values (per 100 g)\n\n"120 f"| Nutrient | Amount |\n"121 f"|---|---|\n"122 f"| Energy | {n['kcal']:.0f} kcal |\n"123 f"| Fat | {n['fat']:.1f} g |\n"124 f"| β Saturated fat | {n['sat_fat']:.1f} g |\n"125 f"| Carbohydrates | {n['carbs']:.1f} g |\n"126 f"| β Sugars | {n['sugar']:.1f} g |\n"127 f"| Fiber | {n['fiber']:.1f} g |\n"128 f"| Protein | {n['protein']:.1f} g |\n"129 f"| Salt | {n['salt']:.1f} g |\n\n"130 f"**Health Category:** {health_emoji} {health}\n\n"131 f"*Confidence: {proba_str}*"132 )133 134 # --- NLP output ---135 sources = nlp.get("sources", [])136 sources_str = " Β· ".join(dict.fromkeys(sources)) if sources else "WHO Β· DGE Β· Harvard"137 nlp_text = f"{nlp['answer']}\n\n*Sources: {sources_str}*"138 139 return cv_text, ml_text, nlp_text140 141 except EnvironmentError as exc:142 logger.error("Environment error: %s", exc)143 return (144 "β οΈ Configuration error.",145 str(exc),146 "Please set OPENAI_API_KEY in your .env file.",147 )148 except Exception as exc:149 logger.error("Pipeline error: %s", exc, exc_info=True)150 return f"β οΈ Error: {exc}", "", "Please try again or check the logs."151 152 153# ββ Gradio layout ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ154 155_EXAMPLES_DIR = Path("assets/examples")156 157 158def _find_examples() -> list:159 """Return example image paths if the directory exists."""160 if not _EXAMPLES_DIR.exists():161 return []162 paths = sorted(163 list(_EXAMPLES_DIR.glob("*.jpg"))164 + list(_EXAMPLES_DIR.glob("*.jpeg"))165 + list(_EXAMPLES_DIR.glob("*.png"))166 )167 return [[str(p), ""] for p in paths[:5]]168 169 170with gr.Blocks(171 title="SmartPlate β AI Nutrition Assistant",172 theme=gr.themes.Soft(),173) as demo:174 gr.Markdown(175 "# SmartPlate β AI Nutrition Assistant π½οΈ\n"176 "Photograph your meal and get instant nutritional analysis with "177 "evidence-based health advice."178 )179 180 with gr.Row():181 with gr.Column(scale=1):182 img_input = gr.Image(type="pil", label="Upload a meal photo")183 question_input = gr.Textbox(184 label="Ask a question (optional)",185 placeholder="e.g. Can I eat this on a diet?",186 lines=2,187 )188 submit_btn = gr.Button("Analyze π", variant="primary")189 190 with gr.Column(scale=2):191 cv_output = gr.Markdown(label="Dish Recognition")192 ml_output = gr.Markdown(label="Nutritional Analysis")193 nlp_output = gr.Markdown(label="Health Advice")194 195 examples = _find_examples()196 if examples:197 gr.Examples(198 examples=examples,199 inputs=[img_input, question_input],200 label="Try an example",201 )202 203 submit_btn.click(204 fn=analyze_meal,205 inputs=[img_input, question_input],206 outputs=[cv_output, ml_output, nlp_output],207 )208 209 gr.Markdown(210 "---\n"211 "**Sources:** WHO Β· DGE (Deutsche Gesellschaft fΓΌr ErnΓ€hrung) Β· Harvard Nutrition\n\n"212 "*For educational use only β not medical advice. "213 "ZHAW KI-Anwendungen FS 2026.*"214 )215 216 217if __name__ == "__main__":218 share = os.getenv("GRADIO_SHARE", "false").lower() == "true"219 demo.launch(show_api=False)