Commit ed77ee9e by PLN (Algolia)

feat(audio-api): @nech/api — fully-generated TS client + Verdaccio runbook (#44)

The hand-written nech.ts was always interim; this replaces it with a client
GENERATED from the OpenAPI spec, so the SDK can never drift from the API.

Source-of-truth fixes (the spec drives the ergonomics):
- Clean operationIds on all 13 routes (operation_id="analyzeEmotion" etc.) so
  the generator emits `audio.analyzeEmotion({file})`, not the default
  `analyzeEmotionAnalyzeEmotionPost`. Also cleans /docs.
- One router tag ["audio"] so the generated class is AudioApi, not DefaultApi.
- refresh_openapi.py: the canonical snapshot dump. app.openapi() omits the
  public `servers` block (only injected when served behind root_path), and the
  old README recipe silently dropped it — test_openapi_snapshot guards it, so
  the refresh now injects https://api.nech.pl/audio/v1 itself.

The package (@nech/api, clients/nech-api/):
- typescript-fetch generator → src/audio/ (checked in; regenerate via
  codegen.sh), bundled with tsup to a single ESM file + .d.ts.
- Umbrella package, per-domain SUBPATH exports: `import {AudioApi} from
  '@nech/api/audio'` — one install/version, tree-shakeable, geo/iris slot in as
  siblings later (codegen.sh + exports map have the stubs).
- Build via tsup not bare tsc: the generated code uses extensionless relative
  imports (./runtime) that Node ESM can't resolve from plain tsc output;
  bundling sidesteps it entirely. Verified end-to-end: `@nech/api/audio`
  resolves through the exports map, 13/13 methods present.
- publishConfig + .npmrc point the @nech scope at https://npm.nech.pl.

VERDACCIO.md: copy-paste runbook to stand the private registry up on erable
(container :4873, nginx TLS vhost for npm.nech.pl, seed publisher + lock
signups, publish). NOT yet run — `ssh erable` failed with publickey from the
build host; needs the key loaded. DNS for npm.nech.pl is already set.

README reframed: @nech/api is the official client; nech.ts stays documented as
the working drop-in until the registry is live, then it's retired. Scrubbed a
$DOUANIER_TOKEN codename leak in the curl example. 67 API tests still green.
parent 12e03903
# npm.nech.pl — private npm registry (Verdaccio) on erable
Hosts the `@nech/*` packages (the generated API clients, `@nech/api`). Public
deps proxy through to npmjs, so a project using `@nech/api` still resolves
`react` etc. normally. Same shape as `api.nech.pl`: a loopback container behind
the nginx TLS edge.
DNS `npm.nech.pl` → erable is set. erable = `pln@nech.pl`, Docker, nginx in
`/etc/nginx/sites-enabled/`, passwordless sudo. Run as `pln`.
> **Status:** runbook ready, NOT yet deployed — needs an ssh session to erable
> (`ssh erable` failed with `Permission denied (publickey)` from the build host;
> load the key first). Everything below is copy-paste once you're on erable.
## 1. Verdaccio container (loopback :4873)
```bash
mkdir -p ~/srv/verdaccio/{storage,conf}
# config — auth required to publish, @nech is local, everything else proxies npmjs
cat > ~/srv/verdaccio/conf/config.yaml <<'YAML'
storage: /verdaccio/storage
auth:
htpasswd:
file: /verdaccio/storage/htpasswd
max_users: -1 # -1 after seeding = no open self-registration
uplinks:
npmjs:
url: https://registry.npmjs.org/
packages:
'@nech/*':
access: $authenticated
publish: $authenticated
unpublish: $authenticated
'**':
access: $authenticated
publish: $authenticated
proxy: npmjs
server:
keepAliveTimeout: 60
log: { type: stdout, format: pretty, level: info }
YAML
docker run -d --name verdaccio --restart unless-stopped \
-p 127.0.0.1:4873:4873 \
-v ~/srv/verdaccio/storage:/verdaccio/storage \
-v ~/srv/verdaccio/conf:/verdaccio/conf \
verdaccio/verdaccio:6
```
(If Docker is the old 19.03 that needs it for other images, Verdaccio's node
image is fine without `seccomp=unconfined` — it's not the glibc/clone3 case.)
## 2. nginx vhost + TLS
```bash
sudo certbot certonly --webroot -w /var/www/challenges -d npm.nech.pl
sudo tee /etc/nginx/sites-available/npm.nech.pl.conf >/dev/null <<'NGINX'
server {
listen 80; listen [::]:80;
server_name npm.nech.pl;
include includes/acme-challenge;
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl http2; listen [::]:443 ssl http2;
server_name npm.nech.pl;
ssl_certificate /etc/letsencrypt/live/npm.nech.pl/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/npm.nech.pl/privkey.pem;
ssl_protocols TLSv1.2;
client_max_body_size 50m; # tarballs
location / {
proxy_pass http://127.0.0.1:4873;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
}
}
NGINX
sudo ln -sf /etc/nginx/sites-available/npm.nech.pl.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```
## 3. Seed the publisher user, then lock signups
```bash
npm adduser --registry https://npm.nech.pl # create pln
# then set max_users: -1 (already above) and restart so no one else can self-register
docker restart verdaccio
```
## 4. Publish @nech/api
```bash
cd armada/api/clients/nech-api
npm login --registry https://npm.nech.pl
npm publish # prepublishOnly runs the build
```
Consumers (hexa): `echo '@nech:registry=https://npm.nech.pl' >> .npmrc && npm i @nech/api`.
......@@ -44,7 +44,8 @@ app.add_middleware(
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"],
)
v1 = APIRouter() # routes mounted at root; the gateway owns the /audio/v1 prefix.
v1 = APIRouter(tags=["audio"]) # routes mounted at root; the gateway owns the /audio/v1 prefix.
# tag => generated client class is AudioApi (not DefaultApi).
# ── observability (#37) — a counter the erable Prometheus scrapes ───────────────
_REQ_COUNTS: dict[tuple[str, int], int] = {}
......@@ -113,7 +114,7 @@ def require_auth(request: Request) -> auth.Principal:
# ── health ──────────────────────────────────────────────────────────────────
@v1.get("/healthz")
@v1.get("/healthz", operation_id="healthz")
def healthz():
ok, hint = ears.available()
return {
......@@ -168,7 +169,7 @@ def metrics():
return Response("\n".join(lines) + "\n", media_type="text/plain; version=0.0.4")
@v1.get("/me")
@v1.get("/me", operation_id="whoami")
def whoami(principal: auth.Principal = Depends(require_auth)):
"""Echo the caller's gateway-resolved identity — handy for onboarding smoke tests."""
return {"account": principal.account, "scopes": sorted(principal.scopes),
......@@ -206,7 +207,7 @@ def _cached(kind: str, params: dict, data: bytes, filename: str, compute, respon
return cid, result
@v1.post("/analyze/emotion")
@v1.post("/analyze/emotion", operation_id="analyzeEmotion")
async def analyze_emotion(response: Response, file: UploadFile = File(...),
_p: auth.Principal = Depends(require)):
"""Upload a short audio clip → valence/arousal + top emotions. Cached (#26):
......@@ -217,7 +218,7 @@ async def analyze_emotion(response: Response, file: UploadFile = File(...),
return {"filename": file.filename, "content_id": cid, "emotion": result}
@v1.post("/features")
@v1.post("/features", operation_id="features")
async def features(response: Response, file: UploadFile = File(...), rhythm: bool = True,
_p: auth.Principal = Depends(require)):
"""Upload a clip → the ~35-dim audio feature stack (spectral moments, MFCCs,
......@@ -229,7 +230,7 @@ async def features(response: Response, file: UploadFile = File(...), rhythm: boo
return {"filename": file.filename, "content_id": cid, **result}
@v1.post("/analyze/samples")
@v1.post("/analyze/samples", operation_id="analyzeSamples")
async def analyze_samples(response: Response, file: UploadFile = File(...), name: str = "",
_p: auth.Principal = Depends(require)):
"""Upload a one-shot/loop → per-sample EDA + MEASURED role (percs|bass|melodic|
......@@ -242,7 +243,7 @@ async def analyze_samples(response: Response, file: UploadFile = File(...), name
return {"filename": file.filename, "content_id": cid, **result}
@v1.post("/onsets")
@v1.post("/onsets", operation_id="onsets")
async def onsets(response: Response, file: UploadFile = File(...),
_p: auth.Principal = Depends(require)):
"""Upload audio → onset hit times (s) + tempo + onset rate. The cheapest tier
......@@ -252,7 +253,7 @@ async def onsets(response: Response, file: UploadFile = File(...),
return {"filename": file.filename, "content_id": cid, **result}
@v1.post("/waveform")
@v1.post("/waveform", operation_id="waveform")
async def waveform(response: Response, file: UploadFile = File(...), bins: int = 800,
_p: auth.Principal = Depends(require)):
"""Upload audio → render-ready waveform: per-bin [min,max] + a 0..1 RMS energy
......@@ -263,7 +264,7 @@ async def waveform(response: Response, file: UploadFile = File(...), bins: int =
return {"filename": file.filename, "content_id": cid, **result}
@v1.post("/analyze")
@v1.post("/analyze", operation_id="analyzeAll")
async def analyze_all(response: Response, file: UploadFile = File(...),
_p: auth.Principal = Depends(require)):
"""One call → emotion + features + sample role for a clip (fewer round-trips
......@@ -279,7 +280,7 @@ async def analyze_all(response: Response, file: UploadFile = File(...),
return {"filename": file.filename, "content_id": cid, **result}
@v1.post("/grade")
@v1.post("/grade", operation_id="grade")
async def grade(response: Response, file: UploadFile = File(...), role: str = "",
_p: auth.Principal = Depends(require)):
"""Upload a loop/one-shot → the Foundry's MECHANICAL quality grade: composite
......@@ -293,7 +294,7 @@ async def grade(response: Response, file: UploadFile = File(...), role: str = ""
return {"filename": file.filename, "content_id": cid, "grade": result}
@v1.post("/loudness")
@v1.post("/loudness", operation_id="loudness")
async def loudness(response: Response, file: UploadFile = File(...),
_p: auth.Principal = Depends(require)):
"""Upload audio → BS.1770 integrated LUFS + sample/true peak + crest + the
......@@ -304,7 +305,7 @@ async def loudness(response: Response, file: UploadFile = File(...),
return {"filename": file.filename, "content_id": cid, **result}
@v1.post("/spectrum")
@v1.post("/spectrum", operation_id="spectrum")
async def spectrum(response: Response, file: UploadFile = File(...),
bands: int = 64, frames: int = 200, mel: bool = True,
_p: auth.Principal = Depends(require)):
......@@ -319,7 +320,7 @@ async def spectrum(response: Response, file: UploadFile = File(...),
return {"filename": file.filename, "content_id": cid, **result}
@v1.post("/naming")
@v1.post("/naming", operation_id="naming")
async def naming(response: Response, file: UploadFile = File(...), index: int = 1,
_p: auth.Principal = Depends(require)):
"""Upload a sample → a convention-compliant name (NN_role_character) derived
......@@ -338,7 +339,7 @@ async def naming(response: Response, file: UploadFile = File(...), index: int =
# ── #37 separation seam — no CPU path on erable; env-selected GPU backend later ──
@v1.post("/separate")
@v1.post("/separate", operation_id="separate")
async def separate(_p: auth.Principal = Depends(require)):
"""Stem separation (demucs/roformer). erable has no GPU and no CPU-viable path,
so this is a deliberate 503 until a GPU-runner backend is wired (env-selected,
......
......@@ -9,48 +9,52 @@ Every analysis is a `multipart` POST with one `file` field (an audio clip) and
returns JSON. Identical audio is **cached** — the `X-Nech-Cache: hit|miss`
header (and `result._cache` in the TS client) tells you which.
## TypeScript client (zero deps)
## Official TypeScript client — `@nech/api` (generated)
Drop [`nech.ts`](./nech.ts) into your project (Node 18+, Vercel Functions, or
Edge — it only uses global `fetch`/`FormData`/`Blob`). One umbrella client,
namespaced per sub-API (`nech.audio.…`, and `nech.geo.…` when it ships):
The official client is **generated** from this spec, so it can never drift from
the API. One package, per-domain subpath imports (you only bundle what you use).
Source + build: [`nech-api/`](./nech-api/).
```ts
import { NechAPI } from "./nech";
const nech = new NechAPI({ token: process.env.NECHPL_TOKEN! });
// a clip from anywhere → a Blob
const clip = await fetch(trackUrl).then(r => r.blob());
const mood = await nech.audio.emotion(clip); // { emotion: { valence, arousal, top, … } }
const spec = await nech.audio.spectrum(clip, { bands: 64, frames: 200 }); // FFT-as-a-service
const wave = await nech.audio.waveform(clip, { bins: 800 }); // render-ready envelope
const loud = await nech.audio.loudness(clip); // { lufs_integrated, gain_to_target_db, … }
if (mood._cache === "hit") { /* came free from the cache */ }
```bash
echo '@nech:registry=https://npm.nech.pl' >> .npmrc # the @nech scope is private
npm i @nech/api
```
Want a smaller bundle? Import just the sub-client:
```ts
import { NechAudio } from "./nech";
const audio = new NechAudio({ token: process.env.NECHPL_TOKEN! });
const mood = await audio.emotion(clip);
import { AudioApi, Configuration } from "@nech/api/audio";
const audio = new AudioApi(new Configuration({
basePath: "https://api.nech.pl/audio/v1",
accessToken: process.env.NECH_TOKEN,
}));
const clip = await fetch(trackUrl).then(r => r.blob()); // Blob/File
const mood = await audio.analyzeEmotion({ file: clip }); // method = operationId
const loud = await audio.loudness({ file: clip });
```
### Vercel Function example
```ts
// app/api/mood/route.ts (Next.js App Router, Node runtime)
import { NechAPI } from "@/lib/nech";
import { AudioApi, Configuration } from "@nech/api/audio";
export async function POST(req: Request) {
const clip = await req.blob();
const nech = new NechAPI({ token: process.env.NECHPL_TOKEN! });
const { emotion } = await nech.audio.emotion(clip);
return Response.json(emotion);
const audio = new AudioApi(new Configuration({
basePath: "https://api.nech.pl/audio/v1",
accessToken: process.env.NECH_TOKEN,
}));
const res = await audio.analyzeEmotion({ file: clip });
return Response.json(res);
}
```
> **Interim:** until Verdaccio (`npm.nech.pl`) is deployed (see
> [`../VERDACCIO.md`](../VERDACCIO.md)), the zero-dep drop-in
> [`nech.ts`](./nech.ts) — `new NechAPI({token}).audio.emotion(clip)` — is the
> working client. It's superseded by `@nech/api` once the registry is live.
## Scopes
Access is by the platform's hierarchical scope convention. The scope a route
......@@ -82,18 +86,26 @@ convention: the platform's `RECIPE.md` / `scopes.py`.)
## curl
```bash
curl -s -H "Authorization: Bearer $DOUANIER_TOKEN" \
curl -s -H "Authorization: Bearer $NECH_TOKEN" \
-F file=@clip.wav https://api.nech.pl/audio/v1/loudness | jq
```
## Regenerating the snapshot / a full client
## Regenerating the spec + client
`openapi.json` here is a checked-in snapshot. To codegen a client in any language:
`openapi.json` is a checked-in snapshot of the live app; `@nech/api` is generated
from it. Both are reproducible:
```bash
npx @openapitools/openapi-generator-cli generate \
-i armada/api/clients/openapi.json -g typescript-fetch -o ./generated
# 1. refresh the snapshot from the app (adds the public server block)
~/.virtualenvs/douanier/bin/python clients/refresh_openapi.py
# 2. regenerate + bundle the TS client
cd clients/nech-api && ./codegen.sh
```
The hand-written `nech.ts` is the lean, dependency-free alternative — prefer
it unless you specifically want generated models.
`test_openapi_snapshot.py` guards the snapshot against route drift. For an
ad-hoc client in another language, point any generator at `openapi.json`:
```bash
npx @openapitools/openapi-generator-cli generate \
-i armada/api/clients/openapi.json -g python -o ./generated # e.g. Python (nech_api, later)
```
# The @nech scope lives on the private registry (Verdaccio on erable).
# Consumers need this line too (in their project .npmrc or ~/.npmrc):
@nech:registry=https://npm.nech.pl
# @nech/api
Generated TypeScript clients for the [Nech.PL APIs](https://api.nech.pl). One
package, **per-domain subpath imports** — you only bundle what you import.
```bash
# one-time: point the @nech scope at the private registry
echo '@nech:registry=https://npm.nech.pl' >> .npmrc
npm i @nech/api
```
## Usage
```ts
import { AudioApi, Configuration } from '@nech/api/audio'
const audio = new AudioApi(new Configuration({
basePath: 'https://api.nech.pl/audio/v1',
accessToken: process.env.NECH_TOKEN, // your platform token (npl_live_…)
}))
// every analysis is a multipart POST of one audio clip → JSON
const emotion = await audio.analyzeEmotion({ file: clip }) // clip: Blob | File
const grade = await audio.grade({ file: clip })
```
Method names come straight from the API's `operationId`s, so the call site
tracks the spec. `clip` is a `Blob`/`File` (browser) or a `Blob` from
`fs.openAsBlob()` (Node ≥20).
Future domains land as sibling subpaths: `@nech/api/geo`, `@nech/api/iris`.
## This package is generated — don't hand-edit `src/`
Everything under `src/<domain>/` is emitted by
[openapi-generator](https://openapi-generator.tech) (`typescript-fetch`) from the
checked-in OpenAPI snapshot, then bundled with tsup.
```bash
# 1. refresh the snapshot from the live app (run from ../, in the venv)
~/.virtualenvs/douanier/bin/python ../refresh_openapi.py
# 2. regenerate + bundle every domain
./codegen.sh
```
`dist/` is built on publish (`prepublishOnly`); it's gitignored.
## Publishing (private registry)
Published to **Verdaccio at `https://npm.nech.pl`** (see
`../../VERDACCIO.md` for the erable deploy).
```bash
npm login --registry https://npm.nech.pl # one-time
npm publish # runs the build first
```
#!/usr/bin/env bash
# Regenerate @nech/api from the checked-in OpenAPI snapshot(s), then bundle.
# Fully generated — no hand-written client code to drift. Re-run after any API
# change (first refresh the snapshot: ../refresh_openapi.py).
#
# ./codegen.sh
#
# Adding a domain (geo, iris): vendor its openapi.json next to ../openapi.json
# and add a generate block below + an exports entry in package.json.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
GEN="npx --yes @openapitools/openapi-generator-cli generate"
PROPS="supportsES6=true,typescriptThreePlus=true"
gen() { # gen <domain> <spec-path>
local domain="$1" spec="$2"
echo "→ generating $domain from $spec"
$GEN -i "$spec" -g typescript-fetch -o "$HERE/src/$domain" \
--additional-properties="$PROPS"
rm -rf "$HERE/src/$domain/docs" "$HERE/src/$domain/.openapi-generator" \
"$HERE/src/$domain/.openapi-generator-ignore"
}
gen audio "$HERE/../openapi.json"
# gen geo "$HERE/../openapi.geo.json" # when geo's spec is vendored
# gen iris "$HERE/../openapi.iris.json" # when iris's spec is vendored
cd "$HERE"
npm run build
echo "✓ @nech/api regenerated + bundled"
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.23.0"
}
}
{
"name": "@nech/api",
"version": "0.1.0",
"description": "Generated TypeScript clients for the Nech.PL APIs (api.nech.pl). One package, per-domain subpath imports.",
"type": "module",
"license": "UNLICENSED",
"private": false,
"publishConfig": {
"registry": "https://npm.nech.pl"
},
"exports": {
"./audio": {
"types": "./dist/audio/index.d.ts",
"import": "./dist/audio/index.js",
"default": "./dist/audio/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup src/audio/index.ts --format esm --dts --clean --out-dir dist/audio",
"prepublishOnly": "npm run build"
},
"engines": {
"node": ">=18"
}
}
/* tslint:disable */
/* eslint-disable */
/**
* Nech.PL Audio Intelligence API
* Stem-aware audio analysis — emotion, features, sample role, loop grading, onsets/tempo, waveform & spectrogram, loudness, naming. The `audio` API of the Nech.PL APIs platform, served at /audio/v1/...; the gateway does central auth + metering (Bearer token).
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import * as runtime from '../runtime';
import {
type HTTPValidationError,
HTTPValidationErrorFromJSON,
HTTPValidationErrorToJSON,
} from '../models/HTTPValidationError';
export interface AnalyzeAllRequest {
file: Blob;
}
export interface AnalyzeEmotionRequest {
file: Blob;
}
export interface AnalyzeSamplesRequest {
file: Blob;
name?: string;
}
export interface FeaturesRequest {
file: Blob;
rhythm?: boolean;
}
export interface GradeRequest {
file: Blob;
role?: string;
}
export interface LoudnessRequest {
file: Blob;
}
export interface NamingRequest {
file: Blob;
index?: number;
}
export interface OnsetsRequest {
file: Blob;
}
export interface SpectrumRequest {
file: Blob;
bands?: number;
frames?: number;
mel?: boolean;
}
export interface WaveformRequest {
file: Blob;
bins?: number;
}
/**
*
*/
export class AudioApi extends runtime.BaseAPI {
/**
* Creates request options for analyzeAll without sending the request
*/
async analyzeAllRequestOpts(requestParameters: AnalyzeAllRequest): Promise<runtime.RequestOpts> {
if (requestParameters['file'] == null) {
throw new runtime.RequiredError(
'file',
'Required parameter "file" was null or undefined when calling analyzeAll().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
const consumes: runtime.Consume[] = [
{ contentType: 'multipart/form-data' },
];
// @ts-ignore: canConsumeForm may be unused
const canConsumeForm = runtime.canConsumeForm(consumes);
let formParams: { append(param: string, value: any): any };
let useForm = false;
// use FormData to transmit files using content-type "multipart/form-data"
useForm = canConsumeForm;
if (useForm) {
formParams = new FormData();
} else {
formParams = new URLSearchParams();
}
if (requestParameters['file'] != null) {
formParams.append('file', requestParameters['file'] as any);
}
let urlPath = `/analyze`;
return {
path: urlPath,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: formParams,
};
}
/**
* One call → emotion + features + sample role for a clip (fewer round-trips for hexa). Each is the same engine the dedicated routes use. Cached as a unit.
* Analyze All
*/
async analyzeAllRaw(requestParameters: AnalyzeAllRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<any>> {
const requestOptions = await this.analyzeAllRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
if (this.isJsonMime(response.headers.get('content-type'))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* One call → emotion + features + sample role for a clip (fewer round-trips for hexa). Each is the same engine the dedicated routes use. Cached as a unit.
* Analyze All
*/
async analyzeAll(requestParameters: AnalyzeAllRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<any> {
const response = await this.analyzeAllRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Creates request options for analyzeEmotion without sending the request
*/
async analyzeEmotionRequestOpts(requestParameters: AnalyzeEmotionRequest): Promise<runtime.RequestOpts> {
if (requestParameters['file'] == null) {
throw new runtime.RequiredError(
'file',
'Required parameter "file" was null or undefined when calling analyzeEmotion().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
const consumes: runtime.Consume[] = [
{ contentType: 'multipart/form-data' },
];
// @ts-ignore: canConsumeForm may be unused
const canConsumeForm = runtime.canConsumeForm(consumes);
let formParams: { append(param: string, value: any): any };
let useForm = false;
// use FormData to transmit files using content-type "multipart/form-data"
useForm = canConsumeForm;
if (useForm) {
formParams = new FormData();
} else {
formParams = new URLSearchParams();
}
if (requestParameters['file'] != null) {
formParams.append('file', requestParameters['file'] as any);
}
let urlPath = `/analyze/emotion`;
return {
path: urlPath,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: formParams,
};
}
/**
* Upload a short audio clip → valence/arousal + top emotions. Cached (#26): the same audio returns instantly on a repeat instead of recomputing.
* Analyze Emotion
*/
async analyzeEmotionRaw(requestParameters: AnalyzeEmotionRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<any>> {
const requestOptions = await this.analyzeEmotionRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
if (this.isJsonMime(response.headers.get('content-type'))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* Upload a short audio clip → valence/arousal + top emotions. Cached (#26): the same audio returns instantly on a repeat instead of recomputing.
* Analyze Emotion
*/
async analyzeEmotion(requestParameters: AnalyzeEmotionRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<any> {
const response = await this.analyzeEmotionRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Creates request options for analyzeSamples without sending the request
*/
async analyzeSamplesRequestOpts(requestParameters: AnalyzeSamplesRequest): Promise<runtime.RequestOpts> {
if (requestParameters['file'] == null) {
throw new runtime.RequiredError(
'file',
'Required parameter "file" was null or undefined when calling analyzeSamples().'
);
}
const queryParameters: any = {};
if (requestParameters['name'] != null) {
queryParameters['name'] = requestParameters['name'];
}
const headerParameters: runtime.HTTPHeaders = {};
const consumes: runtime.Consume[] = [
{ contentType: 'multipart/form-data' },
];
// @ts-ignore: canConsumeForm may be unused
const canConsumeForm = runtime.canConsumeForm(consumes);
let formParams: { append(param: string, value: any): any };
let useForm = false;
// use FormData to transmit files using content-type "multipart/form-data"
useForm = canConsumeForm;
if (useForm) {
formParams = new FormData();
} else {
formParams = new URLSearchParams();
}
if (requestParameters['file'] != null) {
formParams.append('file', requestParameters['file'] as any);
}
let urlPath = `/analyze/samples`;
return {
path: urlPath,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: formParams,
};
}
/**
* Upload a one-shot/loop → per-sample EDA + MEASURED role (percs|bass|melodic| tops|atmos) from the spectrum, never the name. Optional `name` only disambiguates breaks/drums. Torch-free, cached.
* Analyze Samples
*/
async analyzeSamplesRaw(requestParameters: AnalyzeSamplesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<any>> {
const requestOptions = await this.analyzeSamplesRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
if (this.isJsonMime(response.headers.get('content-type'))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* Upload a one-shot/loop → per-sample EDA + MEASURED role (percs|bass|melodic| tops|atmos) from the spectrum, never the name. Optional `name` only disambiguates breaks/drums. Torch-free, cached.
* Analyze Samples
*/
async analyzeSamples(requestParameters: AnalyzeSamplesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<any> {
const response = await this.analyzeSamplesRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Creates request options for features without sending the request
*/
async featuresRequestOpts(requestParameters: FeaturesRequest): Promise<runtime.RequestOpts> {
if (requestParameters['file'] == null) {
throw new runtime.RequiredError(
'file',
'Required parameter "file" was null or undefined when calling features().'
);
}
const queryParameters: any = {};
if (requestParameters['rhythm'] != null) {
queryParameters['rhythm'] = requestParameters['rhythm'];
}
const headerParameters: runtime.HTTPHeaders = {};
const consumes: runtime.Consume[] = [
{ contentType: 'multipart/form-data' },
];
// @ts-ignore: canConsumeForm may be unused
const canConsumeForm = runtime.canConsumeForm(consumes);
let formParams: { append(param: string, value: any): any };
let useForm = false;
// use FormData to transmit files using content-type "multipart/form-data"
useForm = canConsumeForm;
if (useForm) {
formParams = new FormData();
} else {
formParams = new URLSearchParams();
}
if (requestParameters['file'] != null) {
formParams.append('file', requestParameters['file'] as any);
}
let urlPath = `/features`;
return {
path: urlPath,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: formParams,
};
}
/**
* Upload a clip → the ~35-dim audio feature stack (spectral moments, MFCCs, chroma/key, envelope, + rhythm when `rhythm=true`). Torch-free, cached.
* Features
*/
async featuresRaw(requestParameters: FeaturesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<any>> {
const requestOptions = await this.featuresRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
if (this.isJsonMime(response.headers.get('content-type'))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* Upload a clip → the ~35-dim audio feature stack (spectral moments, MFCCs, chroma/key, envelope, + rhythm when `rhythm=true`). Torch-free, cached.
* Features
*/
async features(requestParameters: FeaturesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<any> {
const response = await this.featuresRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Creates request options for grade without sending the request
*/
async gradeRequestOpts(requestParameters: GradeRequest): Promise<runtime.RequestOpts> {
if (requestParameters['file'] == null) {
throw new runtime.RequiredError(
'file',
'Required parameter "file" was null or undefined when calling grade().'
);
}
const queryParameters: any = {};
if (requestParameters['role'] != null) {
queryParameters['role'] = requestParameters['role'];
}
const headerParameters: runtime.HTTPHeaders = {};
const consumes: runtime.Consume[] = [
{ contentType: 'multipart/form-data' },
];
// @ts-ignore: canConsumeForm may be unused
const canConsumeForm = runtime.canConsumeForm(consumes);
let formParams: { append(param: string, value: any): any };
let useForm = false;
// use FormData to transmit files using content-type "multipart/form-data"
useForm = canConsumeForm;
if (useForm) {
formParams = new FormData();
} else {
formParams = new URLSearchParams();
}
if (requestParameters['file'] != null) {
formParams.append('file', requestParameters['file'] as any);
}
let urlPath = `/grade`;
return {
path: urlPath,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: formParams,
};
}
/**
* Upload a loop/one-shot → the Foundry\'s MECHANICAL quality grade: composite 0..1 + S/A/B/C/D tier, per-rule sub-scores (seam click, zero-crossing cleanliness, DC, bar self-consistency, level, bass mono-compat) + flags. `role` (\'bass\'|\'drums\'|…) is a hint. WAV/FLAC preferred. Torch-free, cached.
* Grade
*/
async gradeRaw(requestParameters: GradeRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<any>> {
const requestOptions = await this.gradeRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
if (this.isJsonMime(response.headers.get('content-type'))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* Upload a loop/one-shot → the Foundry\'s MECHANICAL quality grade: composite 0..1 + S/A/B/C/D tier, per-rule sub-scores (seam click, zero-crossing cleanliness, DC, bar self-consistency, level, bass mono-compat) + flags. `role` (\'bass\'|\'drums\'|…) is a hint. WAV/FLAC preferred. Torch-free, cached.
* Grade
*/
async grade(requestParameters: GradeRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<any> {
const response = await this.gradeRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Creates request options for healthz without sending the request
*/
async healthzRequestOpts(): Promise<runtime.RequestOpts> {
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
let urlPath = `/healthz`;
return {
path: urlPath,
method: 'GET',
headers: headerParameters,
query: queryParameters,
};
}
/**
* Healthz
*/
async healthzRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<any>> {
const requestOptions = await this.healthzRequestOpts();
const response = await this.request(requestOptions, initOverrides);
if (this.isJsonMime(response.headers.get('content-type'))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* Healthz
*/
async healthz(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<any> {
const response = await this.healthzRaw(initOverrides);
return await response.value();
}
/**
* Creates request options for loudness without sending the request
*/
async loudnessRequestOpts(requestParameters: LoudnessRequest): Promise<runtime.RequestOpts> {
if (requestParameters['file'] == null) {
throw new runtime.RequiredError(
'file',
'Required parameter "file" was null or undefined when calling loudness().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
const consumes: runtime.Consume[] = [
{ contentType: 'multipart/form-data' },
];
// @ts-ignore: canConsumeForm may be unused
const canConsumeForm = runtime.canConsumeForm(consumes);
let formParams: { append(param: string, value: any): any };
let useForm = false;
// use FormData to transmit files using content-type "multipart/form-data"
useForm = canConsumeForm;
if (useForm) {
formParams = new FormData();
} else {
formParams = new URLSearchParams();
}
if (requestParameters['file'] != null) {
formParams.append('file', requestParameters['file'] as any);
}
let urlPath = `/loudness`;
return {
path: urlPath,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: formParams,
};
}
/**
* Upload audio → BS.1770 integrated LUFS + sample/true peak + crest + the gain (dB) to hit each delivery target (-14 streaming, -9 club). Torch-free, cached.
* Loudness
*/
async loudnessRaw(requestParameters: LoudnessRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<any>> {
const requestOptions = await this.loudnessRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
if (this.isJsonMime(response.headers.get('content-type'))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* Upload audio → BS.1770 integrated LUFS + sample/true peak + crest + the gain (dB) to hit each delivery target (-14 streaming, -9 club). Torch-free, cached.
* Loudness
*/
async loudness(requestParameters: LoudnessRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<any> {
const response = await this.loudnessRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Creates request options for naming without sending the request
*/
async namingRequestOpts(requestParameters: NamingRequest): Promise<runtime.RequestOpts> {
if (requestParameters['file'] == null) {
throw new runtime.RequiredError(
'file',
'Required parameter "file" was null or undefined when calling naming().'
);
}
const queryParameters: any = {};
if (requestParameters['index'] != null) {
queryParameters['index'] = requestParameters['index'];
}
const headerParameters: runtime.HTTPHeaders = {};
const consumes: runtime.Consume[] = [
{ contentType: 'multipart/form-data' },
];
// @ts-ignore: canConsumeForm may be unused
const canConsumeForm = runtime.canConsumeForm(consumes);
let formParams: { append(param: string, value: any): any };
let useForm = false;
// use FormData to transmit files using content-type "multipart/form-data"
useForm = canConsumeForm;
if (useForm) {
formParams = new FormData();
} else {
formParams = new URLSearchParams();
}
if (requestParameters['file'] != null) {
formParams.append('file', requestParameters['file'] as any);
}
let urlPath = `/naming`;
return {
path: urlPath,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: formParams,
};
}
/**
* Upload a sample → a convention-compliant name (NN_role_character) derived from the MEASURED role + character, not the file name. Torch-free, cached.
* Naming
*/
async namingRaw(requestParameters: NamingRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<any>> {
const requestOptions = await this.namingRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
if (this.isJsonMime(response.headers.get('content-type'))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* Upload a sample → a convention-compliant name (NN_role_character) derived from the MEASURED role + character, not the file name. Torch-free, cached.
* Naming
*/
async naming(requestParameters: NamingRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<any> {
const response = await this.namingRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Creates request options for onsets without sending the request
*/
async onsetsRequestOpts(requestParameters: OnsetsRequest): Promise<runtime.RequestOpts> {
if (requestParameters['file'] == null) {
throw new runtime.RequiredError(
'file',
'Required parameter "file" was null or undefined when calling onsets().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
const consumes: runtime.Consume[] = [
{ contentType: 'multipart/form-data' },
];
// @ts-ignore: canConsumeForm may be unused
const canConsumeForm = runtime.canConsumeForm(consumes);
let formParams: { append(param: string, value: any): any };
let useForm = false;
// use FormData to transmit files using content-type "multipart/form-data"
useForm = canConsumeForm;
if (useForm) {
formParams = new FormData();
} else {
formParams = new URLSearchParams();
}
if (requestParameters['file'] != null) {
formParams.append('file', requestParameters['file'] as any);
}
let urlPath = `/onsets`;
return {
path: urlPath,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: formParams,
};
}
/**
* Upload audio → onset hit times (s) + tempo + onset rate. The cheapest tier (no model): rhythmic hits for visual sync / slicing. Torch-free, cached.
* Onsets
*/
async onsetsRaw(requestParameters: OnsetsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<any>> {
const requestOptions = await this.onsetsRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
if (this.isJsonMime(response.headers.get('content-type'))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* Upload audio → onset hit times (s) + tempo + onset rate. The cheapest tier (no model): rhythmic hits for visual sync / slicing. Torch-free, cached.
* Onsets
*/
async onsets(requestParameters: OnsetsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<any> {
const response = await this.onsetsRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Creates request options for separate without sending the request
*/
async separateRequestOpts(): Promise<runtime.RequestOpts> {
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
let urlPath = `/separate`;
return {
path: urlPath,
method: 'POST',
headers: headerParameters,
query: queryParameters,
};
}
/**
* Stem separation (demucs/roformer). erable has no GPU and no CPU-viable path, so this is a deliberate 503 until a GPU-runner backend is wired (env-selected, same response shape). The route exists now so hexa can code against it and get a clean, documented \"compute pending\" rather than a 404.
* Separate
*/
async separateRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<any>> {
const requestOptions = await this.separateRequestOpts();
const response = await this.request(requestOptions, initOverrides);
if (this.isJsonMime(response.headers.get('content-type'))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* Stem separation (demucs/roformer). erable has no GPU and no CPU-viable path, so this is a deliberate 503 until a GPU-runner backend is wired (env-selected, same response shape). The route exists now so hexa can code against it and get a clean, documented \"compute pending\" rather than a 404.
* Separate
*/
async separate(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<any> {
const response = await this.separateRaw(initOverrides);
return await response.value();
}
/**
* Creates request options for spectrum without sending the request
*/
async spectrumRequestOpts(requestParameters: SpectrumRequest): Promise<runtime.RequestOpts> {
if (requestParameters['file'] == null) {
throw new runtime.RequiredError(
'file',
'Required parameter "file" was null or undefined when calling spectrum().'
);
}
const queryParameters: any = {};
if (requestParameters['bands'] != null) {
queryParameters['bands'] = requestParameters['bands'];
}
if (requestParameters['frames'] != null) {
queryParameters['frames'] = requestParameters['frames'];
}
if (requestParameters['mel'] != null) {
queryParameters['mel'] = requestParameters['mel'];
}
const headerParameters: runtime.HTTPHeaders = {};
const consumes: runtime.Consume[] = [
{ contentType: 'multipart/form-data' },
];
// @ts-ignore: canConsumeForm may be unused
const canConsumeForm = runtime.canConsumeForm(consumes);
let formParams: { append(param: string, value: any): any };
let useForm = false;
// use FormData to transmit files using content-type "multipart/form-data"
useForm = canConsumeForm;
if (useForm) {
formParams = new FormData();
} else {
formParams = new URLSearchParams();
}
if (requestParameters['file'] != null) {
formParams.append('file', requestParameters['file'] as any);
}
let urlPath = `/spectrum`;
return {
path: urlPath,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: formParams,
};
}
/**
* FFT-as-a-service: a downsampled, render-ready spectrogram (`bands`×`frames`, 0..1). `mel=` perceptual vs log-linear; `frames=1` → a single averaged FFT spectrum. Bounded payload → caches cheaply. Torch-free.
* Spectrum
*/
async spectrumRaw(requestParameters: SpectrumRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<any>> {
const requestOptions = await this.spectrumRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
if (this.isJsonMime(response.headers.get('content-type'))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* FFT-as-a-service: a downsampled, render-ready spectrogram (`bands`×`frames`, 0..1). `mel=` perceptual vs log-linear; `frames=1` → a single averaged FFT spectrum. Bounded payload → caches cheaply. Torch-free.
* Spectrum
*/
async spectrum(requestParameters: SpectrumRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<any> {
const response = await this.spectrumRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Creates request options for waveform without sending the request
*/
async waveformRequestOpts(requestParameters: WaveformRequest): Promise<runtime.RequestOpts> {
if (requestParameters['file'] == null) {
throw new runtime.RequiredError(
'file',
'Required parameter "file" was null or undefined when calling waveform().'
);
}
const queryParameters: any = {};
if (requestParameters['bins'] != null) {
queryParameters['bins'] = requestParameters['bins'];
}
const headerParameters: runtime.HTTPHeaders = {};
const consumes: runtime.Consume[] = [
{ contentType: 'multipart/form-data' },
];
// @ts-ignore: canConsumeForm may be unused
const canConsumeForm = runtime.canConsumeForm(consumes);
let formParams: { append(param: string, value: any): any };
let useForm = false;
// use FormData to transmit files using content-type "multipart/form-data"
useForm = canConsumeForm;
if (useForm) {
formParams = new FormData();
} else {
formParams = new URLSearchParams();
}
if (requestParameters['file'] != null) {
formParams.append('file', requestParameters['file'] as any);
}
let urlPath = `/waveform`;
return {
path: urlPath,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: formParams,
};
}
/**
* Upload audio → render-ready waveform: per-bin [min,max] + a 0..1 RMS energy envelope (`?bins=`, ≤4000). For audio-reactive visuals. Torch-free, cached.
* Waveform
*/
async waveformRaw(requestParameters: WaveformRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<any>> {
const requestOptions = await this.waveformRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
if (this.isJsonMime(response.headers.get('content-type'))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* Upload audio → render-ready waveform: per-bin [min,max] + a 0..1 RMS energy envelope (`?bins=`, ≤4000). For audio-reactive visuals. Torch-free, cached.
* Waveform
*/
async waveform(requestParameters: WaveformRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<any> {
const response = await this.waveformRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Creates request options for whoami without sending the request
*/
async whoamiRequestOpts(): Promise<runtime.RequestOpts> {
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
let urlPath = `/me`;
return {
path: urlPath,
method: 'GET',
headers: headerParameters,
query: queryParameters,
};
}
/**
* Echo the caller\'s gateway-resolved identity — handy for onboarding smoke tests.
* Whoami
*/
async whoamiRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<any>> {
const requestOptions = await this.whoamiRequestOpts();
const response = await this.request(requestOptions, initOverrides);
if (this.isJsonMime(response.headers.get('content-type'))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* Echo the caller\'s gateway-resolved identity — handy for onboarding smoke tests.
* Whoami
*/
async whoami(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<any> {
const response = await this.whoamiRaw(initOverrides);
return await response.value();
}
}
/* tslint:disable */
/* eslint-disable */
export * from './AudioApi';
/* tslint:disable */
/* eslint-disable */
export * from './runtime';
export * from './apis/index';
export * from './models/index';
/* tslint:disable */
/* eslint-disable */
/**
* Nech.PL Audio Intelligence API
* Stem-aware audio analysis — emotion, features, sample role, loop grading, onsets/tempo, waveform & spectrogram, loudness, naming. The `audio` API of the Nech.PL APIs platform, served at /audio/v1/...; the gateway does central auth + metering (Bearer token).
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
import type { ValidationError } from './ValidationError';
import {
ValidationErrorFromJSON,
ValidationErrorFromJSONTyped,
ValidationErrorToJSON,
ValidationErrorToJSONTyped,
} from './ValidationError';
/**
*
* @export
* @interface HTTPValidationError
*/
export interface HTTPValidationError {
/**
*
* @type {Array<ValidationError>}
* @memberof HTTPValidationError
*/
detail?: Array<ValidationError>;
}
/**
* Check if a given object implements the HTTPValidationError interface.
*/
export function instanceOfHTTPValidationError(value: object): value is HTTPValidationError {
return true;
}
export function HTTPValidationErrorFromJSON(json: any): HTTPValidationError {
return HTTPValidationErrorFromJSONTyped(json, false);
}
export function HTTPValidationErrorFromJSONTyped(json: any, ignoreDiscriminator: boolean): HTTPValidationError {
if (json == null) {
return json;
}
return {
'detail': json['detail'] == null ? undefined : ((json['detail'] as Array<any>).map(ValidationErrorFromJSON)),
};
}
export function HTTPValidationErrorToJSON(json: any): HTTPValidationError {
return HTTPValidationErrorToJSONTyped(json, false);
}
export function HTTPValidationErrorToJSONTyped(value?: HTTPValidationError | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'detail': value['detail'] == null ? undefined : ((value['detail'] as Array<any>).map(ValidationErrorToJSON)),
};
}
/* tslint:disable */
/* eslint-disable */
/**
* Nech.PL Audio Intelligence API
* Stem-aware audio analysis — emotion, features, sample role, loop grading, onsets/tempo, waveform & spectrogram, loudness, naming. The `audio` API of the Nech.PL APIs platform, served at /audio/v1/...; the gateway does central auth + metering (Bearer token).
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
/**
*
* @export
* @interface LocationInner
*/
export interface LocationInner {
}
/**
* Check if a given object implements the LocationInner interface.
*/
export function instanceOfLocationInner(value: object): value is LocationInner {
return true;
}
export function LocationInnerFromJSON(json: any): LocationInner {
return LocationInnerFromJSONTyped(json, false);
}
export function LocationInnerFromJSONTyped(json: any, ignoreDiscriminator: boolean): LocationInner {
return json;
}
export function LocationInnerToJSON(json: any): LocationInner {
return LocationInnerToJSONTyped(json, false);
}
export function LocationInnerToJSONTyped(value?: LocationInner | null, ignoreDiscriminator: boolean = false): any {
return value;
}
/* tslint:disable */
/* eslint-disable */
/**
* Nech.PL Audio Intelligence API
* Stem-aware audio analysis — emotion, features, sample role, loop grading, onsets/tempo, waveform & spectrogram, loudness, naming. The `audio` API of the Nech.PL APIs platform, served at /audio/v1/...; the gateway does central auth + metering (Bearer token).
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
import type { LocationInner } from './LocationInner';
import {
LocationInnerFromJSON,
LocationInnerFromJSONTyped,
LocationInnerToJSON,
LocationInnerToJSONTyped,
} from './LocationInner';
/**
*
* @export
* @interface ValidationError
*/
export interface ValidationError {
/**
*
* @type {Array<LocationInner>}
* @memberof ValidationError
*/
loc: Array<LocationInner>;
/**
*
* @type {string}
* @memberof ValidationError
*/
msg: string;
/**
*
* @type {string}
* @memberof ValidationError
*/
type: string;
/**
*
* @type {any}
* @memberof ValidationError
*/
input?: any | null;
/**
*
* @type {object}
* @memberof ValidationError
*/
ctx?: object;
}
/**
* Check if a given object implements the ValidationError interface.
*/
export function instanceOfValidationError(value: object): value is ValidationError {
if (!('loc' in value) || value['loc'] === undefined) return false;
if (!('msg' in value) || value['msg'] === undefined) return false;
if (!('type' in value) || value['type'] === undefined) return false;
return true;
}
export function ValidationErrorFromJSON(json: any): ValidationError {
return ValidationErrorFromJSONTyped(json, false);
}
export function ValidationErrorFromJSONTyped(json: any, ignoreDiscriminator: boolean): ValidationError {
if (json == null) {
return json;
}
return {
'loc': ((json['loc'] as Array<any>).map(LocationInnerFromJSON)),
'msg': json['msg'],
'type': json['type'],
'input': json['input'] == null ? undefined : json['input'],
'ctx': json['ctx'] == null ? undefined : json['ctx'],
};
}
export function ValidationErrorToJSON(json: any): ValidationError {
return ValidationErrorToJSONTyped(json, false);
}
export function ValidationErrorToJSONTyped(value?: ValidationError | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'loc': ((value['loc'] as Array<any>).map(LocationInnerToJSON)),
'msg': value['msg'],
'type': value['type'],
'input': value['input'],
'ctx': value['ctx'],
};
}
/* tslint:disable */
/* eslint-disable */
export * from './HTTPValidationError';
export * from './LocationInner';
export * from './ValidationError';
/* tslint:disable */
/* eslint-disable */
/**
* Nech.PL Audio Intelligence API
* Stem-aware audio analysis — emotion, features, sample role, loop grading, onsets/tempo, waveform & spectrogram, loudness, naming. The `audio` API of the Nech.PL APIs platform, served at /audio/v1/...; the gateway does central auth + metering (Bearer token).
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export const BASE_PATH = "https://api.nech.pl/audio/v1".replace(/\/+$/, "");
export interface ConfigurationParameters {
basePath?: string; // override base path
fetchApi?: FetchAPI; // override for fetch implementation
middleware?: Middleware[]; // middleware to apply before/after fetch requests
queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings
username?: string; // parameter for basic security
password?: string; // parameter for basic security
apiKey?: string | Promise<string> | ((name: string) => string | Promise<string>); // parameter for apiKey security
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string | Promise<string>); // parameter for oauth2 security
headers?: HTTPHeaders; //header params we want to use on every request
credentials?: RequestCredentials; //value for the credentials param we want to use on each request
}
export class Configuration {
constructor(private configuration: ConfigurationParameters = {}) {}
set config(configuration: Configuration) {
this.configuration = configuration;
}
get basePath(): string {
return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH;
}
get fetchApi(): FetchAPI | undefined {
return this.configuration.fetchApi;
}
get middleware(): Middleware[] {
return this.configuration.middleware || [];
}
get queryParamsStringify(): (params: HTTPQuery) => string {
return this.configuration.queryParamsStringify || querystring;
}
get username(): string | undefined {
return this.configuration.username;
}
get password(): string | undefined {
return this.configuration.password;
}
get apiKey(): ((name: string) => string | Promise<string>) | undefined {
const apiKey = this.configuration.apiKey;
if (apiKey) {
return typeof apiKey === 'function' ? apiKey : () => apiKey;
}
return undefined;
}
get accessToken(): ((name?: string, scopes?: string[]) => string | Promise<string>) | undefined {
const accessToken = this.configuration.accessToken;
if (accessToken) {
return typeof accessToken === 'function' ? accessToken : async () => accessToken;
}
return undefined;
}
get headers(): HTTPHeaders | undefined {
return this.configuration.headers;
}
get credentials(): RequestCredentials | undefined {
return this.configuration.credentials;
}
}
export const DefaultConfig = new Configuration();
/**
* This is the base class for all generated API classes.
*/
export class BaseAPI {
private static readonly jsonRegex = /^(:?application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(:?;.*)?$/i;
private middleware: Middleware[];
constructor(protected configuration = DefaultConfig) {
this.middleware = configuration.middleware;
}
withMiddleware<T extends BaseAPI>(this: T, ...middlewares: Middleware[]) {
const next = this.clone<T>();
next.middleware = next.middleware.concat(...middlewares);
return next;
}
withPreMiddleware<T extends BaseAPI>(this: T, ...preMiddlewares: Array<Middleware['pre']>) {
const middlewares = preMiddlewares.map((pre) => ({ pre }));
return this.withMiddleware<T>(...middlewares);
}
withPostMiddleware<T extends BaseAPI>(this: T, ...postMiddlewares: Array<Middleware['post']>) {
const middlewares = postMiddlewares.map((post) => ({ post }));
return this.withMiddleware<T>(...middlewares);
}
/**
* Check if the given MIME is a JSON MIME.
* JSON MIME examples:
* application/json
* application/json; charset=UTF8
* APPLICATION/JSON
* application/vnd.company+json
* @param mime - MIME (Multipurpose Internet Mail Extensions)
* @return True if the given MIME is JSON, false otherwise.
*/
protected isJsonMime(mime: string | null | undefined): boolean {
if (!mime) {
return false;
}
return BaseAPI.jsonRegex.test(mime);
}
protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise<Response> {
const { url, init } = await this.createFetchParams(context, initOverrides);
const response = await this.fetchApi(url, init);
if (response && (response.status >= 200 && response.status < 300)) {
return response;
}
throw new ResponseError(response, 'Response returned an error code');
}
private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) {
let url = this.configuration.basePath + context.path;
if (context.query !== undefined && Object.keys(context.query).length !== 0) {
// only add the querystring to the URL if there are query parameters.
// this is done to avoid urls ending with a "?" character which buggy webservers
// do not handle correctly sometimes.
url += '?' + this.configuration.queryParamsStringify(context.query);
}
const headers = Object.assign({}, this.configuration.headers, context.headers);
Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {});
const initOverrideFn =
typeof initOverrides === "function"
? initOverrides
: async () => initOverrides;
const initParams = {
method: context.method,
headers,
body: context.body,
credentials: this.configuration.credentials,
};
const overriddenInit: RequestInit = {
...initParams,
...(await initOverrideFn({
init: initParams,
context,
}))
};
let body: any;
if (isFormData(overriddenInit.body)
|| (overriddenInit.body instanceof URLSearchParams)
|| isBlob(overriddenInit.body)) {
body = overriddenInit.body;
} else if (this.isJsonMime(headers['Content-Type'])) {
body = JSON.stringify(overriddenInit.body);
} else {
body = overriddenInit.body;
}
const init: RequestInit = {
...overriddenInit,
body
};
return { url, init };
}
private fetchApi = async (url: string, init: RequestInit) => {
let fetchParams = { url, init };
for (const middleware of this.middleware) {
if (middleware.pre) {
fetchParams = await middleware.pre({
fetch: this.fetchApi,
...fetchParams,
}) || fetchParams;
}
}
let response: Response | undefined = undefined;
try {
response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init);
} catch (e) {
for (const middleware of this.middleware) {
if (middleware.onError) {
response = await middleware.onError({
fetch: this.fetchApi,
url: fetchParams.url,
init: fetchParams.init,
error: e,
response: response ? response.clone() : undefined,
}) || response;
}
}
if (response === undefined) {
if (e instanceof Error) {
throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response');
} else {
throw e;
}
}
}
for (const middleware of this.middleware) {
if (middleware.post) {
response = await middleware.post({
fetch: this.fetchApi,
url: fetchParams.url,
init: fetchParams.init,
response: response.clone(),
}) || response;
}
}
return response;
}
/**
* Create a shallow clone of `this` by constructing a new instance
* and then shallow cloning data members.
*/
private clone<T extends BaseAPI>(this: T): T {
const constructor = this.constructor as any;
const next = new constructor(this.configuration);
next.middleware = this.middleware.slice();
return next;
}
};
function isBlob(value: any): value is Blob {
return typeof Blob !== 'undefined' && value instanceof Blob;
}
function isFormData(value: any): value is FormData {
return typeof FormData !== "undefined" && value instanceof FormData;
}
export class ResponseError extends Error {
override name: "ResponseError" = "ResponseError";
constructor(public response: Response, msg?: string) {
super(msg);
// restore prototype chain
const actualProto = new.target.prototype;
if (Object.setPrototypeOf) {
Object.setPrototypeOf(this, actualProto);
}
}
}
export class FetchError extends Error {
override name: "FetchError" = "FetchError";
constructor(public cause: Error, msg?: string) {
super(msg);
// restore prototype chain
const actualProto = new.target.prototype;
if (Object.setPrototypeOf) {
Object.setPrototypeOf(this, actualProto);
}
}
}
export class RequiredError extends Error {
override name: "RequiredError" = "RequiredError";
constructor(public field: string, msg?: string) {
super(msg);
// restore prototype chain
const actualProto = new.target.prototype;
if (Object.setPrototypeOf) {
Object.setPrototypeOf(this, actualProto);
}
}
}
export const COLLECTION_FORMATS = {
csv: ",",
ssv: " ",
tsv: "\t",
pipes: "|",
};
export type FetchAPI = WindowOrWorkerGlobalScope['fetch'];
export type Json = any;
export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';
export type HTTPHeaders = { [key: string]: string };
export type HTTPQuery = { [key: string]: string | number | null | boolean | Array<string | number | null | boolean> | Set<string | number | null | boolean> | HTTPQuery };
export type HTTPBody = Json | FormData | URLSearchParams;
export type HTTPRequestInit = { headers?: HTTPHeaders; method: HTTPMethod; credentials?: RequestCredentials; body?: HTTPBody };
export type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | 'original';
export type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, context: RequestOpts }) => Promise<RequestInit>
export interface FetchParams {
url: string;
init: RequestInit;
}
export interface RequestOpts {
path: string;
method: HTTPMethod;
headers: HTTPHeaders;
query?: HTTPQuery;
body?: HTTPBody;
}
export function querystring(params: HTTPQuery, prefix: string = ''): string {
return Object.keys(params)
.map(key => querystringSingleKey(key, params[key], prefix))
.filter(part => part.length > 0)
.join('&');
}
function querystringSingleKey(key: string, value: string | number | null | undefined | boolean | Array<string | number | null | boolean> | Set<string | number | null | boolean> | HTTPQuery, keyPrefix: string = ''): string {
const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key);
if (value instanceof Array) {
const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue)))
.join(`&${encodeURIComponent(fullKey)}=`);
return `${encodeURIComponent(fullKey)}=${multiValue}`;
}
if (value instanceof Set) {
const valueAsArray = Array.from(value);
return querystringSingleKey(key, valueAsArray, keyPrefix);
}
if (value instanceof Date) {
return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`;
}
if (value instanceof Object) {
return querystring(value as HTTPQuery, fullKey);
}
return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`;
}
export function exists(json: any, key: string) {
const value = json[key];
return value !== null && value !== undefined;
}
export function mapValues(data: any, fn: (item: any) => any) {
const result: { [key: string]: any } = {};
for (const key of Object.keys(data)) {
result[key] = fn(data[key]);
}
return result;
}
export function canConsumeForm(consumes: Consume[]): boolean {
for (const consume of consumes) {
if ('multipart/form-data' === consume.contentType) {
return true;
}
}
return false;
}
export interface Consume {
contentType: string;
}
export interface RequestContext {
fetch: FetchAPI;
url: string;
init: RequestInit;
}
export interface ResponseContext {
fetch: FetchAPI;
url: string;
init: RequestInit;
response: Response;
}
export interface ErrorContext {
fetch: FetchAPI;
url: string;
init: RequestInit;
error: unknown;
response?: Response;
}
export interface Middleware {
pre?(context: RequestContext): Promise<FetchParams | void>;
post?(context: ResponseContext): Promise<Response | void>;
onError?(context: ErrorContext): Promise<Response | void>;
}
export interface ApiResponse<T> {
raw: Response;
value(): Promise<T>;
}
export interface ResponseTransformer<T> {
(json: any): T;
}
export class JSONApiResponse<T> {
constructor(public raw: Response, private transformer: ResponseTransformer<T> = (jsonValue: any) => jsonValue) {}
async value(): Promise<T> {
return this.transformer(await this.raw.json());
}
}
export class VoidApiResponse {
constructor(public raw: Response) {}
async value(): Promise<void> {
return undefined;
}
}
export class BlobApiResponse {
constructor(public raw: Response) {}
async value(): Promise<Blob> {
return await this.raw.blob();
};
}
export class TextApiResponse {
constructor(public raw: Response) {}
async value(): Promise<string> {
return await this.raw.text();
};
}
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["ES2020", "DOM"]
},
"include": ["src/**/*.ts"],
"exclude": ["**/docs/**", "**/.openapi-generator/**"]
}
......@@ -2,14 +2,17 @@
"openapi": "3.1.0",
"info": {
"title": "Nech.PL Audio Intelligence API",
"description": "Stem-aware audio analysis \u2014 emotion, features, sample role, loop grading, onsets/tempo, waveform & spectrogram, loudness, naming. The `audio` API of the Nech.PL APIs platform, served at /audio/v1/...; the gateway does central auth + metering (Bearer token).",
"description": "Stem-aware audio analysis emotion, features, sample role, loop grading, onsets/tempo, waveform & spectrogram, loudness, naming. The `audio` API of the Nech.PL APIs platform, served at /audio/v1/...; the gateway does central auth + metering (Bearer token).",
"version": "0.1.0"
},
"paths": {
"/healthz": {
"get": {
"tags": [
"audio"
],
"summary": "Healthz",
"operationId": "healthz_healthz_get",
"operationId": "healthz",
"responses": {
"200": {
"description": "Successful Response",
......@@ -24,9 +27,12 @@
},
"/me": {
"get": {
"tags": [
"audio"
],
"summary": "Whoami",
"description": "Echo the caller's gateway-resolved identity \u2014 handy for onboarding smoke tests.",
"operationId": "whoami_me_get",
"description": "Echo the caller's gateway-resolved identity handy for onboarding smoke tests.",
"operationId": "whoami",
"responses": {
"200": {
"description": "Successful Response",
......@@ -41,14 +47,17 @@
},
"/analyze/emotion": {
"post": {
"tags": [
"audio"
],
"summary": "Analyze Emotion",
"description": "Upload a short audio clip \u2192 valence/arousal + top emotions. Cached (#26):\nthe same audio returns instantly on a repeat instead of recomputing.",
"operationId": "analyze_emotion_analyze_emotion_post",
"description": "Upload a short audio clip valence/arousal + top emotions. Cached (#26):\nthe same audio returns instantly on a repeat instead of recomputing.",
"operationId": "analyzeEmotion",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_analyze_emotion_analyze_emotion_post"
"$ref": "#/components/schemas/Body_analyzeEmotion"
}
}
},
......@@ -78,9 +87,12 @@
},
"/features": {
"post": {
"tags": [
"audio"
],
"summary": "Features",
"description": "Upload a clip \u2192 the ~35-dim audio feature stack (spectral moments, MFCCs,\nchroma/key, envelope, + rhythm when `rhythm=true`). Torch-free, cached.",
"operationId": "features_features_post",
"description": "Upload a clip the ~35-dim audio feature stack (spectral moments, MFCCs,\nchroma/key, envelope, + rhythm when `rhythm=true`). Torch-free, cached.",
"operationId": "features",
"parameters": [
{
"name": "rhythm",
......@@ -98,7 +110,7 @@
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_features_features_post"
"$ref": "#/components/schemas/Body_features"
}
}
}
......@@ -127,9 +139,12 @@
},
"/analyze/samples": {
"post": {
"tags": [
"audio"
],
"summary": "Analyze Samples",
"description": "Upload a one-shot/loop \u2192 per-sample EDA + MEASURED role (percs|bass|melodic|\ntops|atmos) from the spectrum, never the name. Optional `name` only\ndisambiguates breaks/drums. Torch-free, cached.",
"operationId": "analyze_samples_analyze_samples_post",
"description": "Upload a one-shot/loop per-sample EDA + MEASURED role (percs|bass|melodic|\ntops|atmos) from the spectrum, never the name. Optional `name` only\ndisambiguates breaks/drums. Torch-free, cached.",
"operationId": "analyzeSamples",
"parameters": [
{
"name": "name",
......@@ -147,7 +162,7 @@
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_analyze_samples_analyze_samples_post"
"$ref": "#/components/schemas/Body_analyzeSamples"
}
}
}
......@@ -176,14 +191,17 @@
},
"/onsets": {
"post": {
"tags": [
"audio"
],
"summary": "Onsets",
"description": "Upload audio \u2192 onset hit times (s) + tempo + onset rate. The cheapest tier\n(no model): rhythmic hits for visual sync / slicing. Torch-free, cached.",
"operationId": "onsets_onsets_post",
"description": "Upload audio onset hit times (s) + tempo + onset rate. The cheapest tier\n(no model): rhythmic hits for visual sync / slicing. Torch-free, cached.",
"operationId": "onsets",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_onsets_onsets_post"
"$ref": "#/components/schemas/Body_onsets"
}
}
},
......@@ -213,9 +231,12 @@
},
"/waveform": {
"post": {
"tags": [
"audio"
],
"summary": "Waveform",
"description": "Upload audio \u2192 render-ready waveform: per-bin [min,max] + a 0..1 RMS energy\nenvelope (`?bins=`, \u22644000). For audio-reactive visuals. Torch-free, cached.",
"operationId": "waveform_waveform_post",
"description": "Upload audio → render-ready waveform: per-bin [min,max] + a 0..1 RMS energy\nenvelope (`?bins=`, ≤4000). For audio-reactive visuals. Torch-free, cached.",
"operationId": "waveform",
"parameters": [
{
"name": "bins",
......@@ -233,7 +254,7 @@
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_waveform_waveform_post"
"$ref": "#/components/schemas/Body_waveform"
}
}
}
......@@ -262,14 +283,17 @@
},
"/analyze": {
"post": {
"tags": [
"audio"
],
"summary": "Analyze All",
"description": "One call \u2192 emotion + features + sample role for a clip (fewer round-trips\nfor hexa). Each is the same engine the dedicated routes use. Cached as a unit.",
"operationId": "analyze_all_analyze_post",
"description": "One call emotion + features + sample role for a clip (fewer round-trips\nfor hexa). Each is the same engine the dedicated routes use. Cached as a unit.",
"operationId": "analyzeAll",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_analyze_all_analyze_post"
"$ref": "#/components/schemas/Body_analyzeAll"
}
}
},
......@@ -299,9 +323,12 @@
},
"/grade": {
"post": {
"tags": [
"audio"
],
"summary": "Grade",
"description": "Upload a loop/one-shot \u2192 the Foundry's MECHANICAL quality grade: composite\n0..1 + S/A/B/C/D tier, per-rule sub-scores (seam click, zero-crossing\ncleanliness, DC, bar self-consistency, level, bass mono-compat) + flags.\n`role` ('bass'|'drums'|\u2026) is a hint. WAV/FLAC preferred. Torch-free, cached.",
"operationId": "grade_grade_post",
"description": "Upload a loop/one-shot → the Foundry's MECHANICAL quality grade: composite\n0..1 + S/A/B/C/D tier, per-rule sub-scores (seam click, zero-crossing\ncleanliness, DC, bar self-consistency, level, bass mono-compat) + flags.\n`role` ('bass'|'drums'|…) is a hint. WAV/FLAC preferred. Torch-free, cached.",
"operationId": "grade",
"parameters": [
{
"name": "role",
......@@ -319,7 +346,7 @@
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_grade_grade_post"
"$ref": "#/components/schemas/Body_grade"
}
}
}
......@@ -348,14 +375,17 @@
},
"/loudness": {
"post": {
"tags": [
"audio"
],
"summary": "Loudness",
"description": "Upload audio \u2192 BS.1770 integrated LUFS + sample/true peak + crest + the\ngain (dB) to hit each delivery target (-14 streaming, -9 club). Torch-free, cached.",
"operationId": "loudness_loudness_post",
"description": "Upload audio BS.1770 integrated LUFS + sample/true peak + crest + the\ngain (dB) to hit each delivery target (-14 streaming, -9 club). Torch-free, cached.",
"operationId": "loudness",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_loudness_loudness_post"
"$ref": "#/components/schemas/Body_loudness"
}
}
},
......@@ -385,9 +415,12 @@
},
"/spectrum": {
"post": {
"tags": [
"audio"
],
"summary": "Spectrum",
"description": "FFT-as-a-service: a downsampled, render-ready spectrogram (`bands`\u00d7`frames`,\n0..1). `mel=` perceptual vs log-linear; `frames=1` \u2192 a single averaged FFT\nspectrum. Bounded payload \u2192 caches cheaply. Torch-free.",
"operationId": "spectrum_spectrum_post",
"description": "FFT-as-a-service: a downsampled, render-ready spectrogram (`bands`×`frames`,\n0..1). `mel=` perceptual vs log-linear; `frames=1` → a single averaged FFT\nspectrum. Bounded payload → caches cheaply. Torch-free.",
"operationId": "spectrum",
"parameters": [
{
"name": "bands",
......@@ -425,7 +458,7 @@
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_spectrum_spectrum_post"
"$ref": "#/components/schemas/Body_spectrum"
}
}
}
......@@ -454,9 +487,12 @@
},
"/naming": {
"post": {
"tags": [
"audio"
],
"summary": "Naming",
"description": "Upload a sample \u2192 a convention-compliant name (NN_role_character) derived\nfrom the MEASURED role + character, not the file name. Torch-free, cached.",
"operationId": "naming_naming_post",
"description": "Upload a sample a convention-compliant name (NN_role_character) derived\nfrom the MEASURED role + character, not the file name. Torch-free, cached.",
"operationId": "naming",
"parameters": [
{
"name": "index",
......@@ -474,7 +510,7 @@
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_naming_naming_post"
"$ref": "#/components/schemas/Body_naming"
}
}
}
......@@ -503,9 +539,12 @@
},
"/separate": {
"post": {
"tags": [
"audio"
],
"summary": "Separate",
"description": "Stem separation (demucs/roformer). erable has no GPU and no CPU-viable path,\nso this is a deliberate 503 until a GPU-runner backend is wired (env-selected,\nsame response shape). The route exists now so hexa can code against it and get\na clean, documented \"compute pending\" rather than a 404.",
"operationId": "separate_separate_post",
"operationId": "separate",
"responses": {
"200": {
"description": "Successful Response",
......@@ -521,7 +560,7 @@
},
"components": {
"schemas": {
"Body_analyze_all_analyze_post": {
"Body_analyzeAll": {
"properties": {
"file": {
"type": "string",
......@@ -533,9 +572,9 @@
"required": [
"file"
],
"title": "Body_analyze_all_analyze_post"
"title": "Body_analyzeAll"
},
"Body_analyze_emotion_analyze_emotion_post": {
"Body_analyzeEmotion": {
"properties": {
"file": {
"type": "string",
......@@ -547,9 +586,9 @@
"required": [
"file"
],
"title": "Body_analyze_emotion_analyze_emotion_post"
"title": "Body_analyzeEmotion"
},
"Body_analyze_samples_analyze_samples_post": {
"Body_analyzeSamples": {
"properties": {
"file": {
"type": "string",
......@@ -561,9 +600,9 @@
"required": [
"file"
],
"title": "Body_analyze_samples_analyze_samples_post"
"title": "Body_analyzeSamples"
},
"Body_features_features_post": {
"Body_features": {
"properties": {
"file": {
"type": "string",
......@@ -575,9 +614,9 @@
"required": [
"file"
],
"title": "Body_features_features_post"
"title": "Body_features"
},
"Body_grade_grade_post": {
"Body_grade": {
"properties": {
"file": {
"type": "string",
......@@ -589,9 +628,9 @@
"required": [
"file"
],
"title": "Body_grade_grade_post"
"title": "Body_grade"
},
"Body_loudness_loudness_post": {
"Body_loudness": {
"properties": {
"file": {
"type": "string",
......@@ -603,9 +642,9 @@
"required": [
"file"
],
"title": "Body_loudness_loudness_post"
"title": "Body_loudness"
},
"Body_naming_naming_post": {
"Body_naming": {
"properties": {
"file": {
"type": "string",
......@@ -617,9 +656,9 @@
"required": [
"file"
],
"title": "Body_naming_naming_post"
"title": "Body_naming"
},
"Body_onsets_onsets_post": {
"Body_onsets": {
"properties": {
"file": {
"type": "string",
......@@ -631,9 +670,9 @@
"required": [
"file"
],
"title": "Body_onsets_onsets_post"
"title": "Body_onsets"
},
"Body_spectrum_spectrum_post": {
"Body_spectrum": {
"properties": {
"file": {
"type": "string",
......@@ -645,9 +684,9 @@
"required": [
"file"
],
"title": "Body_spectrum_spectrum_post"
"title": "Body_spectrum"
},
"Body_waveform_waveform_post": {
"Body_waveform": {
"properties": {
"file": {
"type": "string",
......@@ -659,7 +698,7 @@
"required": [
"file"
],
"title": "Body_waveform_waveform_post"
"title": "Body_waveform"
},
"HTTPValidationError": {
"properties": {
......@@ -718,8 +757,7 @@
},
"servers": [
{
"url": "https://api.nech.pl/audio/v1",
"description": "Nech.PL APIs gateway (public)"
"url": "https://api.nech.pl/audio/v1"
}
]
}
\ No newline at end of file
}
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.23.0"
}
}
#!/usr/bin/env python3
"""Refresh clients/openapi.json from the live app — the snapshot hexa + the
generated clients build against (#32, #44).
`app.app.openapi()` omits the public `servers` block (it's only injected when
served behind the gateway's root_path), so we add it here. test_openapi_snapshot
guards both the path set and this server URL — keep them in sync via THIS script.
~/.virtualenvs/douanier/bin/python clients/refresh_openapi.py
"""
import json
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent)) # the service package (app.py, config.py)
import app # noqa: E402 (imported for its FastAPI instance)
import config # noqa: E402
PUBLIC_BASE = "https://api.nech.pl"
def main() -> None:
spec = app.app.openapi()
spec["servers"] = [{"url": PUBLIC_BASE + config.ROOT_PATH}]
out = HERE / "openapi.json"
out.write_text(json.dumps(spec, indent=2, ensure_ascii=False) + "\n")
ops = [o.get("operationId") for p in spec["paths"].values() for o in p.values()]
print(f"wrote {out} ({len(ops)} operations, server={spec['servers'][0]['url']})")
if __name__ == "__main__":
main()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment