Skip to content
KugelAudioDocs

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

POST/v1/audio/enhance

Send a multipart/form-data upload.

model
string
body
required

clarity-1. Any other value is rejected.

file
file
body
required

The recording, as a WAV file. At most 300 seconds, and at most 64 MB for the whole request.

task
string
bodydefault: noise_removal

noise_removal or target_speaker_extraction.

speaker
file
body

Required 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-Timeout
number
header

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

Errors

Errors return the standard JSON error body, and every response carries an x-request-id header; see Error Codes.

Statuserror_codeTrigger
400VALIDATION_ERRORUnreadable 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.
401UNAUTHORIZEDMissing or invalid API key.
402INSUFFICIENT_CREDITSPaid 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).
402FREE_PERIOD_ENDEDFree plan only: the organization's free Clarity month is over; switch to paid. See Free month and paid plan.
403UNAUTHORIZEDSpeech enhancement is not enabled for this organization.
403PERMISSION_DENIEDThe API key's permissions do not allow Speech enhancement.
413VALIDATION_ERRORThe request is larger than 64 MB. Send 16-bit mono audio, or a shorter recording.
429RATE_LIMITEDYour plan's requests-per-minute limit (with Retry-After) or concurrency limit (without) is reached. See Limits and errors.
502INTERNAL_ERROREnhancement failed while processing the request. Retry.
503MODEL_UNAVAILABLESpeech enhancement is at capacity. Please try again shortly. Retry after the Retry-After seconds (currently 5).

Enhance live audio

WSS/v1/audio/enhance/stream

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

  1. Client → text config:
    JSON
    {"type": "config", "model": "clarity-1", "task": "noise_removal", "sample_rate_hz": 16000, "encoding": "pcm_s16le"}
    model is required and must be clarity-1. task is noise_removal or target_speaker_extraction. sample_rate_hz is 8000–48000. For target_speaker_extraction, add "speaker_wav_b64": the speaker sample as a base64-encoded WAV file, 2–8 seconds.
  2. Server → text {"type": "ready", "sample_rate_hz": 24000, "encoding": "pcm_s16le"}.
  3. Client → binary: mono 16-bit little-endian PCM at your declared rate, at most 1 second of audio per message.
  4. Server → binary: enhanced mono 16-bit little-endian PCM at 24 kHz, sent as it is produced.
  5. Client → text {"type": "end"}. The server sends the remaining audio, then {"type": "done", "duration_s": <seconds>}, and closes with 1000. The total output matches your input's duration.
Python
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:

  1. Client → text the same config as above. This is when the audio counts against your per-minute and concurrency limits.
  2. Server → text {"type": "ready", "sample_rate_hz": 24000, "encoding": "pcm_s16le", "request_id": "<id>"}. request_id names this audio.
  3. Audio in and out, then {"type": "end"}, exactly as above.
  4. Server → text {"type": "done", "duration_s": <seconds>, "request_id": "<id>"}. The connection stays open for the next config, 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.

LimitValue
Open sessions per organizationTwice 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 audios60 seconds without a config, then the server closes with 1000. Inside an audio, 30 seconds without audio still closes with 4000.
Connection lifetimeOne 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:

JSON
{"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

POST/v1/audio/enhance/warmup

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