"""Gradio frontend for the Küchenpass-Agent. Run locally: python -m space.app Deployment: this file is uploaded as-is to a HuggingFace Space (see the metadata header in space/README.md). """ from __future__ import annotations import json import os import sys from datetime import datetime from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from src.runtime import configure_runtime, ensure_model_artifacts # noqa: E402 configure_runtime() ensure_model_artifacts( required_files=( "prep_time_pipeline.joblib", "food_classifier.pth", ) ) # Avoid noisy/proxy-sensitive Gradio analytics requests during local demos. os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False") import gradio as gr from PIL import Image from src.pipeline.orchestrator import KuechenpassPipeline # noqa: E402 PIPELINE = KuechenpassPipeline() EXAMPLE_ORDERS = [ "Two cheeseburgers medium-rare and a side of fries please", "Eine Pizza Margherita ohne Knoblauch und einen Caesar Salad", "drei spaghetti bolognese extra parmesan", "I'd like the steak well done and a tiramisu for dessert", "ramen mit extra ei, dazu sushi mix 8 stück", ] def _format_ticket(ticket: dict) -> str: items = ticket.get("items", []) if not items: return "_(keine Items erkannt)_" lines = ["| Menge | Gericht | Station | Modifier |", "|---|---|---|---|"] for it in items: mods = ", ".join(it.get("modifiers", []) or []) or "-" lines.append( f"| {it.get('quantity', 1)} | {it.get('dish','?')} | {it.get('station','?')} | {mods} |" ) return "\n".join(lines) def _format_prep_times(items: list[dict], total: float | None) -> str: if not items: return "_ML-Modell nicht verfügbar — Pipeline läuft im Demo-Modus._" lines = ["| Gericht | Menge | Station | Geschätzte Zeit |", "|---|---|---|---|"] for p in items: lines.append( f"| {p['dish']} | {p['quantity']} | {p['station']} | {p['minutes']:.1f} min |" ) if total is not None: lines.append(f"\n**Gesamt-ETA (parallelisiert):** {total:.1f} min") return "\n".join(lines) def _format_cv(preds: list[dict] | None) -> str: if not preds: return "_(kein Foto hochgeladen)_" lines = ["| Rang | Label | Confidence |", "|---|---|---|"] for i, p in enumerate(preds, 1): lines.append(f"| {i} | {p['label']} | {p['confidence']*100:.1f} % |") return "\n".join(lines) def _format_decision(decision: dict | None) -> str: if not decision: return "_(noch keine Pass-Entscheidung)_" verdict = decision["verdict"] emoji = "✅" if verdict == "to_guest" else "🚫" label = "KANN ZUM GAST" if verdict == "to_guest" else "GANG NEU MACHEN" return ( f"### {emoji} **{label}**\n\n" f"- Grund: {decision['reason']}\n" f"- CV-Label: `{decision['predicted_label']}` ({decision['confidence']*100:.1f} %)\n" f"- Match: `{decision.get('matched_item') or '—'}`" ) def run_pipeline( order_text: str, kitchen_load: int, photo: Image.Image | None, prompt_version: str, ) -> tuple[str, str, str, str, str]: result = PIPELINE.run( order_text=order_text or "", kitchen_load=int(kitchen_load), photo=photo, prompt_version=prompt_version, order_datetime=datetime.now(), ) ticket_md = _format_ticket(result.ticket) prep_md = _format_prep_times( result.estimated_minutes_per_item, result.estimated_minutes_total ) cv_md = _format_cv(result.cv_predictions) decision_md = _format_decision(result.pass_decision) raw_json = json.dumps( { "ticket": result.ticket, "estimated_minutes_per_item": result.estimated_minutes_per_item, "estimated_minutes_total": result.estimated_minutes_total, "cv_predictions": result.cv_predictions, "pass_decision": result.pass_decision, }, indent=2, ensure_ascii=False, ) return ticket_md, prep_md, cv_md, decision_md, raw_json def build_ui() -> gr.Blocks: with gr.Blocks(title="Küchenpass-Agent", theme=gr.themes.Soft()) as demo: gr.Markdown( "# Küchenpass-Agent\n" "Free-Text-Bestellung → strukturiertes Ticket (NLP) → ETA-Prognose (ML)" " → Pass-Prüfung anhand Foto (CV).\n" "_Semesterprojekt ZHAW · KI Anwendungen FS 2026._" ) with gr.Row(): with gr.Column(scale=1): order_text = gr.Textbox( label="Bestellung (Free Text, DE/EN)", lines=4, placeholder="z.B. Zwei Burger medium und eine Pizza Margherita", ) kitchen_load = gr.Slider( minimum=0, maximum=40, step=1, value=5, label="Aktuelle Küchen-Auslastung (offene Bestellungen)", ) prompt_version = gr.Radio( choices=["v1_basic", "v2_fewshot", "v3_constrained"], value="v3_constrained", label="Prompt-Strategie", ) photo = gr.Image( label="Foto vom fertigen Gang (optional)", type="pil", height=240, ) submit = gr.Button("An den Pass schicken", variant="primary") gr.Examples( examples=[[x] for x in EXAMPLE_ORDERS], inputs=[order_text], label="Beispiel-Bestellungen", cache_examples=False, ) with gr.Column(scale=2): gr.Markdown("### 1. Geparstes Ticket (NLP)") ticket_out = gr.Markdown() gr.Markdown("### 2. Geschätzte Zubereitungszeiten (ML)") prep_out = gr.Markdown() gr.Markdown("### 3. CV-Predictions am Pass") cv_out = gr.Markdown() gr.Markdown("### 4. Pass-Entscheid") decision_out = gr.Markdown() with gr.Accordion("Rohdaten (JSON)", open=False): raw_out = gr.Code(language="json") submit.click( fn=run_pipeline, inputs=[order_text, kitchen_load, photo, prompt_version], outputs=[ticket_out, prep_out, cv_out, decision_out, raw_out], ) return demo def main() -> None: demo = build_ui() demo.launch() if __name__ == "__main__": main()