alami-ci
Deploy from alami-eco/alami-trash-ai@aee69796b70947e95efdb9c7483fa52f8d3b4520
76838d6
|
Raw
History Blame Contribute Delete
7.87 kB
# 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
```