Spaces:
Running
Running
| # 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: <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/<bundle>/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 | |
| ``` | |