{"openapi":"3.1.0","info":{"title":"Nech.PL Audio Intelligence API","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","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/me":{"get":{"tags":["audio"],"summary":"Whoami","description":"Echo the caller's gateway-resolved identity — handy for onboarding smoke tests.","operationId":"whoami","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/jobs/{job_id}":{"get":{"tags":["audio"],"summary":"Get Job","description":"Poll an async job. 404 if it doesn't exist OR belongs to another tenant\n(we don't leak job existence across accounts).","operationId":"getJob","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatus"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["audio"],"summary":"Cancel Job","description":"Cancel a pending/running job (cooperative — a running handler stops at its\nnext progress checkpoint). Idempotent: cancelling a finished job is a no-op.","operationId":"cancelJob","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatus"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sources":{"post":{"tags":["audio"],"summary":"Submit Source","description":"Fetch a URL's best audio → 44.1k WAV, as an async job. Returns `202` with a\n`job_id`; poll `GET /jobs/{id}` for the source descriptor (content_id +\nartifact_ref). A URL we've already fetched (and whose artifact still exists)\ncomes back as a born-`done` job immediately — no re-download.","operationId":"submitSource","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatus"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/artifacts/{content_id}/{name}":{"get":{"tags":["audio"],"summary":"Get Artifact","description":"Stream a stored artifact (a fetched source WAV, later stems/loops). Any\nauthenticated tenant holding the (opaque, content-addressed) ref may fetch it.\nIn prod the app returns an `X-Accel-Redirect` and nginx streams the bytes\n(Range-capable, zero-copy); in dev it falls back to a direct FileResponse.","operationId":"getArtifact","parameters":[{"name":"content_id","in":"path","required":true,"schema":{"type":"string","title":"Content Id"}},{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/analyze/emotion":{"post":{"tags":["audio"],"summary":"Analyze Emotion","description":"Upload a short audio clip → valence/arousal + top emotions. Cached (#26):\nthe same audio returns instantly on a repeat instead of recomputing.\nClip-tier: the emotion engine reads the first ~60 s (`source.duration_s`\nreports the full length).","operationId":"analyzeEmotion","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_analyzeEmotion"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/features":{"post":{"tags":["audio"],"summary":"Features","description":"Upload a clip → the ~35-dim audio feature stack (spectral moments, MFCCs,\nchroma/key, envelope, + rhythm when `rhythm=true`). Torch-free, cached.\nClip-tier: analyses the first ~30 s (`source.duration_s` is the full length).","operationId":"features","parameters":[{"name":"rhythm","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Rhythm"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_features"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/analyze/samples":{"post":{"tags":["audio"],"summary":"Analyze Samples","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. Clip-tier: first ~30 s.","operationId":"analyzeSamples","parameters":[{"name":"name","in":"query","required":false,"schema":{"type":"string","default":"","title":"Name"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_analyzeSamples"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/onsets":{"post":{"tags":["audio"],"summary":"Onsets","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.\nFull-track: streams the WHOLE upload (`full_track:true`), not just a head clip.","operationId":"onsets","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_onsets"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/waveform":{"post":{"tags":["audio"],"summary":"Waveform","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.\nFull-track: bins span the WHOLE track via an exact streaming reduction.","operationId":"waveform","parameters":[{"name":"bins","in":"query","required":false,"schema":{"type":"integer","default":800,"title":"Bins"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_waveform"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/analyze":{"post":{"tags":["audio"],"summary":"Analyze All","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.\nClip-tier composite: emotion ~60 s, features/sample ~30 s of the head.","operationId":"analyzeAll","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_analyzeAll"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/grade":{"post":{"tags":["audio"],"summary":"Grade","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","in":"query","required":false,"schema":{"type":"string","default":"","title":"Role"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_grade"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/loudness":{"post":{"tags":["audio"],"summary":"Loudness","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.\nFull-track: peak/true-peak/RMS are exact; integrated LUFS is chunked-approximate\n(`approximate:true`) — pyloudnorm gating can't merge chunks exactly.","operationId":"loudness","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_loudness"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/spectrum":{"post":{"tags":["audio"],"summary":"Spectrum","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. Full-track: frames span\nthe WHOLE track (streamed windows binned into the global time grid).","operationId":"spectrum","parameters":[{"name":"bands","in":"query","required":false,"schema":{"type":"integer","default":64,"title":"Bands"}},{"name":"frames","in":"query","required":false,"schema":{"type":"integer","default":200,"title":"Frames"}},{"name":"mel","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Mel"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_spectrum"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/naming":{"post":{"tags":["audio"],"summary":"Naming","description":"Upload a sample → a convention-compliant name (NN_role_character) derived\nfrom the MEASURED role + character, not the file name. Torch-free, cached.\nClip-tier: first ~30 s.","operationId":"naming","parameters":[{"name":"index","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Index"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_naming"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/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","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"Body_analyzeAll":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_analyzeAll"},"Body_analyzeEmotion":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_analyzeEmotion"},"Body_analyzeSamples":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_analyzeSamples"},"Body_features":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_features"},"Body_grade":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_grade"},"Body_loudness":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_loudness"},"Body_naming":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_naming"},"Body_onsets":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_onsets"},"Body_spectrum":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_spectrum"},"Body_waveform":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_waveform"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"JobStatus":{"properties":{"job_id":{"type":"string","title":"Job Id"},"type":{"type":"string","title":"Type"},"status":{"type":"string","title":"Status"},"progress":{"type":"number","title":"Progress"},"result":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Result"},"result_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result Ref"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"created":{"type":"number","title":"Created"},"started":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Started"},"finished":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Finished"}},"type":"object","required":["job_id","type","status","progress","created"],"title":"JobStatus","description":"The status of an async job (separate/sources/loops). `result` is populated\nonce `status == \"done\"`; `error` once `status == \"error\"`."},"SourceRequest":{"properties":{"url":{"type":"string","title":"Url"},"priority":{"type":"integer","title":"Priority","default":0},"webhook_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Webhook Url"}},"type":"object","required":["url"],"title":"SourceRequest","description":"Submit a URL for audio fetch. `url` is any http(s) URL yt-dlp supports."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}},"servers":[{"url":"/audio/v1"}]}