Speech Enhancement API
REST and WebSocket reference for speech enhancement with Clarity: remove background noise or keep one speaker's voice in recordings and live audio.
The enhancement endpoints run clarity-1 on a recording (REST) or on live
audio (WebSocket). Each request does one task: noise_removal removes
background noise and keeps the speech; target_speaker_extraction keeps the
one person in a speaker sample you send and removes every other voice and
sound. The output is always mono 16-bit PCM at 24 kHz, the same length as your
input.
All requests need an API key; see Authentication.
Your organization also needs access to clarity-1.
This page is the wire protocol. The Python and TypeScript SDKs wrap both endpoints (SDK reference); the free month, the paid plan, the rate limits and what to do about each error are in the Speech enhancement guide.
Enhance a recording
/v1/audio/enhanceSend a multipart/form-data upload.
modelclarity-1. Any other value is rejected.
fileThe recording, as a WAV file. At most 300 seconds, and at most 64 MB for the whole request.
tasknoise_removalnoise_removal or target_speaker_extraction.
speakerRequired for target_speaker_extraction, rejected for noise_removal. A WAV
sample of the person to keep, 2–8 seconds long. See
A good speaker sample.
X-Queue-TimeoutHow many seconds the request may wait for capacity before it is refused with
503 MODEL_UNAVAILABLE, for clients that would rather fail fast than wait. It
can only shorten the server's own wait, never lengthen it. Omit it to wait as
long as the server allows.
Accepted WAV formats for both file and speaker: 16-, 24- or 32-bit
integer PCM, or 32-bit float; mono or stereo (stereo is mixed down to mono);
sample rates from 8 kHz to 48 kHz.
Response 200: an audio/wav body, mono 16-bit PCM at 24 kHz, the same
duration as the input. The X-Audio-Duration-Seconds header carries that
duration.
curl https://api.kugelaudio.com/v1/audio/enhance \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "model=clarity-1" \
-F "file=@call.wav" \
-F "task=noise_removal" \
-o call-clean.wavcurl https://api.kugelaudio.com/v1/audio/enhance \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "model=clarity-1" \
-F "file=@meeting.wav" \
-F "task=target_speaker_extraction" \
-F "speaker=@speaker.wav" \
-o one-voice.wavErrors
Errors return the standard JSON error body, and every response carries an
x-request-id header; see Error Codes.
| Status | error_code | Trigger |
|---|---|---|
400 | VALIDATION_ERROR | Unreadable or unsupported WAV, empty audio, input over 300 seconds, missing or unsupported model, sample rate outside 8–48 kHz, unknown task, speaker missing for target_speaker_extraction or sent with noise_removal, a speaker sample outside 2–8 seconds, or an X-Queue-Timeout that is not a positive number. |
401 | UNAUTHORIZED | Missing or invalid API key. |
402 | INSUFFICIENT_CREDITS | Paid plan only: the organization's remaining credits do not cover the input audio (checked before processing; for a stream, the one-second minimum at the handshake). |
402 | FREE_PERIOD_ENDED | Free plan only: the organization's free Clarity month is over; switch to paid. See Free month and paid plan. |
403 | UNAUTHORIZED | Speech enhancement is not enabled for this organization. |
403 | PERMISSION_DENIED | The API key's permissions do not allow Speech enhancement. |
413 | VALIDATION_ERROR | The request is larger than 64 MB. Send 16-bit mono audio, or a shorter recording. |
429 | RATE_LIMITED | Your plan's requests-per-minute limit (with Retry-After) or concurrency limit (without) is reached. See Limits and errors. |
502 | INTERNAL_ERROR | Enhancement failed while processing the request. Retry. |
503 | MODEL_UNAVAILABLE | Speech enhancement is at capacity. Please try again shortly. Retry after the Retry-After seconds (currently 5). |
Enhance live audio
/v1/audio/enhance/streamConnect to wss://api.kugelaudio.com/v1/audio/enhance/stream and authenticate
in the handshake, as for any WebSocket
(Authentication).
The API key, access to clarity-1, your plan (the free month, or your credits
on paid) and its rate limits are all checked during the handshake, before the connection opens. A refusal is an
HTTP response with the status and error_code from the table above,
the standard error body and, where it applies, Retry-After; see
Refused WebSocket upgrades.
- Client → text
config:JSON{"type": "config", "model": "clarity-1", "task": "noise_removal", "sample_rate_hz": 16000, "encoding": "pcm_s16le"}modelis required and must beclarity-1.taskisnoise_removalortarget_speaker_extraction.sample_rate_hzis 8000–48000. Fortarget_speaker_extraction, add"speaker_wav_b64": the speaker sample as a base64-encoded WAV file, 2–8 seconds. - Server → text
{"type": "ready", "sample_rate_hz": 24000, "encoding": "pcm_s16le"}. - Client → binary: mono 16-bit little-endian PCM at your declared rate, at most 1 second of audio per message.
- Server → binary: enhanced mono 16-bit little-endian PCM at 24 kHz, sent as it is produced.
- Client → text
{"type": "end"}. The server sends the remaining audio, then{"type": "done", "duration_s": <seconds>}, and closes with1000. The total output matches your input's duration.
import asyncio
import base64
import json
import os
import wave
import websockets
URL = "wss://api.kugelaudio.com/v1/audio/enhance/stream"
async def main():
headers = {"Authorization": f"Bearer {os.environ['KUGELAUDIO_API_KEY']}"}
speaker = open("speaker.wav", "rb").read()
out = bytearray()
with wave.open("meeting.wav", "rb") as src: # mono, 16-bit
rate = src.getframerate()
async with websockets.connect(URL, additional_headers=headers) as ws:
await ws.send(json.dumps({
"type": "config",
"model": "clarity-1",
"task": "target_speaker_extraction",
"sample_rate_hz": rate,
"encoding": "pcm_s16le",
"speaker_wav_b64": base64.b64encode(speaker).decode(),
}))
ready = json.loads(await ws.recv())
if ready["type"] != "ready":
raise RuntimeError(ready)
async def send():
while chunk := src.readframes(rate // 5): # 200 ms per message
await ws.send(chunk)
await ws.send(json.dumps({"type": "end"}))
sender = asyncio.create_task(send())
async for message in ws:
if isinstance(message, bytes):
out += message # mono PCM16 at 24 kHz
continue
event = json.loads(message)
if event["type"] == "error":
raise RuntimeError(event)
if event["type"] == "done":
break
await sender
with wave.open("one-voice.wav", "wb") as dst:
dst.setnchannels(1)
dst.setsampwidth(2)
dst.setframerate(24000)
dst.writeframes(bytes(out))
asyncio.run(main())Several audios on one connection
Add session=1 to the URL to send one audio after another over the same
connection, for example to open it while your agent starts and have it ready
when the first user speaks:
wss://api.kugelaudio.com/v1/audio/enhance/stream?session=1.
Without session=1 the connection carries exactly one audio, as described above.
The handshake checks the API key, access to clarity-1, your plan and (paid
plan) that your credits cover at least one second, and counts the connection
against your organization's open-session limit. It does not use a request from
your per-minute limit or a concurrent-request slot; each audio does.
For each audio:
- Client → text the same
configas above. This is when the audio counts against your per-minute and concurrency limits. - Server → text
{"type": "ready", "sample_rate_hz": 24000, "encoding": "pcm_s16le", "request_id": "<id>"}.request_idnames this audio. - Audio in and out, then
{"type": "end"}, exactly as above. - Server → text
{"type": "done", "duration_s": <seconds>, "request_id": "<id>"}. The connection stays open for the nextconfig, which may use a different task, speaker sample or sample rate.
Send {"type": "close"} (or close the WebSocket) between audios to end the
session; the server closes with 1000.
| Limit | Value |
|---|---|
| Open sessions per organization | Twice your plan's concurrency limit (none when concurrency is unlimited). Over it, the handshake is refused with 429 RATE_LIMITED, Open session limit reached (N), without Retry-After. |
| Time between audios | 60 seconds without a config, then the server closes with 1000. Inside an audio, 30 seconds without audio still closes with 4000. |
| Connection lifetime | One hour. An audio still running at that point finishes first; the server then closes with 1000. |
Each audio is billed on its own, for its input seconds (rounded up, one-second
minimum), under its request_id. If a config is over your per-minute or
concurrency limit, you get an error message with RATE_LIMITED (and
retry_after for the per-minute limit) and the connection stays open; send the
config again later. Every other error inside an audio closes the connection
as described below. During a rolling deploy the server finishes the audio in
progress, then closes with 1012; reconnect and continue.
Errors and close codes
After the connection opens, an error arrives as an error message in the
standard error shape, with the connection's
request_id and, when a retry makes sense, retry_after in seconds:
{"type": "error", "error": "Invalid request", "error_code": "VALIDATION_ERROR", "code": 400, "request_id": "9f2c1b7e4a6d4c8fb0e5a3d719c26f84"}The server then closes the connection with the close code for that
error_code, the same codes text-to-speech uses; see
WebSocket close codes. For this
endpoint, 1000 is the normal close after done (for a session, after
close, 60 idle seconds or one hour); 1012 means the server is restarting,
so reconnect; 4000 means an invalid
config or audio message, no audio for 30 seconds, or a stream longer than one
hour of audio; 4003 means INSUFFICIENT_CREDITS or FREE_PERIOD_ENDED; and
1011 means enhancement failed mid-stream.
Warm up before audio
/v1/audio/enhance/warmupEnhancement capacity scales down when nobody uses it, so the first request
after a quiet period can wait several seconds before it starts. If you know
audio is coming (a user opened your recording screen, a call is about to
connect), send this request first. It answers right away and readies
enhancement in the background, so the request that follows starts without
that wait. It needs the same API key and clarity-1 access as the other
endpoints, takes no body, and is not billed.
Response 202: {"status": "warming"}.
curl -X POST https://api.kugelaudio.com/v1/audio/enhance/warmup \
-H "Authorization: Bearer YOUR_API_KEY"Errors are the 401, 402 FREE_PERIOD_ENDED and 403 rows of the recording
endpoint's error table.