# Calling Douanier (the nech.pl `audio` API)

Base URL: **`https://api.nech.pl/audio/v1`** · Auth: **`Authorization: Bearer <token>`**
(your platform token, provisioned by SRE for your tenant). Interactive docs +
schema: `https://api.nech.pl/audio/v1/docs` · `…/openapi.json` (snapshot:
[`openapi.json`](./openapi.json)).

Every analysis is a `multipart` POST with one `file` field (an audio clip) and
returns JSON. Identical audio is **cached** — the `X-Douanier-Cache: hit|miss`
header (and `result._cache` in the TS client) tells you which.

## TypeScript client (zero deps)

Drop [`douanier.ts`](./douanier.ts) into your project (Node 18+, Vercel
Functions, or Edge — it only uses global `fetch`/`FormData`/`Blob`).

```ts
import { Douanier } from "./douanier";

const dz = new Douanier({ token: process.env.DOUANIER_TOKEN! });

// a clip from anywhere → a Blob
const clip = await fetch(trackUrl).then(r => r.blob());

const mood = await dz.emotion(clip);     // { emotion: { valence, arousal, top, … } }
const spec = await dz.spectrum(clip, { bands: 64, frames: 200 });  // FFT-as-a-service
const wave = await dz.waveform(clip, { bins: 800 });               // render-ready envelope
const loud = await dz.loudness(clip);    // { lufs_integrated, gain_to_target_db, … }
if (mood._cache === "hit") { /* came free from the cache */ }
```

### Vercel Function example

```ts
// app/api/mood/route.ts  (Next.js App Router, Node runtime)
import { Douanier } from "@/lib/douanier";

export async function POST(req: Request) {
  const clip = await req.blob();
  const dz = new Douanier({ token: process.env.DOUANIER_TOKEN! });
  const { emotion } = await dz.emotion(clip);
  return Response.json(emotion);
}
```

## Scopes

Access is by the platform's hierarchical scope convention. The scope a route
needs is **derived from its path**: `/audio/v1/analyze/emotion` requires
`api:audio:analyze:emotion`, `/audio/v1/grade` requires `api:audio:grade`, etc.
A grant of **`api:audio:*`** covers the whole audio API, and **`api:*`** covers
every API on the platform — so most tokens just carry one of those. (Full
convention: the platform's `RECIPE.md` / `scopes.py`.)

## Endpoints (required scope in parens)

| Method | Path | Returns | required scope |
|---|---|---|---|
| GET | `/healthz` | status + engine availability | — (public) |
| POST | `/analyze/emotion` | valence/arousal + top emotions | `api:audio:analyze:emotion` |
| POST | `/features` | ~35-dim feature stack | `api:audio:features` |
| POST | `/analyze/samples` | per-sample EDA + measured role | `api:audio:analyze:samples` |
| POST | `/grade` | mechanical loop quality (tier + flags) | `api:audio:grade` |
| POST | `/onsets` | onset times + tempo | `api:audio:onsets` |
| POST | `/waveform` | `[min,max]` peaks + RMS envelope | `api:audio:waveform` |
| POST | `/spectrum` | downsampled spectrogram (FFT-as-a-service) | `api:audio:spectrum` |
| POST | `/loudness` | BS.1770 LUFS + peak + gain-to-target | `api:audio:loudness` |
| POST | `/naming` | convention-compliant sample name | `api:audio:naming` |
| POST | `/analyze` | emotion + features + sample in one call | `api:audio:analyze` |
| POST | `/separate` | **503** until a GPU runner | `api:audio:separate` |

(All covered by `api:audio:*` or `api:*`.)

## curl

```bash
curl -s -H "Authorization: Bearer $DOUANIER_TOKEN" \
  -F file=@clip.wav https://api.nech.pl/audio/v1/loudness | jq
```

## Regenerating the snapshot / a full client

`openapi.json` here is a checked-in snapshot. To codegen a client in any language:

```bash
npx @openapitools/openapi-generator-cli generate \
  -i armada/api/clients/openapi.json -g typescript-fetch -o ./generated
```

The hand-written `douanier.ts` is the lean, dependency-free alternative — prefer
it unless you specifically want generated models.
