Skip to content
LUNAROUTEDocs

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.

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.

Terminal window
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.

Terminal window
curl --fail-with-body https://gw.lunaroute.com/v1/audio/preprocess \
-H "LUNAROUTE-API-KEY: $LUNAROUTE_API_KEY" \
-F file=@meeting.wav \
-o meeting.opus

Omitting 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.

Terminal window
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.flac

Use 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.

Terminal window
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.opus

The 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 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.

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": omitted gain_db defaults to 0.0; explicit target_lufs or true_peak_dbtp is 422.
  • level.mode="loudnorm": any explicit gain_db, including 0.0, is 422.
  • level.mode="none": any explicit gain_db, target_lufs or true_peak_dbtp is 422.
  • denoise.enabled=false: an explicit mix is 422.
  • sample_format is only meaningful for WAV; bitrate_kbps only for the lossy formats.
  • If both are enabled, highpass_hz must be below lowpass_hz.
  • 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 gets 422 invalid_option with field set to /filter/dc_offset and reason set to unknown_field, rather than a silently ignored option.

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.

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.

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-Sha256 proves that two responses are the same bytes, and that every downstream call was handed identical input.
  • X-Config-Hash proves 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 different X-Audio-Sha256 values.

All of these headers are exposed to browser clients through CORS.

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 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.
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.

Terminal window
GET /v1/audio/preprocess/capabilities

Read 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.

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.

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.