Spaces:
Sleeping
Sleeping
| """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() | |