# Alami Vision API — Referenz Version 2.0.0 · FastAPI · ONNX Runtime (YOLOv8-seg) Die API liefert **Signale, niemals Geldbeträge**. TrashCoin-Berechnung ist server-authoritativ in der Alami-App (siehe `AILLMHANDOFF.md` §5 im Mobile-Repo). ## Authentifizierung Optional per API-Key. Wird über die Env-Variable `ALAMI_API_KEYS` (kommasepariert) aktiviert; ohne diese Variable ist die API offen (Dev-Modus / aktueller Mobile-Client). Header: `x-api-key: `. `/healthz` ist immer offen. > **Achtung Rollout-Reihenfolge:** Die Mobile-App sendet heute noch keinen Key. > Keys erst erzwingen, wenn der App-Client den Header mitschickt — sonst werden > ehrliche Nutzer nicht bezahlt (`ai_status='unavailable'` ⇒ 0 TC). ## Endpoints ### `GET /healthz` Liveness + Modell-Metadaten. ```json { "ok": true, "version": "2.0.0", "imgsz": 640, "names": ["plastic","paper","glass","metal","organic","ewaste","other"], "conf_thr": 0.35, "supabase": false, "auth": "open", "timestamp": "…" } ``` ### `POST /predict` — Legacy-Contract (Mobile-App, stabil) Request: ```json { "image_url": "https://…", "user_id": "uuid|null", "model_hint": "alami-mobile:v1" } ``` Response: ```json { "model_version": "v20251018-220439", "inference_ms": 77, "prediction_id": "uuid", "predictions": [ { "xyxy": [x1,y1,x2,y2], "cls": 0, "conf": 0.86, "label": "plastic", "raw_label": "plastic" } ] } ``` - `label` = Material-Bucket (`plastic|paper|glass|metal|organic|ewaste|other`) — darauf keyed der Economy-Gate der App. - `raw_label` = originale Modellklasse (Nachvollziehbarkeit; bei zukünftigen feingranularen Modellen, z. B. TACO-60 oder Marken-SKUs, weicht sie ab). - Koordinaten in **Originalbild-Pixeln**. ### `POST /v1/analyze` — Standalone Vision API (neu) Superset von `/predict`: Materialzusammenfassung, Stückzahl, Gewichtsschätzung pro Objekt. Request: ```json { "image_url": "https://…", "user_id": "uuid|null", "domain": "trash" } ``` Response: ```json { "prediction_id": "uuid", "model_version": "v20251018-220439", "domain": "trash", "inference_ms": 77, "image": { "width": 800, "height": 600 }, "objects": [ { "label": "metal", "raw_label": "metal", "confidence": 0.91, "bbox_xyxy": [12.0, 40.5, 180.2, 300.0], "area_fraction": 0.105, "weight_estimate_g": 15.0, "weight_source": "material_prior_v0" } ], "summary": { "item_count": 1, "trash_detected": true, "materials": { "metal": 1 }, "total_weight_estimate_g": 15.0, "weight_source": "material_prior_v0" } } ``` **`log`** (default `true`): Auf `false` setzen für Live-Kamera-**Preview**-Frames — das Frame wird analysiert, aber **nicht** persistiert (kein `trash_predictions`/JSONL-Eintrag). So flutet der ~1/s-Preview-Strom die Trainingsdaten nicht; nur das final gesnappte Foto wird mit `log=true` geloggt. **`source`** (optional): Kontext-Tag, z. B. `"product-scan"`. Geloggte Zeilen mit Nicht-Litter-Source hält der Flywheel-Sync in einem **separaten Pool** (`predictions_other_sources.jsonl`) — Regal-/Produktfotos mischen nie ins Litter-Training, bleiben aber für die spätere Marken-Erkennung erhalten. Litter-Pool = `source` fehlt oder `"alami-mobile"`. `domain` ist heute nur `"trash"`; das Feld reserviert die API-Fläche für weitere visuelle Domänen (siehe Roadmap), ohne dass Clients später migrieren müssen. **Gewicht:** `material_prior_v0` ist eine ehrliche Heuristik (Median-Stückgewicht pro Material, überschreibbar via `deploy//weight_priors.json`). Sobald der gelernte Weight-Head live ist, steht in `weight_source` die Modellversion. Konsumenten MÜSSEN `weight_source` auswerten, bevor sie Gewichte anzeigen oder verrechnen. Jedes Objekt trägt zusätzlich Felder für **Kamera-Overlays** (siehe `docs/MOBILE_AGENT_HANDOFF.md` §4b): - `bbox_norm`: `[x, y, w, h]` normiert in 0..1 — Box ohne Kenntnis der Bildgröße zeichnen - `label_de`: deutsches Anzeige-Label (z. B. „Glas") - `color`: stabile Overlay-Farbe (Hex) für das Material ### `POST /v1/analyze/upload` Wie `/v1/analyze`, aber Multipart-Upload statt URL (für API-Kunden ohne öffentliche Bild-URLs). Felder: `file` (Bild, max. 10 MB), `user_id?`, `domain?`. ### `GET /v1/product/{barcode}` Produkt-Nachhaltigkeits-Lookup (Open Food/Products/Beauty Facts): Eco-Score, Verpackung → Material-Bucket + deutscher Entsorgungs-Guide, Litter-Gewicht. Immer HTTP 200 mit `status: found | not_found | unavailable` (400 nur bei ungültigem Barcode). 24-h-Cache serverseitig. **Getrennt vom Modell-Pfad** — kein Einfluss auf `/predict`. Client MUSS `attribution` anzeigen (ODbL). Details + UI-Anleitung: `MOBILE_AGENT_HANDOFF.md` §4d. ### `GET /v1/materials` Statische Anzeige-Metadaten aller 7 Materialien (`bucket`, `label_de`, `color`). Für konsistente Kamera-Overlays — App holt einmal und cacht. ```json { "materials": [ {"bucket":"plastic","label_de":"Plastik","color":"#2563eb"}, {"bucket":"glass","label_de":"Glas","color":"#059669"}, ... ] } ``` ### `POST /feedback` — Human Reinforcement Fire-and-forget-Korrekturen; Trainingsrohstoff für das Flywheel (`ml/scripts/ingest_feedback.py`). ```json { "prediction_id": "uuid", "corrected_type": "plastic", "corrected_weight_kg": 0.5, "notes": "war eine PET-Flasche", "source": "alami-mobile", "corrected_items": [ { "index": 0, "action": "reject" }, { "index": 1, "action": "relabel", "corrected_label": "metal", "corrected_weight_g": 15.0 } ], "added_items": [ { "label": "glass", "point": { "x": 0.42, "y": 0.61 } }, { "label": "plastic", "box": { "x": 0.1, "y": 0.1, "w": 0.2, "h": 0.3 }, "count": 2 } ], "reasons": ["too_dark", "occluded"] } ``` Alle Felder außer `prediction_id` optional; `corrected_weight_kg` ∈ [0.001, 20]. `corrected_items` referenziert per `index` die Objekte der zugehörigen Prediction (objektgenaue Labels für zukünftiges Training). **Feedback v2 (Mobile-Kontrakt #141) — additiv, spiegelt den Live-Client:** - `corrected_items[].action` = `confirm` | `relabel` | `reject` — macht Bestätigungen und **False Positives** zu expliziten Signalen (fehlt → Alt-Verhalten „relabel"). - `added_items` — Objekte, die die KI **übersah** (Recall-Signal). `point`/`box` normalisiert 0..1 zum hochgeladenen Bild; ein verorteter Treffer ist ein trainierbares Detektor-Label, ein bloßes `count` nur eine Zählung. - `reasons` — maschinenlesbare Fehler-Chips: `too_dark` · `blurry` · `occluded` · `unknown_object` · `not_trash` · `unsure_skip` (offene Liste, vorwärtskompatibel). Persistenz nach Supabase `trash_predictions` (`added_items`, `feedback_reasons`; `action` reist im `corrected_items`-JSONB). Fehlt eine Spalte, wird **nur diese** weggelassen — Migration siehe `docs/FLYWHEEL.md §3`. ## Environment-Variablen | Variable | Default | Bedeutung | |---|---|---| | `ALAMI_AI_BUNDLE` | `deploy/latest` | Modell-Bundle (model.onnx, names.json, postprocess_config.json) | | `ALAMI_API_KEYS` | *(leer = offen)* | Kommaseparierte API-Keys | | `ALAMI_FEEDBACK_DIR` | `feedback_logs` | JSONL-Logs (predictions/feedback) | | `ALAMI_CORS_ORIGINS` | `*` | Kommaseparierte CORS-Origins | | `ALAMI_DEBUG` | *(aus)* | `1` ⇒ Debug-Logs **und** Debug-Endpoints | | `ALAMI_INSECURE_FETCH` | *(aus)* | `1` ⇒ TLS-Verify beim Bild-Fetch aus (nur lokal!) | | `SUPABASE_URL` / `SUPABASE_SERVICE_ROLE_KEY` | *(leer)* | Optionales Prediction-Logging nach Supabase (`trash_predictions`) | ## Fehlerverhalten - Nicht erreichbare/zu große Bilder ⇒ 400/408, kein Absturz. - Der Mobile-Client behandelt jeden Fehler als `unavailable` und lässt die Submission mit `needs_review` durch — die API darf den Submit-Pfad niemals blockieren (Hard Contract §5.2). ## Tests ```bash pip install -r requirements.txt pytest httpx pytest tests/ -v ```