Audio preprocessing
POST /v1/audio/preprocess takes one audio file plus a closed JSON object of
processing options and returns one conditioned audio file. The delivered
timeline matches the input, subject to the per-codec guarantees in
Determinism, so several downstream model calls can be handed
byte-identical audio and their output aligned against your original recording.
This is not a transcription endpoint: it returns audio and nothing else. To transcribe the conditioned file, send the response body to Audio transcription.
Availability
Section titled “Availability”Audio preprocessing is available when your plan includes it. Requests draw on your plan’s overflow
capacity, so a plan without overflow capacity has every call refused with a 429 audio_lanes_exhausted and a Retry-After header. Requests are also bounded per instance, so a burst can
answer 503 at_capacity.
There is no additional charge for audio preprocessing. It is included with your plan, and usage is not billed separately.
Quickstart
Section titled “Quickstart”curl --fail-with-body https://gw.lunaroute.com/v1/audio/preprocess \ -H "LUNAROUTE-API-KEY: $LUNAROUTE_API_KEY" \ -F file=@recording.wav \ -F 'options={"output":{"format":"flac"},"audio":{"sample_rate":16000}}' \ -o out.flac--fail-with-body is not decoration: errors are JSON, and without it curl -o
writes an error body into out.flac as if it were audio. LUNAROUTE-API-KEY is
the recommended header; Authorization: Bearer lr_… is also accepted (see
Authentication).
The upload is multipart/form-data with exactly one file part and at most one
options part, in either order. The bytes are opaque to the gateway — the media
type is determined by inspecting content, not by the part’s Content-Type
header, and the part’s filename is advisory. An unknown or duplicated part is
rejected with 400 unsupported_parameter; nothing is silently ignored.
Recipes
Section titled “Recipes”Minimal: the defaults
Section titled “Minimal: the defaults”curl --fail-with-body https://gw.lunaroute.com/v1/audio/preprocess \ -H "LUNAROUTE-API-KEY: $LUNAROUTE_API_KEY" \ -F file=@meeting.wav \ -o meeting.opusOmitting options entirely is legal and is usually the right call: mono, 48 kHz,
48 kb/s Ogg Opus, highpass at 50 Hz, loudness normalised to −23 LUFS with a
−1 dBTP ceiling, denoise off.
Lossless benchmark artifact
Section titled “Lossless benchmark artifact”curl --fail-with-body https://gw.lunaroute.com/v1/audio/preprocess \ -H "LUNAROUTE-API-KEY: $LUNAROUTE_API_KEY" \ -F file=@speech.wav \ -F 'options={"audio":{"sample_rate":16000,"channels":1},"output":{"format":"flac"}}' \ -o speech.flacUse this when a benchmark harness needs lossless input and can afford the bytes. It is only a supported success if the produced file fits the cap; a long recording does not — see Output formats and sizes.
Noisy telephony
Section titled “Noisy telephony”curl --fail-with-body https://gw.lunaroute.com/v1/audio/preprocess \ -H "LUNAROUTE-API-KEY: $LUNAROUTE_API_KEY" \ -F file=@call.wav \ -F 'options={ "audio":{"channels":1}, "filter":{"highpass_hz":120,"notch_hz":[1000]}, "denoise":{"enabled":true,"engine":"rnnoise","mix":0.85}, "level":{"mode":"gain","gain_db":3.0}, "output":{"format":"opus","bitrate_kbps":24} }' \ -o call.opusThe filters run at the input rate — here a 1 kHz notch on an 8 kHz call — and
the delivered Opus timeline is 48 kHz regardless, which is why audio.sample_rate
is not set in this recipe. No WER or accuracy improvement is claimed for any
transform; measure on your own data.
Options reference
Section titled “Options reference”options is a JSON object. Unknown keys, duplicate JSON member names, wrong
types and unsupported combinations are all rejected — never ignored. Every
default below is applied recursively, so a partial object is fine.
| Option | Type | Default | Allowed values | Notes |
|---|---|---|---|---|
audio.sample_rate |
integer or "native" |
48000 for opus, 16000 otherwise |
"native", 8000, 16000, 22050, 32000, 44100, 48000 |
Opus delivers only its 48 kHz timeline, so 48000 is the only accepted rate for it. "native" keeps the input’s rate and must resolve to a rate the selected codec supports, else 422. Never silently resampled to a nearby rate. |
audio.channels |
integer or "native" |
1 |
1, 2, "native" |
Mono→stereo is dual mono; stereo→mono is (L+R)/2. More than two channels is 422 unsupported_channel_layout. |
audio.sample_format |
string | s16le |
s16le, f32le |
WAV only. FLAC accepts s16le only in v1 (f32le is 422). Opus and MP3 reject any explicit value (422): the format is not applicable. |
filter.highpass_hz |
integer | 50 |
0 or 20–500 |
0 disables the stage. Also limited by the input rate: it must stay at or below 0.45 × the input rate, and below an enabled lowpass_hz. |
filter.lowpass_hz |
integer | 0 |
0 or 3000–24000 |
0 disables the stage. Must sit strictly below the Nyquist of the input rate — filters run before the resampler. |
filter.notch_hz |
array of integers | [] |
at most 8 entries, each 20–20000, no duplicates |
Each notch must sit strictly below the Nyquist of the input rate. At an 8 kHz input, 3900 Hz is legal and 4000 Hz is not (4000 is at Nyquist). |
level.mode |
string | loudnorm |
loudnorm, gain, none |
The mode decides which of the three adjustment fields are allowed at all. |
level.target_lufs |
number | -23.0 |
-40.0 to -5.0 |
loudnorm only; explicit under gain or none is 422. |
level.true_peak_dbtp |
number | -1.0 |
-9.0 to 0.0 |
loudnorm only; explicit under gain or none is 422. |
level.gain_db |
number | 0.0 (under gain) |
-30.0 to 30.0 |
gain only; any explicit value under loudnorm — including 0.0 — is 422, because it contradicts the mode. |
denoise.enabled |
boolean | false |
true, false |
RNNoise runs as FFmpeg’s arnndn filter, with the model pinned by pipeline_version. |
denoise.engine |
string | rnnoise |
rnnoise, deepfilternet |
deepfilternet is a reserved value in v1: it answers 422 feature_not_available, and Capabilities reports it with "available": false. |
denoise.mix |
number | 1.0 |
0.0 to 1.0 |
0.0 is dry, 1.0 fully denoised; any value in between is a real dry/wet blend. An explicit mix while denoise.enabled is false is 422, even when it equals the default. |
output.format |
string | opus |
wav, flac, opus, mp3 |
Selects the response representation — it also decides what a present Accept must admit (see Errors). |
output.bitrate_kbps |
integer | 48 for opus and mp3 |
Opus: 8–320. MP3: 32–320 at 32000/44100/48000 Hz, 8–160 at 8000/16000/22050 Hz |
Lossy formats only — an explicit value with wav or flac is 422, not ignored. Total stream bitrate, not per channel. With audio.sample_rate="native" and MP3, only the values both MPEG profiles accept (32–160) are legal. |
output.filename |
string | derived from the upload’s name | any string; sanitised | Overrides the stem only: the extension is always forced from output.format. Sanitised to a basename, allowlisted to [A-Za-z0-9 ._()[]-], capped at 200 bytes; an unusable name falls back to audio.preprocessed.<ext>. |
analysis.speech_ratio |
boolean | false |
true, false |
Reserved and not implemented in v1: an explicit true is 422 feature_not_available. false is accepted and changes nothing. |
analysis.snr_estimate |
boolean | false |
true, false |
Reserved and not implemented in v1: an explicit true is 422 feature_not_available. |
Rules that depend on another option
Section titled “Rules that depend on another option”Presence is validated before mode-specific defaults are applied, so these are refused even when the value you sent happens to equal the default:
level.mode="gain": omittedgain_dbdefaults to0.0; explicittarget_lufsortrue_peak_dbtpis422.level.mode="loudnorm": any explicitgain_db, including0.0, is422.level.mode="none": any explicitgain_db,target_lufsortrue_peak_dbtpis422.denoise.enabled=false: an explicitmixis422.sample_formatis only meaningful for WAV;bitrate_kbpsonly for the lossy formats.- If both are enabled,
highpass_hzmust be belowlowpass_hz.
Removed in v1
Section titled “Removed in v1”filter.dc_offset— removed, not merely absent. The highpass stage is the DC rejection, and a second stage with no defined transfer function was unspecified. An integration that still sends it now gets422 invalid_optionwithfieldset to/filter/dc_offsetandreasonset tounknown_field, rather than a silently ignored option.
Output formats and sizes
Section titled “Output formats and sizes”output.bitrate_kbps is the total stream bitrate, not per channel. MB is
1,000,000 bytes; MiB is 1,048,576 bytes.
| Use | Format | One hour | Fits the 25 MiB output cap? |
|---|---|---|---|
| Lossless PCM interchange | WAV s16 | 115.20 MB / 109.86 MiB + header | No |
| Float interchange | WAV f32 | 230.40 MB / 219.73 MiB + header | No |
| Lossless benchmark artifact | FLAC s16 | Signal- and encoder-dependent; no universal size | Only if the actual bytes fit |
| Gateway-bound | Ogg Opus 48 kb/s | 21.60 MB / 20.60 MiB nominal + Ogg overhead | Expected for the pinned CBR profile |
| Human review | MP3 64 kb/s | 28.80 MB / 27.47 MiB nominal + metadata | No |
A one-hour lossless WAV exceeds the output cap and returns
413 output_too_large. The encoded result is checked against the cap before
a success is committed; v1 never splits, truncates, lowers the bitrate or
switches format for you. Clients that need a one-hour lossless hand-off must
preprocess locally.
Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Upload size | 25 MiB (26,214,400 bytes) per file part, counted while reading |
options size |
64 KiB of UTF-8 |
| Input duration | 3600 seconds |
| Request deadline | 600 seconds for the whole request |
| Channels | mono or stereo |
Being under one limit does not promise acceptance by another: an upload cap is
counted in bytes, so a compressed 25 MiB file can carry far more than an hour,
and the duration limit still refuses it with 400 audio_too_long.
A preprocessing request is admitted through the same organization concurrency gate as LunaRoute’s other managed work, so it counts against your concurrency allowance while it runs.
Response headers
Section titled “Response headers”Every 200 carries the delivered bytes plus the facts below.
| Header | Value | Meaning |
|---|---|---|
Content-Type |
audio/wav, audio/flac, audio/ogg (Opus), audio/mpeg (MP3) |
Matches output.format. |
Content-Disposition |
attachment; filename="…" |
Sanitised name; the extension always follows output.format. |
Content-Length |
bytes | Verify it after any transport interruption — see Determinism. |
Cache-Control, Pragma |
no-store, no-cache |
Never cached, at any hop. |
x-lunaroute-request-id |
UUID | The request’s server-minted id; present on errors too. Quote it when reporting a problem. |
X-Pipeline-Version |
version string | The certified pipeline manifest that produced these bytes. |
X-Config-Hash |
hex digest | Digest of the effective configuration plus the certified manifest. It identifies configuration, not audio — see below. |
X-Audio-Sha256 |
hex digest | SHA-256 of the body returned. This is the content identity. |
X-Input-Frames, X-Input-Sample-Rate |
integers | Exactly what was decoded: presentation frames and rate. |
X-Output-Frames, X-Output-Sample-Rate |
integers | Exactly what was delivered. |
X-Level-Status |
ok, peak_limited, loudness_unavailable, loudness_target_not_reached |
One value, decided from the delivered measurement. |
X-Level-Measured-Lufs, X-True-Peak-Dbtp |
number | Present only when a measurement exists. |
X-Analysis-Status |
not_analyzed |
Always present in v1: no speech ratio or SNR estimate is computed, so neither X-Speech-Ratio nor X-Snr-Estimate-Db is ever emitted. |
X-Warnings |
comma-separated tokens | Present only when non-empty. gapless_decoder_required is emitted for MP3. |
Two of them are the ones a benchmark should keep:
X-Audio-Sha256proves that two responses are the same bytes, and that every downstream call was handed identical input.X-Config-Hashproves that the same effective configuration was applied. It is a hash of the configuration — not of the audio. Two different files preprocessed with one configuration share a config hash and have differentX-Audio-Sha256values.
All of these headers are exposed to browser clients through CORS.
Determinism
Section titled “Determinism”For identical upload bytes, an identical effective configuration and the same
pipeline_version, the audio body bytes are byte-identical. That is the
property the endpoint exists to provide: a fusion harness can say by digest, not
by assertion, that three models got the same audio.
Timeline guarantees per output:
| Output | Guarantee |
|---|---|
WAV s16le / f32le |
Decoding yields exactly round_half_up(in_frames × out_rate / in_rate) frames. |
FLAC s16le |
Same frame count as WAV. |
| Ogg Opus | An RFC 7845-conforming decoder applying pre-skip and final-granule trimming yields the same count, on the 48 kHz timeline. |
| MP3 | Only the pinned reference decoder applying Xing/LAME gapless metadata yields it. Other decoders may expose priming and padding, which is why X-Warnings says gapless_decoder_required. |
What is not guaranteed:
- Resampling does not preserve sample indices, and a phase-changing filter does not keep every peak on the same sample index. The guarantee is about presentation time, not about the samples between the endpoints.
- Nothing about downstream decoding, resampling or feature extraction: the endpoint hands you one artifact, it cannot force a consumer to read it the way another consumer does.
- “Preprocessed” does not mean speech was found. No voice activity detection runs.
A transport interruption after the 200 was committed can leave a truncated
body, so verify Content-Length and X-Audio-Sha256. Range and resume are not
supported in v1.
Errors
Section titled “Errors”Errors use the standard envelope with the field and reason extensions
described on Errors & rate limits. For an option refusal,
field is the JSON pointer to the offending option (/filter/notch_hz) and
reason names the rule that failed; neither ever echoes the value you sent.
| Code | Status | Meaning |
|---|---|---|
invalid_request |
400 |
Malformed multipart body, a missing file part, an unusable or oversized options part, or options that is not JSON at all. |
unsupported_parameter |
400 |
An unknown multipart part, or a part that appeared more than once. |
invalid_option |
422 |
An option is out of range, contradicts the selected mode or format, is duplicated, or is not in the schema. field and reason say which — see the reason tokens below. |
feature_not_available |
422 |
A reserved value the schema accepts but v1 does not implement: denoise.engine="deepfilternet", or an explicit analysis.* = true. field names it. |
unsupported_channel_layout |
422 |
The input has more than two channels, or a layout v1 cannot read unambiguously. |
normalization_not_converged |
422 |
The delivered true peak still exceeded the ceiling after the pipeline’s correction renders, so no audio is returned. |
unsupported_media_type |
415 |
The upload is not a media file v1 can preprocess: nothing decodable, a chained Ogg, or a decode the pinned build could not complete. |
invalid_audio |
400 |
The container decodes but not into audio v1 accepts: more than one audio track to pick from, or non-finite samples. A file that is not decodable at all is 415 unsupported_media_type. |
audio_too_large |
413 |
The file part exceeded the upload cap. |
output_too_large |
413 |
The produced body would exceed the output cap; no audio is returned. |
audio_too_long |
400 |
The decoded input exceeded the duration cap. |
body_read_timeout |
408 |
The upload did not finish arriving within the request deadline. |
not_acceptable |
406 |
A present Accept excludes the media type of output.format. Absent Accept means any representation; the format is selected by output.format, never by negotiation. |
pipeline_version_unavailable |
409 |
The LUNAROUTE-PIPELINE-VERSION you pinned is not installed here. No silent fallback. |
processing_timeout |
504 |
The request exceeded its deadline. Inputs like this need a shorter file, not a longer client timeout. |
at_capacity |
503 |
Every preprocessing slot on this instance is busy. Retry. |
audio_lanes_exhausted |
429 |
Your organization is at its concurrency allowance. Back off for Retry-After and retry. |
audio_preprocess_disabled |
503 |
The feature is switched off for this deployment, or this instance is draining for shutdown. See Availability. |
routing_plan_not_found |
404 |
Only on the /r/{routing_plan_id} surface: the named routing plan does not exist. |
routing_plan_unavailable |
503 |
Only on the /r/{routing_plan_id} surface: the routing-plan store could not answer. Retry. |
internal_error |
500 |
The pipeline produced output it could not validate. The cause goes to the operator log; the body carries one fixed message and no detail. |
invalid_option reason tokens
Section titled “invalid_option reason tokens”reason |
Meaning |
|---|---|
out_of_range |
A numeric or frequency value outside its allowed range, including the post-probe Nyquist limit. |
unsupported_value |
A value the schema reads but this format cannot carry, e.g. bitrate_kbps outside the MP3 profile matrix. |
mutually_exclusive |
A field that the selected mode or format forbids, e.g. gain_db under loudnorm. |
duplicate |
The same notch frequency twice. |
too_many |
More than 8 notches. |
duplicate_member |
The same member name twice in one JSON object. serde-style “last one wins” is never applied. |
unknown_field |
A key the closed schema does not accept, including filter.dc_offset. |
invalid_type |
A value of the wrong type, or an enum value outside the schema, e.g. output.format="ogg". |
Retry 429 and 503 with bounded backoff, honouring Retry-After. Every 429
carries one. A timeout or a dropped connection has an ambiguous execution
outcome; a repeated POST is a new attempt — there is no idempotency key in v1.
Capabilities
Section titled “Capabilities”GET /v1/audio/preprocess/capabilitiesRead this instead of hardcoding anything from this page. The document describes the host that will serve you and is the published form of the enforced matrix:
| Field | Contents |
|---|---|
schema_version |
The request and capabilities schema revision. |
pipeline_version.default, pipeline_version.installed |
The version used when you do not pin one, and the versions you may pin. |
build.ffmpeg, build.rnnoise_model_sha256 |
The pinned build’s self-report and the denoise model’s digest. |
output.default_format, output.formats[] |
Per format: mime, extension, sample_rates, sample_formats, bitrate_kbps, default_sample_rate, default_bitrate_kbps. |
input.containers, input.sample_rates, input.channel_layouts, input.max_channels, input.max_duration_secs |
The input matrix. |
denoise.engines[] |
Each engine with available (and, for RNNoise, its filter, model digest, mix range and enabled_by_default). deepfilternet is advertised with "available": false. |
analysis |
speech_ratio and snr_estimate, both "available": false in v1. |
limits |
max_upload_bytes, max_output_bytes, max_concurrent_tasks, request_deadline_secs. |
pipeline_version is what a benchmark pins: it identifies the immutable manifest
of everything that can change a delivered byte. Send it as
LUNAROUTE-PIPELINE-VERSION to require an exact installed version — an
unavailable one answers 409 pipeline_version_unavailable before any work
starts, with no silent fallback. Old versions are not promised forever, so a
benchmark client keeps the artifact and records the version it used.
Availability
Section titled “Availability”Audio preprocessing is operator-gated. While it is switched off, every request —
this endpoint and the capabilities document — answers 503 audio_preprocess_disabled and the route stays registered rather than
disappearing. Treat that as “temporarily unavailable”: nothing about your request
is wrong, and retrying later is the right response.
The endpoint is served on the same auth surfaces as the rest of the API:
| Surface | Path |
|---|---|
| Header auth | /v1/audio/preprocess |
| Path auth | /a/{api_key}/v1/audio/preprocess |
| Project path | /a/{api_key}/p/{project}/v1/audio/preprocess |
| Routing plan | /r/{routing_plan_id}/v1/audio/preprocess |
GET /v1/audio/preprocess/capabilities answers on the same four. Responses on
the /a/... path surfaces carry Deprecation: true, like every other path-auth
route. A routing-plan id is attribution here only: preprocessing never resolves
a model, so no inference routing occurs.
Privacy
Section titled “Privacy”Uploaded audio, the produced audio, filenames and option payloads are never logged and never persisted. Request audio exists only for the life of the request. Operational and usage metadata — org, key, project, request id, status, byte counts, timing — is recorded.