lederyou's picture
Upload app.py with huggingface_hub
699b039 verified
Raw
History Blame Contribute Delete
6.71 kB
"""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()