# Douanier — ParVagues Audio-Intelligence API 🛂

The customs gate over the fleet's audio-intelligence engines: one self-hosted,
token-authed, async HTTP API that turns a URL or a clip into **stems, loops,
emotion reads, sample analysis, and features**. Named after Le Douanier Rousseau.

Productizes the [Foundry](../../tools/foundry/) + the tide-table "ears"
(`emotion_ontology`, `sample_semantics`, the feature stack) for **hexa** (Shipow)
and future seats. Full design + decisions: memory `project_douanier_api`, EPIC
task **#21**, and [`SRE.md`](./SRE.md) for the hosting handoff.

## Layout

```
armada/api/
  app.py          FastAPI app — /v1 router, /v1/healthz, /v1/analyze/emotion
  config.py       paths · dev token · limits (env-overridable)
  engines/        thin adapters over the fleet libs (lazy heavy imports)
    ears.py       emotion read = sample_semantics embed → emotion_ontology.score
  tests/          smoke tests (no CLAP/torch needed — engine call is mocked)
  requirements.txt   API-layer deps only; ML stack inherited from system venv
```

## Run (dev)

```bash
python3 -m venv --system-site-packages ~/.virtualenvs/douanier   # once
~/.virtualenvs/douanier/bin/pip install -r requirements.txt      # once

cd armada/api
DOUANIER_DEV_TOKEN=$(openssl rand -hex 16) \
  ~/.virtualenvs/douanier/bin/uvicorn app:app --port 8780
```

- `GET  /v1/healthz` — status + engine availability
- `GET  /v1/me` — echo the calling token's account + scopes (onboarding check)
- `POST /v1/analyze/emotion` — `multipart` audio file → `{valence, arousal, top…}`
  (Bearer token with the `emotion` or `*` scope)
- `GET  /v1/docs` — Swagger UI

### Emotion engine selection

`DOUANIER_EMOTION_ENGINE` picks the V/A backend (same `/v1` response shape either way):

- `clap` (default, dev) — CLAP/torch, rich, ~4 GB, needs the GPU/dev box.
- `light` (prod / erable) — librosa-heuristic V/A, **no torch**, fits a ~2 GB-RAM
  container. Arousal from energy/tempo/brightness, valence from major/minor mode;
  mapped onto the same emotion anchors. A baseline (Essentia/CLAP-grade tier lands
  later behind the same shape — see task #37).

```bash
DOUANIER_EMOTION_ENGINE=light ~/.virtualenvs/douanier/bin/uvicorn app:app --port 8780
```

### Tokens (`douanier` CLI)

Real tokens live in SQLite (only their sha256 hash is stored). The dev one-liner
above uses `DOUANIER_DEV_TOKEN` as a local-only wildcard bootstrap; for anything
real, mint:

```bash
~/.virtualenvs/douanier/bin/python douanier.py token mint --account hexa --scopes '*' --ttl 31
~/.virtualenvs/douanier/bin/python douanier.py token list
~/.virtualenvs/douanier/bin/python douanier.py token revoke <id>
```

Scopes are space-separated engine names (`emotion features samples separate loops`)
or `*`. `--ttl never` for no expiry; `--metered` to enforce quota (default
unlimited; quota enforcement lands in #24).

```bash
curl -s -H "Authorization: Bearer $DOUANIER_DEV_TOKEN" \
  -F file=@clip.wav http://127.0.0.1:8780/v1/analyze/emotion | jq
```

## Test

```bash
cd armada/api && ~/.virtualenvs/douanier/bin/python -m pytest tests/ -q
```

## Status

Done: scaffold + emotion endpoint (#22/#36), **SQLite bearer-token auth + scopes
+ `douanier` CLI (#23)**, **content-addressed cache (#26)** — a repeat clip
returns in ~2ms instead of paying CLAP again (measured 5663× on a real run;
`X-Douanier-Cache: hit`). Queued, building *under* this without changing the
public shape: per-token quota (#24), async jobs + worker (#25), signed artifact
URLs (#27), `/features`+`/samples` hardened (#28),
`/sources`→`/separate`→`/loops` heavy chain (#29–#31), OpenAPI client (#32),
ops/runbook (#34).

> **Why a venv with `--system-site-packages`?** The ears/Foundry engines need the
> multi-GB ML stack (torch/CLAP/librosa/demucs) already installed system-wide on
> the box; the venv reuses it and only adds FastAPI/uvicorn. Arch's PEP-668 lock
> is why we don't `pip install` into system Python.
