> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kugelaudio.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Speech to text

> Guide to speech to text with Kugel Ear 1: transcribe recordings or live calls in nine European languages, choosing or detecting the language, word alternatives, KugelTurn end-of-turn and custom vocabulary.

Speech to text turns speech in nine European languages into text with Kugel
Ear 1 (`kugel-ear-1`). Send a finished recording and get its transcript back, or
stream a live call and read the words while the person is still speaking.

| Feature | What it does |
|---|---|
| Nine languages | German, English, French, Italian, Spanish, Portuguese, Dutch, Polish and Romanian. Name the language, or let the model detect it among the languages you list (German and English when you list none). |
| Word alternatives | Lists other likely spellings for words the recognizer may have written differently from what was meant, such as names, so your agent can ask one targeted question. |
| KugelTurn end-of-turn | On a live stream, the server hears when the speaker has finished and ends the turn, without a voice-activity signal from you. |
| Custom vocabulary | Names, brands and domain terms you send as boosted phrases, and the words of your project's [dictionaries](/features/dictionaries), come out in your spelling. |

## Choose how to send audio

| You have | Use | Reference |
|---|---|---|
| A recording, up to 300 seconds | `client.asr.transcribe`, or `POST /v1/audio/transcriptions` | [SDKs](/sdks/speech-to-text), [REST](/api-reference/endpoints/speech-to-text#upload-a-complete-recording) |
| Live audio, transcribed as it arrives | `client.asr.stream`, or the `/v1/audio/transcriptions/stream` WebSocket | [SDKs](/sdks/speech-to-text#stream-live-audio), [WebSocket](/api-reference/endpoints/speech-to-text#stream-live-audio) |

A stream sends revisable partial transcripts while the person speaks, then the
final transcript of each turn, and shortly after it a more accurate revision of
the same turn; [Stream a live call](#stream-a-live-call) shows how to read them.

## Quick start

Every request needs an [API key](/api-reference/authentication) allowed to use
**Speech to text**.

### Transcribe a recording

<CodeGroup>

```python Python
from kugelaudio import KugelAudio

client = KugelAudio(api_key="YOUR_API_KEY")

with open("call.wav", "rb") as f:
    result = client.asr.transcribe(f.read())

print(result.text, result.language)
```

```typescript TypeScript
import { readFile } from 'node:fs/promises';
import { KugelAudio } from 'kugelaudio';

const client = new KugelAudio({ apiKey: 'YOUR_API_KEY' });

const audio = new Blob([await readFile('call.wav')], { type: 'audio/wav' });
const result = await client.asr.transcribe({ audio, filename: 'call.wav' });
console.log(result.text, result.language);
```

```java Java
KugelAudio client = KugelAudio.fromEnv(); // reads KUGELAUDIO_API_KEY
TranscriptionResponse result = client.asr().transcribe(Files.readAllBytes(Path.of("call.wav")));
System.out.println(result.getText() + " " + result.getLanguage());
```

```bash cURL
curl https://api.kugelaudio.com/v1/audio/transcriptions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@call.wav"
```

</CodeGroup>

Uploads can be WAV, FLAC, Ogg or most other common container formats, at 8 to
48 kHz. Every argument and result field is in the
[SDK reference](/sdks/speech-to-text#transcribe-a-file).

### Stream a live call

`client.asr.stream` opens the stream, sends your audio as it goes and yields
every transcript the server sends back. This example streams a file, so you can
try it without a live source:

<CodeGroup>

```python Python
# pip install -U kugelaudio
import asyncio
from kugelaudio import KugelAudio, load_audio_stream

client = KugelAudio(api_key="YOUR_API_KEY")

async def main():
    audio = load_audio_stream("call.wav")  # mono, 16-bit
    async for event in client.asr.stream(audio):  # ends after the last turn
        if event.revises_final:
            print("revised:", event.partial_text, event.language)
        elif event.is_final:
            print("final:", event.partial_text, event.turn_end_reason)
        elif event.type == "partial":
            print("partial:", event.partial_text)

asyncio.run(main())
```

```typescript TypeScript
// npm install kugelaudio
import { KugelAudio, loadAudioStream } from 'kugelaudio';

const client = new KugelAudio({ apiKey: 'YOUR_API_KEY' });

const audio = await loadAudioStream('call.wav'); // mono, 16-bit
for await (const event of client.asr.stream(audio)) { // ends after the last turn
  if (event.revises_final) console.log('revised:', event.partial_text, event.language);
  else if (event.is_final) console.log('final:', event.partial_text, event.turn_end_reason);
  else if (event.type === 'partial') console.log('partial:', event.partial_text);
}
```

```java Java
// No streaming client in the Java SDK yet: open the
// /v1/audio/transcriptions/stream WebSocket as the SDK reference shows.
```

</CodeGroup>

Each turn of the call reaches you as a sequence of events. Check them in the
order of this table, as the example does:

| Event | How to recognize it | What to do with it |
|---|---|---|
| Revision | `revises_final` is true | A more accurate transcript of the turn just finalized, with its `language`. Replace the final's text where your application can still update it. |
| Final | `is_final` is true | The turn has ended. Hand `partial_text` to your agent; `turn_end_reason` says why the turn ended. |
| Partial | any other event whose `type` is `"partial"` | Show it as live text and replace it with the next one; the words can still change. |

Each `partial_text` is the whole turn so far, not the words added since the last
event. Word alternatives for the turn arrive in an extra `alternatives` event
after the final; see [Using word
alternatives](/api-reference/endpoints/speech-to-text#using-word-alternatives).

The connection stays open across turns, so one stream covers the whole call.
To transcribe live audio, such as a microphone or a phone call, pass its mono
16-bit PCM chunks as they arrive, as an iterable or async iterable, together
with their sample rate:

<CodeGroup>

```python Python
async for event in client.asr.stream(
    microphone_chunks(),  # any iterable or async iterable of PCM16 bytes
    sample_rate=16000,
    language=["de", "en"],
):
    ...
```

```typescript TypeScript
for await (const event of client.asr.stream(microphoneChunks(), { // any (async) iterable of PCM16 chunks
  sampleRate: 16000,
  language: ['de', 'en'],
})) {
  // ...
}
```

```java Java
// No streaming client in the Java SDK yet: send the sample rate and language
// in the config frame of the /v1/audio/transcriptions/stream WebSocket.
```

</CodeGroup>

When your source ends, the SDK tells the server so; the last turn ends and the
iteration finishes with it. The other stream options, such as turn detection
and boosted phrases, are in the
[SDK reference](/sdks/speech-to-text#stream-live-audio); without an SDK, the
[WebSocket protocol](/api-reference/endpoints/speech-to-text#stream-live-audio)
describes the same frames.

## Languages

Kugel Ear 1 transcribes these languages:

| Language | Code |
|---|---|
| German | `de` |
| English | `en` |
| French | `fr` |
| Italian | `it` |
| Spanish | `es` |
| Portuguese | `pt` |
| Dutch | `nl` |
| Polish | `pl` |
| Romanian | `ro` |

Choose the language with `language`, the same two-letter code the text to
speech API takes:

- **One code**, such as `"fr"`: the audio is transcribed in that language.
- **A list**, such as `["de", "fr", "it"]`: the model detects which of these is
  spoken.
- **`"auto"` or nothing**: the model detects whether German or English is
  spoken, and only those two.

So for any language other than German and English, send its code or include it
in the list; otherwise the audio is transcribed as German or English. Detection
runs once per recording for an upload, and per turn on a stream, so a
conversation that switches language between turns is followed. An unknown
code such as `"ja"`, or a form such as `"de-DE"` or `"German"`, is refused
rather than ignored. The result reports the language it was transcribed in as its code,
such as `"fr"`.

On a stream, the live partial transcripts are most accurate in English,
German, Spanish and French. The revision of each turn, the more accurate
transcript that follows its final, is transcribed in the language you chose.

KugelTurn ends turns in every one of these languages. On LiveKit's open
end-of-turn benchmark it takes fewer of a speaker's pauses for the end of the
turn than any published model in each language the benchmark shares with Kugel
Ear 1: German, English, French, Italian, Spanish, Portuguese and Dutch. Polish
and Romanian are not in the benchmark. The numbers per language are on the
[turn detection benchmark](https://kugelaudio.com/turn-detection-benchmark).

## Word alternatives

Names often have several spellings that sound the same (Catherine, Katherine,
Kathryn). For a word like that, the result lists the likely spellings with their
probabilities, most likely first. Pass them to the language model behind your
agent along with the transcript, so it can ask the caller about the one word in
doubt instead of asking them to spell everything. The list is conservative and
never delays or replaces the transcript; see
[Using word alternatives](/api-reference/endpoints/speech-to-text#using-word-alternatives).

## End of turn with KugelTurn

On a stream, you send the audio as it is, pauses included, and the server ends
each turn when the speaker has finished. By default KugelTurn, our end-of-turn
model, decides from a short pause, and a pause it still holds after a few
seconds ends the turn anyway. The `config` frame can choose a
conservative level that holds longer through a pause mid-thought, a fixed pause
instead of KugelTurn, or turns that end only when you say so. The options are in
[Client frames](/api-reference/endpoints/speech-to-text#client-frames).

## Custom vocabulary

Tell the recognizer which names, brands and terms to expect, and it writes them
your way where the audio fits them:

- **Boosted phrases** you send with a request or a stream's `config` frame.
- **Your [dictionaries](/features/dictionaries)**, the same ones that shape
  text to speech: the written word of every entry applies to transcription too,
  without sending anything. Choose dictionaries per request, or none.

A phrase is a hint, not a replacement rule. List what speakers in this audio may
actually say: the closer the list is to the call, the more it helps. Limits and
examples are in
[Custom vocabulary](/api-reference/endpoints/speech-to-text#custom-vocabulary).

## Access, billing and limits

- Speech to text has to be enabled for your organization; until it is, requests
  are refused with `403` and `Transcription is not enabled for this
  organization.`
- The API key needs the **Speech to text**
  [permission](/api-reference/authentication#key-permissions).
- Usage is billed from your organization's credits; see
  [pricing](https://kugelaudio.com/pricing). A spent balance is refused with
  `402 INSUFFICIENT_CREDITS`.
- An upload, and a turn of a stream, can be up to 300 seconds long.
- No word timestamps and no speaker separation (diarization) today.

Every status and close code is in the
[API reference](/api-reference/endpoints/speech-to-text#errors); the shared error
format is in [Error codes](/api-reference/errors).

## Next steps

<CardGroup cols={2}>
  <Card title="Python SDK" icon="python" href="/sdks/speech-to-text?lang=python">
    Transcribe a recording and stream live audio
  </Card>
  <Card title="TypeScript SDK" icon="js" href="/sdks/speech-to-text?lang=typescript">
    The same for Node.js
  </Card>
  <Card title="Java SDK" icon="java" href="/sdks/speech-to-text?lang=java">
    The same for the JVM
  </Card>
  <Card title="API reference" icon="code" href="/api-reference/endpoints/speech-to-text">
    The REST and WebSocket wire protocol
  </Card>
</CardGroup>
