Audio Separation
Split a soundtrack into Dialogue, Music, Ambience, Effects and a without-dialogue (M&E) track with Kolbo's stem separation pipeline.
Split any soundtrack into separate audio layers — Dialogue, Music, Ambience, Effects, and a without-dialogue (M&E) track. Built for dubbing and localisation, podcast cleanup, re-scoring, and handing an editor usable stems.
This is Kolbo's own multi-pass pipeline, not a single "vocal remover" call. The layers are pulled out one after another from the remainder of the previous pass — speech first, then music from what is left, then ambience from what is left after that — so the lanes stay disjoint instead of each one being extracted independently from the full mix. A second pass also lifts cinematic impacts and whooshes out of the Music lane and folds them into Effects.
These endpoints answer inline. There is no generation id to poll — the response body
carries the finished layer URLs. A typical clip takes 20–90 seconds. Do not call
/v1/generate/:id/status on the result.
On the
audio_urlpath, all three endpoints accept an optionalproject_idbody field that bills against and files the work into a specific project (otherwise your default API project is used). On thegeneration_idpath,project_idis ignored — the generation's own project is billed. See Projects.
Endpoints
POST /api/v1/audio/separate # the main split
POST /api/v1/audio/clean-dialogue # remove voice leaking into the M&E track
POST /api/v1/audio/ambience # pull room tone out of the Effects (or Music) bedPricing
Every stems action is priced by the length of the audio it processes:
20 credits per started 30 seconds. A 45-second clip costs 40 credits; a 3-minute clip costs
120. The duration is measured on the file that action actually processes — the source for
separate, the M&E track for clean-dialogue, the Effects/Music bed for ambience. The exact
amount charged comes back as credits_used.
Before any work starts, the request is refused if your balance is below 20 credits. The exact, duration-based price is then checked against your balance before anything is reserved.
Two ways to name the source
Every endpoint takes one of:
| Field | Type | Description |
|---|---|---|
audio_url | string | Any public audio or video URL — a Kolbo CDN asset, an upload_media result, or any direct link. Aliases video_url, media_url, file_url and url are accepted and behave identically (the first non-empty one wins). |
generation_id | string | The id of a Kolbo video generation you already created, and can edit. |
If you send both, generation_id wins and the URL is ignored. The two paths differ in important ways:
audio_url | generation_id | |
|---|---|---|
| Result storage | Layer URLs returned to you; nothing is stored against the source | Layers are also saved onto the generation |
| Repeat calls | Not cached — separates again and bills again | Cached; a repeat call returns the same layers for 0 credits (cached: true). A full split also answers a later dialogue request, but a dialogue-only split is re-run (and billed) when you ask for full |
| Source limits | Up to 15 minutes and 2 GB | Whatever the generation is |
project_id | Honoured | Ignored — the generation's project is billed |
| Visible in the web app | As a stem-split entry in Audio Studio history | Yes — the Shot Editor mixer reads the same layers |
| Extra response fields | has_dialogue, has_music (separate); passes, margin_db, already_clean (clean-dialogue); skipped, levels (ambience) | generation_id, cached |
Prefer generation_id whenever the source is a Kolbo generation.
POST /v1/audio/separate
Splits the soundtrack.
Request
| Field | Type | Required | Description |
|---|---|---|---|
audio_url | string | No* | Public audio/video URL to separate (aliases: video_url, media_url, file_url, url) |
generation_id | string | No* | Kolbo video generation id to separate instead |
mode | string | No | "full" (default) — every layer. "dialogue" — a single pass returning only dialogue and me (plus original). Any other value returns 400 INVALID_MODE |
project_id | string | No | Project to bill against and file into (audio_url path only) |
*One of audio_url / generation_id is required.
curl -X POST https://api.kolbo.ai/api/v1/audio/separate \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{"audio_url": "https://media.kolbo.ai/uploads/interview.mp4", "mode": "full"}'Response
audio_url path, mode: "full":
{
"success": true,
"source": "url",
"layers": [
{ "type": "original", "label": "Original", "url": "https://media.kolbo.ai/.../original.wav" },
{ "type": "dialogue", "label": "Dialogue", "url": "https://media.kolbo.ai/.../dialogue.wav" },
{ "type": "music", "label": "Music", "url": "https://media.kolbo.ai/.../music.wav" },
{ "type": "ambience", "label": "Ambience", "url": "https://media.kolbo.ai/.../ambience.wav" },
{ "type": "sfx", "label": "Effects", "url": "https://media.kolbo.ai/.../sfx.wav" },
{ "type": "me", "label": "Without dialogue", "url": "https://media.kolbo.ai/.../me.wav" }
],
"has_dialogue": true,
"has_music": true,
"credits_used": 40,
"credit_usage_id": "6650f1c2..."
}generation_id path:
{
"success": true,
"source": "generation",
"generation_id": "664f...",
"layers": [ { "type": "original", "label": "Original", "url": "..." } ],
"cached": false,
"credits_used": 40,
"credit_usage_id": "6650f1c2..."
}All layers are WAV files on the Kolbo CDN. Do not assume the stems are stereo — the separation model can return mono even for a stereo source.
type | What it contains |
|---|---|
original | The untouched mix, for reference |
dialogue | Speech only |
music | Score / song bed (full mode only) |
ambience | Room tone / atmosphere (full mode only) |
sfx | Effects and Foley, including impacts lifted out of the music (full mode only) |
me | Everything except dialogue — the track you dub over |
Every layer of the chosen mode is returned, even when it is near-silent. A talking-head clip
with no score still returns a music layer. Read has_dialogue / has_music (audio_url path)
to tell whether those lanes actually carry sound before presenting them to a user, and branch on
the type values present rather than on array positions.
POST /v1/audio/clean-dialogue
Strips voices still faintly audible in an M&E track. Billed at the stems rate on the length of the M&E track.
Use this only when residual speech in the M&E actually matters to you — it trades fidelity to do its job, reconstructing the bed less cleanly than the split that produced it. It runs up to three removal passes and stops as soon as a pass pulls nothing meaningful. It is an escalation, not a step in the normal flow.
| Field | Type | Required | Description |
|---|---|---|---|
audio_url | string | No* | The me layer URL returned by /v1/audio/separate (same aliases as above) |
generation_id | string | No* | Kolbo video whose stems were already separated — cleans its M&E layer in place. Returns 400 if it has not been split yet |
project_id | string | No | Project to bill against (audio_url path only) |
*One of audio_url / generation_id is required.
{
"success": true,
"source": "url",
"layers": [{ "type": "me", "label": "Without dialogue", "url": "https://media.kolbo.ai/.../me.wav" }],
"passes": 2,
"margin_db": -38.2,
"already_clean": false,
"credits_used": 40,
"credit_usage_id": "6650f1c2..."
}passes is how many removal passes were kept (0–3). margin_db is the level of the last
attempted pull relative to the track it was pulled from. already_clean: true (passes: 0)
means the analysis found nothing worth removing and url is the track you sent.
On the generation_id path the response is the generation shape (source: "generation",
generation_id, the full updated layers list, cached). A generation whose M&E was already
cleaned returns cached: true for 0 credits.
The analysis pass runs — and is billed — even when the track turns out to be already clean.
Check already_clean before telling a user something was removed.
POST /v1/audio/ambience
Pulls room tone / atmosphere out of an Effects or Music bed, leaving hard effects behind. Billed at the stems rate on the length of the bed.
A full-mode separate already returns an ambience layer, so this endpoint is mainly for beds
you did not split with Kolbo, or for a music lane you want to pull further.
| Field | Type | Required | Description |
|---|---|---|---|
audio_url | string | No* | The sfx (or music) layer URL returned by /v1/audio/separate (same aliases as above) |
source_type | string | No | audio_url path only. Which lane audio_url came from — "sfx" (default), "music" or "me". Sets the type of the leftover layer; unknown values fall back to "sfx" |
generation_id | string | No* | Kolbo video whose stems were already separated — picks the source lane automatically (sfx, then music, then me) |
project_id | string | No | Project to bill against (audio_url path only) |
*One of audio_url / generation_id is required.
{
"success": true,
"source": "url",
"layers": [
{ "type": "ambience", "label": "Ambience", "url": "https://media.kolbo.ai/.../ambience.wav" },
{ "type": "sfx", "label": "Effects", "url": "https://media.kolbo.ai/.../sfx.wav" }
],
"skipped": false,
"levels": { "target": -24.1, "residual": -31.7 },
"credits_used": 20,
"credit_usage_id": "6650f1c2..."
}levels are the mean levels (dBFS) of the pulled ambience (target) and what was left
(residual). The leftover layer is only returned when it is audible — if the whole bed was
ambience, you get the ambience layer alone. skipped: true means the bed had no separable
ambience and layers is empty. As with clean-dialogue, the pass ran and is billed.
On the generation_id path the response is the generation shape with the full updated layers
list. A generation split in full mode already includes ambience, so the call returns it as
cached: true.
Errors
Error bodies carry success: false, a human-readable error (some codes use message instead),
and a code where one applies.
| Code | Status | Meaning |
|---|---|---|
INVALID_MODE | 400 | mode is not full or dialogue |
MISSING_SOURCE | 400 | Neither audio_url (or an alias) nor generation_id was provided |
NO_AUDIO | 400 | The file has no audio stream, or its audio is digital silence. Checked before anything is charged |
SOURCE_TOO_LONG | 400 | The audio_url source is longer than 15 minutes. Trim it or split it into sections. Also checked before charging |
STEMS_REQUIRED / NO_PRO_SOURCE | 400 | (ambience + generation_id) The generation has not been split with the current pipeline, or has no Effects/Music/M&E lane |
INSUFFICIENT_CREDITS | 403 | Balance too low. The 20-credit pre-check includes required and balance; the duration-based check includes required and available on the audio_url path only |
SUBSCRIPTION_INACTIVE | 403 | The paying account cannot spend credits |
| — | 404 | (generation_id only) Generation not found or you lack edit access |
STEMS_IN_PROGRESS / STEMS_REFINE_IN_PROGRESS / STEMS_PRO_IN_PROGRESS | 409 | (generation_id only) The same operation on that generation is already running |
STEMS_BILLING_PENDING / STEMS_BILLING_IN_PROGRESS | 409 | (generation_id only) A previous run's billing is still settling — retry shortly |
| — | 429 | More than 10 accepted requests per minute. This body uses status: false (not success) and includes error, message, limit, window_seconds and retryAfter, plus a Retry-After header |
Rate limits and timeouts
- 10 requests/minute per account across the three endpoints. Only accepted requests count; a rejected call does not spend the budget. The web app's separation surface has its own, equal 10/minute ceiling.
- The route holds its connection for the whole run and gives up at 10 minutes. Set your HTTP client's timeout above that; a shorter client timeout aborts your side while the server keeps working, and you lose the result.
- Concurrent calls are independent — but each one spends credits, and the
audio_urlform has no cache. Two identical calls separate twice and bill twice.
Related
- Transcription — if you want the words as text, not the audio layers
- Video Editing —
generate_audioadds an AI-generated audio layer; it is not the inverse of this endpoint - Media Library —
POST /v1/media/uploadto get a public URL for a local file first