alami-vision-api / docs /PHASE_A_SCENE_GATE.md
alami-ci
Deploy from alami-eco/alami-trash-ai@aee69796b70947e95efdb9c7483fa52f8d3b4520
76838d6
|
Raw
History Blame Contribute Delete
28.9 kB
# 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.*