# Phase A — „Ist das Müll?"-Szenen-Gate (Open-Vocabulary) > **Stand: 2026-07-25.** Umsetzung von `VISION_ROADMAP.md §7.5.2`. Diese Datei ist > die **Spezifikation + der Contract-Vorschau** für Dev/Web/Admin — sie legt > fest, *was* Phase A ist, *wie* die API sich additiv erweitert und *wie wir es > messen, bevor irgendetwas Live-Auszahlungen berührt.* Kein Live-Contract-Change > ohne die Messung aus §5 und Founder-Sign-off. --- ## 1. Warum (in einem Satz) Das Modell hat nie gelernt, wie **Nicht-Müll** aussieht — deshalb „sieht" es zu 86 % „paper" auf einer grauen Fläche (dokumentiert in `VISION_ROADMAP.md §7.1`). Jeder solche False Positive zahlt unverdiente TrashCoin aus **und** belastet die Review-Queue. Phase A gibt dem Modell den fehlenden Weltkontext: *„das ist eine Pflanze / ein Stuhl / eine Katze — kein Müll"* — und liefert nebenbei das verkaufbare Szenen-Inventar *„3 Flaschen + 1 Katze"*. **Zwei Nutzen aus einem Modell:** 1. **FP-Gate** (Geld-Leck stopfen) — der wichtigste kurzfristige Effekt. 2. **Szenen-Inventar** (Nicht-Müll benennen + zählen) — der sichtbare „ChatGPT-kann-das-auch"-Konter und die Basis für den Verkauf an Industrien. --- ## 2. Architektur — Zwei-Schichten-Strategie (aus Roadmap §7.2, konkretisiert) ``` Bild │ ┌─────▼───────────────────────────────────────────────┐ │ Fundament (die Augen): Open-Vocabulary-Detektor │ ← von der Stange │ benennt WAS im Bild ist: bottle, cat, plant, chair, │ (YOLOE / YOLO-World) │ can, cup, bag, person, tree, road … │ └─────┬───────────────────────────────────────────────┘ │ Objekt-Inventar mit Boxen + Labels ┌─────▼───────────────────────────────────────────────┐ │ Alami-Fach-Kopf (das Gehirn): unsere Domänen-Logik │ ← nur Alami hat das │ • ist das Objekt Müll? (Waste-Vokabular-Mapping) │ (aus Flywheel + │ • welches Material-Bucket? welche Entsorgung? │ Fach-Wissen) │ • Nicht-Müll (Pflanze/Tier/Möbel) → explizit raus │ └─────┬───────────────────────────────────────────────┘ │ ┌─────▼───────────────────────────────────────────────┐ │ Fusion mit dem bestehenden YOLOv8-Seg-Detektor #13 │ │ (Material-Feinklassifikation bleibt unser Modell) │ └──────────────────────────────────────────────────────┘ ``` **Wichtig:** Der bestehende Material-Detektor (#13, live) wird **nicht ersetzt**. Das Open-Vocab-Fundament liegt *daneben* und liefert (a) das FP-Veto und (b) das Nicht-Müll-Inventar. Die Material-Feinklassen (plastic/glass/…) kommen weiter aus unserem trainierten Modell — dort ist unser Datengraben. --- ## 3. Backbone-Entscheidung (ML-Engineering-Call, begründet) | Kandidat | Open-Vocab | Lokalisiert | CPU-tauglich (HF Space) | Stack-Fit | Verdikt | |---|---|---|---|---|---| | **YOLOE / YOLO-World v2** | ✅ | ✅ (Boxen) | ✅ (S/M, ONNX) | ✅ **ultralytics 8.4 + ONNX — schon im Serving** | **gewählt** | | CLIP (Zero-Shot, ganzes Bild) | ✅ | ❌ (kein Box) | ✅ (billig) | teils | nur als Ultra-Cheap-Preview-Gate | | Grounding-DINO / OWLv2 | ✅✅ (stärker) | ✅ | ❌ (GPU/$$-Space nötig) | neuer Stack | zu schwer für jetzt | **Gewählt: YOLOE/YOLO-World v2 → ONNX.** Gründe: - **Kein neuer Stack:** Serving läuft bereits auf `ultralytics` + `onnxruntime` (siehe `requirements.txt`, `ml/serving/model_loader.py`). Das Fundament nutzt denselben Export- und Lade-Pfad wie unser Material-Modell. - **CPU-tauglich:** Die S/M-Größe läuft auf dem kostenlosen HF-Space (heutige Betriebsbasis), ohne auf einen GPU-Space ($$) zu wechseln. - **Liefert beides:** Boxen + offene Labels → FP-Gate *und* Inventar aus einem Modell. - **Training/Export schwer, Inferenz leicht** — genau wie beim Material-Modell: schwere Schritte laufen auf **Kaggle-GPU** (bestehende `ml/kaggle/`-Straße), das Serving lädt nur das exportierte ONNX. --- ## 4. API-Erweiterung — **strikt additiv** (Hard Contract §5.1) Der Mobile-Client hängt an der exakten Shape von `/predict` und `/v1/analyze` (`schemas.py`, Kommentar Zeile 6–9). **Nichts Bestehendes ändert sich.** Neu kommt ausschließlich additiv hinzu: ### 4.1 `AnalyzeResponse.summary` — zwei neue Felder (advisory) ```jsonc "summary": { "item_count": 3, "trash_detected": true, // UNVERÄNDERT in Semantik (siehe §6!) "materials": { "plastic": 3 }, "total_weight_estimate_g": 180.0, "weight_source": "material_prior_v0", // --- NEU, additiv, advisory --- "waste_likelihood": 0.94, // 0..1: Wahrscheinlichkeit, dass die Szene echt Müll enthält "scene_gate_version": "yoloe-s-20260801" } ``` ### 4.2 Neuer optionaler `scene`-Block auf `AnalyzeResponse` (default weglassbar) ```jsonc "scene": { "non_waste_objects": [ { "label": "cat", "label_de": "Katze", "confidence": 0.88, "bbox_norm": [x,y,w,h] }, { "label": "plant", "label_de": "Pflanze", "confidence": 0.79, "bbox_norm": [x,y,w,h] } ], "inventory_text": "3 Plastikflaschen · 1 Katze · 1 Pflanze", // fertige Anzeige "counts": { "waste": 3, "non_waste": 2 } } ``` ### 4.3 `domain`-Achse (aus Roadmap §2, additiv) - Heute: `domain="trash"`. Der Scene-Gate läuft **innerhalb** von `trash` mit — bestehende Clients bekommen das FP-Veto automatisch, ohne Code-Change. - Zusätzlich neu: `domain="scene"` = reine Objekt-/Szenen-Inventarisierung für externe API-Kunden (Industrie), ohne Material-Auszahlungs-Logik. **Für Dev/Web heißt das:** keine Breaking Changes. Ihr könnt die neuen Felder ignorieren, bis ihr sie nutzt. Die Feedback-v2-Payload bleibt unverändert. --- ## 5. Mess-Protokoll — **erst messen, dann verdrahten** (wie bei Lauf B) Kein Live-Deploy, bevor wir auf einem Eval-Set gemessen haben. Zwei Zahlen entscheiden: 1. **FP-Kill-Rate** — Anteil der bekannten False Positives (Müll auf leerem Boden / Natur / Innenraum), die das Gate korrekt unterdrückt. *Hoch = gut.* 2. **TP-Retention** — Anteil echter Müll-Detektionen, die das Gate **fälschlich** unterdrückt. *Muss ~0 bleiben* — ein False Negative heißt: ehrlicher Nutzer bekommt 0 TC (Hard Contract §2). **Das ist die Schmerzgrenze.** **Eval-Set (zusammenzustellen):** - **Positive:** echte Müll-Bilder aus unserem bestehenden Val-Set (#13). - **Negative:** leerer Boden/Natur (Auto-Negatives Runde 3, `harvest_negatives.py`), plus Innenraum/Tier/Möbel/Pflanze aus lizenzsauberen offenen Sets. - **Inventar-Stichprobe:** ~200 handgelabelte Bilder für die Zähl-Genauigkeit („3 Flaschen + 1 Katze" — stimmt die Zählung?). **Metriken** kommen als `summary_scene_gate.json` neben `summary_eval.json` (gleiche Konvention wie `evaluate.py`) → fließen als Snapshot in die Chronik. **Gate-Kriterium für Live-Schaltung (Vorschlag):** FP-Kill ≥ 60 % **bei** TP-Retention ≥ 99 %. Wird die Schmerzgrenze verletzt, bleibt das Gate advisory. --- ## 6. Auszahlungs-Sicherheit — der sensible Punkt (Founder + Dev) `trash_detected` speist in der App `ai_status` → beeinflusst TC. Deshalb: - **Stufe advisory (Start):** Das Gate liefert nur `waste_likelihood` + `scene`-Inventar. `trash_detected` behält **exakt** die heutige Semantik (`len(objects) > 0`). Die App zahlt wie bisher. **Null Risiko für ehrliche Nutzer.** - **Stufe aktiv (später, nur mit Messung §5 + Founder- **und** Dev-Sign-off):** Das Gate darf `trash_detected` bei sehr niedriger `waste_likelihood` überstimmen (FP-Veto). Erst wenn TP-Retention nachweislich ~100 % ist. Hard Contracts bleiben unangetastet: KI setzt nie TC-Beträge (§5.1), Submit-Pfad bleibt non-throwing (§5.2), `ai_status`-Vokabular fix (§5.3). --- ## 7. Serving-Integration (Latenz-Budget) - **Snapped Photo (`log=true`):** voller Scene-Gate läuft mit. Latenz-Aufschlag akzeptabel (einmal pro Einreichung). - **Live-Preview (`log=false`, ~1/s):** hier zählt jede ms. Optionen: (a) Gate im Preview auslassen und nur auf dem Schnappschuss laufen, oder (b) nur der ultra-billige Ganzbild-Confidence-Check im Preview. **Default: (a)** — Preview bleibt so schnell wie heute. - Modell-Load wie `model_loader.py`: ONNX-Session einmal beim Boot, Bundle-Dir über Env — das Scene-Gate-ONNX kommt als zweites Bundle neben `deploy/latest`. --- ## 8. Rollout-Stufen | Stufe | Inhalt | Berührt Live-Auszahlung? | |---|---|---| | **A0** | Prototyp + Eval-Harness offline, Backbone auf Eval-Set messen | nein | | **A1** | Scene-Gate-ONNX exportiert (Kaggle), ins Serving als 2. Bundle | nein (advisory) | | **A2** | `scene`-Block + `waste_likelihood` additiv in `/v1/analyze` live | nein (advisory) | | **A3** | FP-Veto auf `trash_detected` — **nur nach §5-Messung + Sign-off** | **ja** | --- ## 8b. A0 — Erste Messung (2026-07-25, Floor-Zahl) Erster Lauf des Mess-Harness auf **127 echten Bildern** (lizenzsauber via `harvest_negatives.py` / openverse CC0): 47 Litter-Positive, 80 Negative (32 leere Szenen + 48 Nicht-Müll-Objekte: Katze/Hund/Pflanze/Stuhl/Auto/Bank). Backbone: **YOLO-World v2 S, zero-shot, unkalibriert** (bewusst die einfachste Stufe). | τ (waste_likelihood) | TP-Retention (echten Müll behalten) | FP-Kill (Nicht-Müll ablehnen) | |---|---|---| | 0.10 | 0,79 | 0,59 | | 0.20 | 0,49 | 0,74 | | 0.40 | 0,28 | 0,94 | **Gate-Kriterium (FP-Kill ≥ 0,60 bei TP-Retention ≥ 0,99): NICHT erfüllt.** Der einzige Punkt mit ~100 % TP-Retention ist τ=0, der 0 FPs killt. **Ehrliche Einordnung — das ist ein FLOOR, nicht die echte Zahl:** - **Daten-Mismatch:** openverse-Web-Bilder („litter on street" liefert auch Luftaufnahmen/Kunstfotos) sind viel härter als unser kuratiertes, nah aufgenommenes Val-Set (#13) — das reale Einsatz-Regime. Die *maßgebliche* Messung läuft auf Kaggle mit unserem Val-Set. - **Roheste mögliche Definition:** `waste_likelihood = max Müll-Confidence`, ein globaler Threshold, S-Modell, keine Kalibrierung, kein Prompt-Tuning. - **Signal ist da:** bei τ=0,10 behalten wir 79 % des echten Mülls und killen 59 % der False Positives. Nur noch nicht an der Schmerzgrenze. **Ungenutzte Hebel (Reihenfolge für A1):** 1. **Per-Box-Veto statt Ganzbild-Score** — der eigentliche Design-Punkt (§2): das Gate vetoiert *einzelne Boxen* unseres Detektors („diese Box ist eine Katze/ leerer Boden"), nicht das ganze Bild. Inhärent präziser als der hier gemessene Ganzbild-Score. 2. **Stärkeres Backbone** (YOLO-World-L/X oder YOLOE-seg) — deutlich besserer Zero-Shot. 3. **Bessere `waste_likelihood`** — Objektzahl + Fläche + Penalty bei dominantem Nicht-Müll-Objekt, statt nur max-Confidence. 4. **Per-Prompt/Material-Thresholds + Prompt-Engineering.** **Schlussfolgerung:** Genau deshalb *advisory-first + messen-vor-schalten*. Die naive Version fasst Auszahlungen nicht an — bestätigt. Das Messgerüst steht, die Hebel sind klar. Ergebnis-JSON-Konvention: `summary_scene_gate.json`. ## 8c. A1 — Per-Box-Veto (2026-07-25, lokales Experiment) Der eigentliche Design-Hebel (§2) gebaut und lokal gemessen: unser Detektor #13 (ONNX, lokal) liefert Material-Boxen, das Open-Vocab-Modell liefert Kontext, der **Per-Box-Veto** entfernt eine Box nur, wenn ein diskretes Nicht-Müll-Objekt sie sicher überlagert (TP-sichere Grundregel, `ml/scene_gate/veto.py`). Gleiches 127-Bild-Set wie A0. | Verfahren | TP-Retention (gefeuert) | FP-Kill (gefeuert) | |---|---|---| | Ganzbild-Score (A0-Floor) | 0,28 @ FP-Kill 0,80 | — | | Per-Box-Veto v1 (Flächen vetoieren) | 0,80 | 0,81 | | **Per-Box-Veto v2 (Flächen vetoieren NICHT)** | **0,93** | 0,68 | **Zwei bestätigte Erkenntnisse:** 1. **Der Per-Box-Veto schlägt den Ganzbild-Score deutlich** (Pareto): bei ~80 % FP-Kill steigt die TP-Retention von 28 % auf 80 %. 2. **Hintergrund-Flächen dürfen nicht vetoieren.** „grass/road/floor/sky" aus der Veto-Liste zu nehmen (Müll liegt AUF ihnen) hob die TP-Retention 80 %→93 %. Preis: einige Leerboden-FPs überleben — die brauchen ein eigenes Signal (Confidence-Downgrade bei „nur Fläche, kein Objekt, keine Müll-Bestätigung"), **keinen** Flächen-Veto (der würde echten Müll auf Flächen bestrafen). **Randbefund zum FP-Problem:** Unser Detektor #13 feuerte auf **39 % zufälliger Nicht-Müll-Bilder** einen False Positive — der Per-Box-Veto (v2) entfernt zwei Drittel davon, ohne echten Müll anzufassen. **Grenzen dieses Laufs (ehrlich):** openverse-Web-Bilder, S-Backbone, Detektor-Recall auf den harten Positiven nur 32 % (Detektor-, nicht Veto-Limit). Die 99 %-Schmerzgrenze ist noch nicht erreicht. Deshalb der nächste Schritt auf UNSEREM Val-Set (Kaggle) mit stärkerem Backbone + Inspektion der Falsch-Vetos. ## 8d. Maßgebliche Messung auf Kaggle — verdrahtet (ready to dispatch) Die autoritative Messung läuft dort, wo die Daten liegen (Founder-Entscheid 2026-07-25): - **Kernel:** `ml/kaggle/scene_gate_kernel.py` — baut einen echten Val-Split (TACO-Positive + openverse-Hard-Negatives), lädt den **deployten** Detektor #13 (`deploy/latest`) + ein stärkeres Open-Vocab-Backbone und misst über `evaluate_valset()` **beide** Betriebsarten (whole_image vs per_box_veto) mit τ-Sweep. Ground Truth = Label-Datei nicht-leer. - **Workflow:** `.github/workflows/scene-gate-eval.yml` — `workflow_dispatch`, Backbone wählbar (S/M/L/X, Default **L**), pollt bis fertig, lädt `summary_scene_gate.json` als Artefakt. Gleiche Kaggle-Secrets wie der Trainings-Workflow. - **Kern-Logik lokal getestet** (`test_scene_gate.py`, Mock-Detektor/-Weltmodell): Ground-Truth-aus-Labels + Sweep + Veto-Pfad. Der Kernel ist dünner Glue. Auslösen: GitHub → Actions → „Scene-Gate Eval on Kaggle" → Run. Ergebnis-JSON fließt als Snapshot in die Chronik; erst danach A2-Entscheidung (additive API). ## 8e. Maßgebliche Messung — ERGEBNIS (2026-07-25, Kaggle GPU, Lauf #3) Backbone **yolov8l-worldv2**, deployter Detektor **#13** (`deploy/latest`), Val-Set **620 Bilder** (339 mit Müll, 281 Hard-Negatives). Kriterium: FP-Kill ≥ 0,60 **und** TP-Retention ≥ 0,99. Artefakt: `scene-gate-eval-3` (Actions-Run `30178326681`). | τ | whole_image TP / FP-Kill | per_box_veto TP / FP-Kill | |---|---|---| | 0,02 | **0,761** / 0,569 | 0,569 / **0,801** | | 0,20 | 0,596 / 0,747 | 0,569 / 0,801 | | 0,30 | 0,466 / 0,819 | 0,507 / 0,826 | | 0,40 | 0,348 / 0,883 | 0,378 / 0,883 | | 0,60 | 0,201 / 0,950 | 0,168 / 0,950 | | 0,80 | 0,027 / 0,989 | 0,083 / 0,979 | **Verdikt: `gate_passed = false` in BEIDEN Betriebsarten.** Der empfohlene Betriebspunkt fällt auf τ=0 zurück (= gar kein Gating) — es existiert **kein** Schwellwert, der gleichzeitig ≥99 % echten Müll behält und ≥60 % Nicht-Müll killt. **Drei belastbare Erkenntnisse:** 1. **Recall-Klippe am ersten Schwellwert.** Von τ=0 auf τ=0,02 bricht die TP-Retention sofort ein: whole_image 100 %→76 %, per_box 100 %→57 %. D.h. das Open-Vocab-Modell gibt **~24 % (whole_image) bzw. ~43 % (per_box)** echter Müllbilder ein waste_likelihood von ~0 — es „sieht" den Müll dort gar nicht. Das ist ein Coverage-/Recall-Problem, kein Schwellwert-Tuning-Problem. 2. **Umkehrung gegenüber dem lokalen 127-Bild-Vorexperiment (§8c).** Dort schlug per_box_veto das Ganzbild klar; auf dem autoritativen Set ist es **umgekehrt** — whole_image behält mehr echten Müll (0,76 vs 0,57 bei τ=0,02). Ursache: der Per-Box-Pfad erbt den **Recall-Deckel des Detektors #13** (findet der Detektor die Box nicht, hat der Veto nichts zu bewerten → Likelihood 0). Der Ganzbild-Pfad umgeht den Detektor und ist daher recall-robuster. 3. **Als reiner FP-Killer** (Recall-Verlust in Kauf genommen) liefert per_box bei τ≈0,02 einen realen Punkt: **80 % der Hintergrund-FPs weg, 57 % echter Müll bleibt.** Für ein hartes Auszahlungs-Gate zu verlustig, als weiches Confidence-Downgrade-Signal aber verwertbar. **Konsequenz (ML-Call):** - **Kein hartes Gate ausliefern.** §6 bleibt: strikt **advisory**, keine Auszahlungs-Kopplung. - Nächster billiger Diagnoseschritt: waste_likelihood-Histogramm über die Positiven, die 0-Signal-Fälle inspizieren. Zwei Hebel — (a) Open-Vocab-Prompts verbreitern (Vokabular deckt viele Müllarten nicht), (b) Detektor-Recall heben. - Ein **einzelner X-Backbone-Lauf** zur Absicherung, dass es keine reine Kapazitätsgrenze ist (Erwartung: hilft wenig gegen eine Coverage-Klippe). - Falls 99 %-Recall Produktziel bleibt: das Gate sollte eher ein **gelernter Kopf / die Detektor-Confidence** sein als Zero-Shot-Open-Vocab. ## 8f. 0-Signal-Diagnose — verdrahtet (2026-07-26) Der Diagnoseschritt aus §8e ist jetzt **im selben Kaggle-Durchlauf** gebaut (`evaluate_valset(collect_diagnosis=True)` + `bucket_misses()`, lokal getestet). Jeder Lauf schreibt zusätzlich `diagnosis_scene_gate.json` + Kontaktbögen der übersehenen Bilder. Die entscheidende Frage, die er beantwortet — **warum** bekommt echter Müll likelihood ~0: - **`model_saw_nothing`** (n_scene_dets = 0): Das Open-Vocab-Modell fand **gar nichts** → reine Detektions-/Recall-Lücke (Vokabular oder Backbone zu schwach). - **`saw_but_not_waste`** (Objekte erkannt, aber keins zählt als Müll) → Vokabular-/Label-Mapping-Lücke (das Ding ist da, aber nicht in unserer Müll-Sprache: Kippe, Textil, Kleinteile). - Überlagernd: `too_dark` (mittlere Helligkeit), `tiny_image`. - Für den Per-Box-Pfad zusätzlich `no_detector_box` (Detektor #13 fand nichts → belegt den in §8e vermuteten Recall-Deckel direkt an den Zahlen). Das Verhältnis `model_saw_nothing : saw_but_not_waste` entscheidet den Hebel: überwiegt Ersteres → stärkeres/anderes Backbone; überwiegt Letzteres → Prompts/ Vokabular erweitern. Feldsignal dazu: `added_items` aus dem Feedback-v2 (§ api.md) liefert laufend reale Miss-Beispiele aus der Produktverteilung. ## 8g. 0-Signal-Diagnose — ERGEBNIS (2026-07-26, L, T4) 339 Positive, Backbone yolov8l-worldv2, Detektor #13. (Ausgeführt direkt via Kaggle-Kernel v4, nicht Actions — Kontingent erschöpft; **Wichtig fürs Repro:** der Kernel MUSS mit `--accelerator NvidiaTeslaT4` laufen. Ein Lauf ohne das Flag landet auf einer P100/sm_60, die das aktuelle PyTorch nicht unterstützt → alle Inferenzen crashen → degeneriertes „100 % übersehen". Das Workflow-YAML setzt das Flag; beim Direkt-Push nicht vergessen.) | Pfad | übersehen (≤0,02) | Aufschlüsselung | |---|---|---| | **whole_image (A0)** | 81/339 (24 %) | **saw_but_not_waste 76** · model_saw_nothing 5 · too_dark 0 · tiny_image 0 | | **per_box_veto (A1)** | 146/339 (43 %) | **no_detector_box 129** · boxes_present_but_zero 17 | **Klartext — die Recall-Klippe ist KEINE Blindheit und KEIN Dunkelheits-/Größenproblem:** 1. **Ganzbild: 94 % der Misses (76/81) sind `saw_but_not_waste`.** Das Open-Vocab- Modell **sieht** die Szene, aber das Müllstück wird nicht als Müll gewertet. Der Blick auf die Kontaktbögen zeigt warum: **kleiner/verstreuter Müll in unruhigen Naturszenen** (Sand, Schlick, Gras, flaches Wasser, Laub/Muscheln) — niedrige Salienz, nicht fehlende Fähigkeit. Nur **5/339** waren echte Blindheit (Mikro-Müll auf Textur). `too_dark`/`tiny_image` = **0** → kein Faktor. → **Hebel: Vokabular/Prompts + Salienz, NICHT größeres Backbone.** Konkret: Klein-Müll-Prompts ergänzen (litter, wrapper, cigarette butt, bottle cap, plastic scrap), Kachel-/Zoom-Inferenz für kleine Objekte, conf-Schwelle senken. Ein X-Backbone würde die Salienz-Klippe kaum bewegen (nur 5 echte Blindfälle). 2. **Per-Box: 88 % der Misses (129/146) sind `no_detector_box`.** Der Engpass ist der **Recall-Deckel des Detektors #13 selbst** (kein Box → nichts zu vetoen). → **Hebel: mehr/härtere Detektor-Trainingsdaten** — genau das, was der Flywheel + `added_items` (Feedback-v2) jetzt laufend einsammeln. **Fazit:** Das „Gate fällt bei 99 % Recall durch" ist erklärt — realer Müll ist oft klein/niedrig-salient. §6 bleibt bis dahin advisory. Artefakte: `diagnosis_scene_gate.json`, `misses_saw_not_waste.jpg`, `misses_saw_nothing.jpg` (Kaggle-Kernel `alami-vision-scene-gate` v4). ## 8h. Hebel getestet: Kachel-Inferenz + Klein-Müll-Prompts — KEIN Pareto-Gewinn (2026-07-26) Hypothese aus §8g (kleiner/niedrig-salienter Müll → Auflösung ist der Hebel) sauber getestet: 2×2-Kachel-Inferenz (SAHI, `ml/scene_gate/tiling.py`) + 4 spezifische Klein-Müll-Prompts, Lauf v5 (L, T4) vs Baseline v4. **Roh** sah es gut aus (Ganzbild-Miss 24 %→11 %, `model_saw_nothing` 5→0). **Aber bei gleichem FP-Kill verschwindet der Gewinn** — es war nur ein verschobener Betriebspunkt auf derselben Kurve: | FP-Kill ≥ | Baseline TP | Kacheln TP | Δ | |---|---|---|---| | 0,60 | 0,714 | 0,752 | +0,04 | | 0,80 | 0,510 | 0,510 | **±0,00** | | 0,90 | 0,339 | 0,357 | +0,02 | | 0,95 | 0,227 | 0,159 | **−0,07** | (per_box_veto: flach ±0,003 — erwartet, der Pfad ist detektor-limitiert.) **Grund:** Kacheln heben die Müll-Confidence auf **Positiven UND Negativen** (ein Sand-/Gras-Ausschnitt triggert öfter ein spurioses „Plastikfetzen"). Die Trennschärfe bleibt gleich. **Der Limiter ist die Score-Aggregation („max Müll-Confidence im Bild"), nicht Auflösung oder Vokabular.** **Konsequenz:** Kachel-Code bleibt dormant (default `tile_grid=0`, getestet, wiederverwendbar), wird **nicht** ausgeliefert. Nächster Hebel adressiert die Trennschärfe direkt: **ein gelernter Szenen-Score** (kleine Klassifikation über Features des Open-Vocab-/Detektor-Outputs statt roher Max-Confidence), CV-validiert auf dem Val-Set — bewegt er die Pareto-Front, lohnt Serving; wenn nicht, ist Zero-Shot nachweislich gedeckelt und der Weg führt über Feld-Daten (`added_items`) + einen echten trainierten Kopf. ## 8i. Gelernter Szenen-Score — DURCHBRUCH (2026-07-26) Der Hebel aus §8h getestet: statt `waste_likelihood = max(Müll-Confidence)` ein **gelerntes lineares Logit** über 11 Output-Features (`ml/scene_gate/features.py`: max/sum/mean/count/area der Müll-Confidences, Nicht-Müll-Max/-Count, Müll-Anteil, Detektor-Box-Statistik). Trainer `ml/scripts/train_scene_score.py` (numpy, kein sklearn), **fair out-of-fold** (5-Fold-CV) auf dem Val-Set (620 Bilder). **Max TP-Retention bei gleichem FP-Kill (OOF):** | FP-Kill ≥ | Baseline (max-conf) | **Gelernt** | Δ | |---|---|---|---| | 0,60 | 0,743 | **0,971** | +0,23 | | 0,80 | 0,501 | **0,879** | **+0,38** | | 0,90 | 0,327 | **0,699** | +0,37 | | 0,95 | 0,206 | **0,581** | +0,38 | **AUC 0,73 → 0,91.** Anders als Kacheln bewegt der gelernte Score die **Pareto-Front massiv** — bei FP-Kill 0,80 steigt die TP-Retention von 50 % auf 88 %. **Damit ist bestätigt: der Limiter war die Score-Aggregation, nicht das Modell/die Auflösung.** **Feature-Wichtigkeit (interpretierbar):** stärkstes Signal ist `ov_nonwaste_max` **negativ** (−1,50) — ein *sicher* erkanntes Nicht-Müll-Objekt spricht gegen eine Auszahlung (die Veto-Intuition, gelernt), dazu `ov_waste_frac`/`det_max_conf` positiv. Das Gate sagt also: „viel Müll-Anteil + Detektor-Confidence UND kein dominantes Nicht-Müll-Objekt". **Status:** Modell-Artefakt `ml/scene_gate/scene_score.json` + reiner Scorer `features.learned_score()` (kein sklearn/torch → serving-fähig). **Ehrliche Grenze:** trainiert auf 620 TACO-Bildern = Obergrenze-Indikation, nicht die App-Verteilung; 99 %-Recall-Hardgate ist noch nicht erreicht (88 % @ FP-Kill 0,80). Aber als **advisory FP-Killer** jetzt stark genug, um Wert zu liefern — und über `added_items`-Feld-Daten neu trainierbar. **Nächster Schritt (Task 3):** den gelernten Score als `waste_likelihood` in `build_scene` verdrahten (Fallback max-conf, wenn Artefakt fehlt), dann Serving- Integration (Open-Vocab im Space) — jetzt datenbelegt lohnend. ## 8j. Serving-Machbarkeit — GEMESSEN, nicht vermutet (2026-07-26) Die offene Frage war: der Space fährt **`onnxruntime` auf `cpu-basic`**, kein torch. Läuft der Gate dort überhaupt? Antwort: **ja.** **1. ONNX-Export funktioniert.** YOLO-Worldv2 backt bei `set_classes()` die Text-Embeddings ein. Weil unser Vokabular **fest** ist, braucht die Laufzeit danach **kein CLIP und kein torch** — der Output ist ein normales Detektions- Modell `(1, 72, 8400)` = 4 Box-Koordinaten + **68 Klassen** (= unsere Prompts). Script: `ml/scripts/export_scene_gate_onnx.py` → Bundle im **gleichen Format wie `deploy/latest`**, damit `ModelBundle` es ohne neuen Inferenz-Code lädt (§7). **2. Latenz auf 2 Threads (cpu-basic-Näherung), gemessen:** | | Modell | Latenz (median) | |---|---|---| | Detektor #13 (heute live) | 13 MB | **106 ms** | | Szenen-Gate **S** (neu) | 50 MB | **224 ms** | | **Summe pro Schnappschuss** | | **~330 ms** | Verdikt: **tragbar** — aber nur auf dem Schnappschuss-Pfad (`log=true`), wie §7 ohnehin vorsieht. Der Live-Preview bleibt unangetastet. **L ist für CPU-Serving zu schwer** → Serving-Kandidat ist **S**; die §8i-Messung lief auf L, der Score muss für S neu trainiert/geprüft werden (offen, siehe unten). **3. End-to-End verifiziert** (Detektor #13 + Gate + gelernter Score auf echten Fotos) — und der §8i-Gewinn wird dabei sichtbar: Auf einem der `saw_but_not_waste`- Miss-Bilder findet das Open-Vocab-Modell **null** Müll-Objekte (`counts.waste=0`), die alte max-conf-Baseline hätte also **0.0** gesagt. Der gelernte Score zieht aus Detektor-Boxen + Kontext **0,61**; eine leere Fläche bekommt **0,22**. **Nebenbefund + Fix:** `ModelBundle.predict()` stürzte bei **reinen Detektions- Bundles** ab (`mask_coef` ist dann `None`, wurde aber indiziert). Fiel nie auf, weil Detektor #13 einen Seg-Kopf hat. Behoben — betrifft jedes maskenlose Bundle. **Offen vor dem Ausrollen:** - ~~Score für **S** validieren~~ → **erledigt, siehe §8k.** - `scene`-Block-Kontrakt: Rückmeldung von PO/Monorepo (`docs/SCENE_BLOCK_CONTRACT.md`). - Bundle-Auslieferung in den Space (Größe/Boot-Zeit) + Env-Schalter. ## 8k. Hält der Score mit dem servebaren Backbone? — JA (2026-07-26) §8i wurde auf **L** gemessen, Serving läuft aber auf **S** (CPU-Budget, §8j). Deshalb S-Feature-Dump + Retrain, gleiche Methode. **Fairer Vergleich:** die 281 Negative des L-Laufs sind eine Teilmenge der 480 des S-Laufs, Positive identisch → beide auf **exakt denselben 620 Bildern** ausgewertet. | Konfiguration | AUC | TP @ FP-Kill 0,80 | |---|---|---| | L + max-conf (Ausgangslage) | 0,73 | 0,50 | | L + gelernt (§8i) | 0,91 | **0,88** | | S + max-conf | 0,66 | 0,39 | | **S + gelernt** (ausgeliefert) | 0,85 | **0,77** | **Verdikt: S trägt.** Der entscheidende Vergleich ist nicht S-gelernt vs L-gelernt (−0,11), sondern **S + gelernt (0,77) vs L + max-conf (0,50)**: das *billige, servebare* Backbone mit gutem Score schlägt das *teure* mit naivem Score deutlich. Serving mit S ist damit verantwortbar — Gate-Kosten 224 ms statt eines nicht-servebaren L. **Wichtig (Fallstrick):** Die Features sind **backbone-spezifisch** — ein auf L trainierter Score passt nicht auf S-Verteilungen. Das ausgelieferte Artefakt `ml/scene_gate/scene_score.json` ist deshalb auf **S**-Features trainiert und trägt das Feld `backbone`; `train_scene_score.py --backbone` schreibt es mit. **Wer das Serving-Backbone wechselt, muss den Score neu trainieren.** **Ehrliche Einordnung:** 0,77 TP-Retention bei 80 % FP-Kill heißt: als **advisory FP-Killer** stark (4 von 5 Fehl-Auszahlungen abfangbar, 77 % echter Müll unbehelligt, Rest in Human-Review). Für ein **hartes** Auto-Credit-Gate (≥99 % Recall) reicht es weiterhin **nicht** — dafür braucht es Feld-Daten (`added_items`) und einen trainierten Kopf statt Zero-Shot. --- ## 9. Offene Entscheidungen (Founder) 1. **Erst-Fokus:** FP-Killer zuerst (Geld-Leck, messbar, risikoarm) — oder das sichtbare Voll-Inventar zuerst (Demo-Wow, „ChatGPT-Konter")? *Empfehlung: ein Modell baut beides; wir **validieren + schalten den FP-Nutzen zuerst**, das Inventar läuft additiv mit.* 2. **Auszahlungs-Stufe:** Bleibt es bis zur Messung strikt advisory (§6)? *Empfehlung: ja.*