Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion api-reference/asyncapi-asr-realtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,7 @@ components:
drafts: true
ignore_timestamps: true
diarize: true
tag_audio_events: true
vad_events: false
eot_events: false
limits:
Expand Down Expand Up @@ -780,7 +781,7 @@ components:
type: integer
text:
type: string
description: The final transcript of the turn. Can contain speaker tags such as `<|speaker:0|>` and sound tags such as `[laughter]`. Can be empty.
description: The final transcript of the turn. Can contain speaker tags such as `<|speaker:0|>` (unless `results.diarize` is `false`) and sound tags such as `[laughter]` (unless `results.tag_audio_events` is `false`). Can be empty.
words:
type: array
items:
Expand Down Expand Up @@ -1175,6 +1176,10 @@ components:
- "false"
default: true
description: Speaker tags in the text. `auto` is the same as `true`; `false` removes them. The strings `"true"` and `"false"` are accepted too.
tag_audio_events:
type: boolean
default: true
description: Sound tags such as `[laughter]` in the text; `false` removes them. Only `true` or `false`.
vad_events:
type: boolean
default: false
Expand Down Expand Up @@ -1229,6 +1234,9 @@ components:
- "true"
- "false"
description: Speaker tags in the text. `auto` is the same as `true`; `false` removes them. The strings `"true"` and `"false"` are accepted too.
tag_audio_events:
type: boolean
description: Sound tags such as `[laughter]` in the text; `false` removes them. Only `true` or `false`.
vad_events:
type: boolean
description: Send `vad` messages.
Expand Down
23 changes: 16 additions & 7 deletions api-reference/endpoint/websocket/asr-realtime.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
asyncapi: "/api-reference/asyncapi-asr-realtime.yml asrRealtime"

Check warning on line 2 in api-reference/endpoint/websocket/asr-realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

api-reference/endpoint/websocket/asr-realtime.mdx#L2

Did you really mean 'asrRealtime'?
title: "WebSocket Realtime Speech to Text"

Check warning on line 3 in api-reference/endpoint/websocket/asr-realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

api-reference/endpoint/websocket/asr-realtime.mdx#L3

Did you really mean 'Realtime'?
description: "Stream audio over a WebSocket and receive transcripts turn by turn, with turn detection, live drafts and conversation context"
icon: "webhook"
iconType: "solid"
Expand All @@ -11,7 +11,7 @@
Connect to `wss://api.fish.audio/v1/asr/realtime` with a WebSocket client.
Every message in both directions is one MessagePack map in a binary frame. A
normal HTTP GET request does not start a session. For an introduction, see
[Realtime Speech to Text](/features/speech-to-text/realtime).

Check warning on line 14 in api-reference/endpoint/websocket/asr-realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

api-reference/endpoint/websocket/asr-realtime.mdx#L14

Did you really mean 'Realtime'?
</Note>

<Note>`transcribe-stream-1` is in preview.</Note>
Expand All @@ -20,7 +20,7 @@

You need a [Fish Audio API key](/developer-guide/getting-started/api-key) and a WebSocket client that can set request headers. The API key travels in a request header, so connect from your server, not from a browser; to capture audio in a browser, relay it through your server as in [Transcribe a live audio stream](/developer-guide/sdk-guide/cookbook/realtime-transcription#capture-from-a-browser-microphone). The examples on these pages use Python with `websockets` and `msgpack`, and Node.js with `ws` and `@msgpack/msgpack`.

This endpoint is separate from the realtime endpoints of the [compatible APIs](/developer-guide/compat/capabilities), which use manual commit and have no server-side turn detection.

Check warning on line 23 in api-reference/endpoint/websocket/asr-realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

api-reference/endpoint/websocket/asr-realtime.mdx#L23

Did you really mean 'realtime'?

## Connect

Expand All @@ -33,11 +33,11 @@
| Header | Required | Description |
| --------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `Authorization` | Yes | `Bearer <API key>`. Write the scheme exactly as `Bearer`, followed by a space and the key. |
| `model` | No | `transcribe-stream-1` (Preview), the default and the only realtime model. The legacy name `transcribe-1-pro-realtime` reaches the same model. |

Check warning on line 36 in api-reference/endpoint/websocket/asr-realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

api-reference/endpoint/websocket/asr-realtime.mdx#L36

Did you really mean 'realtime'?

Each open connection counts against the same concurrency limit as your other API requests, from the moment it opens until it closes, even while you send no audio. See [Concurrent request limits](/developer-guide/models-pricing/pricing-and-rate-limits#concurrent-request-limits). A session cannot be resumed: a new connection is a new session.

Sessions are billed per second of session duration, as Realtime ASR. See [ASR pricing](/developer-guide/models-pricing/pricing-and-rate-limits#automatic-speech-recognition-asr-models).

Check warning on line 40 in api-reference/endpoint/websocket/asr-realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

api-reference/endpoint/websocket/asr-realtime.mdx#L40

Did you really mean 'Realtime'?

If the server refuses the upgrade, it answers with a plain HTTP response whose JSON body is `{"status": <int>, "message": <str>}`:

Expand All @@ -45,7 +45,7 @@
| ------ | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| 401 | The API key is missing or invalid, or the `Authorization` header does not start with `Bearer `. | Check the key and the header. |
| 402 | Insufficient API credit. | Top up your balance. |
| 429 | Your team is at its concurrency limit, or the service is at capacity. | Retry with backoff. Honor `Retry-After` when present. |

Check warning on line 48 in api-reference/endpoint/websocket/asr-realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

api-reference/endpoint/websocket/asr-realtime.mdx#L48

Did you really mean 'backoff'?
| 503 | The service is restarting. | Retry. |

The `101 Switching Protocols` response carries an `x-request-id` header, and so do the capacity 429 and 503 refusals. It is the same value as `request_id` in `session_started`, `error` and `session_done`. Log it, and include it when you contact support. Refusals for the key, balance and your concurrency limit (401, 402 and concurrency 429) happen before a session is assigned and carry no `x-request-id`.
Expand Down Expand Up @@ -92,13 +92,13 @@
{
"type": "config",
"audio": { "format": "pcm_s16le", "sample_rate": 16000, "channels": 1 },
"turn_detection": { "mode": "auto", "eager": true },

Check warning on line 95 in api-reference/endpoint/websocket/asr-realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

api-reference/endpoint/websocket/asr-realtime.mdx#L95

Did you really mean 'turn_detection'?
"results": { "drafts": true, "ignore_timestamps": false },
"context": {
"history": [{ "role": "assistant", "text": "Hi, how can I help?" }],
"terms": ["Fish Audio", "transcribe-stream-1"]
},
"client_reference_id": "call-8f2c"

Check warning on line 101 in api-reference/endpoint/websocket/asr-realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

api-reference/endpoint/websocket/asr-realtime.mdx#L101

Did you really mean 'client_reference_id'?
}
```

Expand All @@ -114,13 +114,14 @@
| `results.drafts` | No | `false` | `true` sends live [`draft`](#draft) messages. |
| `results.ignore_timestamps` | No | `true` | `false` adds word timings (`words`) to `committed`. |
| `results.diarize` | No | `true` | `true`, `false` or `"auto"` (same as `true`); the strings `"true"` and `"false"` are accepted too. `false` removes speaker tags from the text. See [Speakers](#speakers). |
| `results.tag_audio_events` | No | `true` | `false` removes sound tags such as `[laughter]` from the text. See [Sound tags](#sound-tags). |
| `results.vad_events` | No | `false` | `true` sends [`vad`](#vad) messages. |
| `results.eot_events` | No | `false` | `true` sends [`eot_prob`](#eot_prob) messages. |
| `context.history` | No | `[]` | Earlier conversation, oldest first: up to 32 items of `{ "role": "user" \| "assistant", "text": str }`, each up to 2,000 characters. |
| `context.terms` | No | `[]` | Up to 100 terms of up to 32 characters each: names, products and jargon to recognize. |
| `client_reference_id` | No | | Your own ID for the session, up to 256 characters. It is recorded with the session and not sent back. |

A value of the wrong type for `ignore_timestamps`, or a `diarize` value other than those listed, is `invalid_config`. So is an unknown field, including a misspelled one; the error's `message` names the field. `invalid_config` closes the connection with 4400. Send only the fields listed here: not every other field is refused, so check `session_started.config`, which shows the settings in effect.
A value of the wrong type for `ignore_timestamps` or `tag_audio_events` (both take only `true` or `false`), or a `diarize` value other than those listed, is `invalid_config`. So is an unknown field, including a misspelled one; the error's `message` names the field. `invalid_config` closes the connection with 4400. Send only the fields listed here: not every other field is refused, so check `session_started.config`, which shows the settings in effect.

#### Audio format

Expand Down Expand Up @@ -174,7 +175,7 @@

Audio in the format from `config`. `data` must be MessagePack `bin` holding the raw PCM bytes. Do not base64-encode the audio, and do not send it as a string or an array of numbers. If you do, you receive a recoverable `invalid_request`, and those messages are dropped: nothing in them is transcribed or counted as received audio.

Send audio after `session_started` (see [`config`](#config) for audio sent earlier). For live audio, send it as it is captured, 20–40 ms per message (see [Audio format](#audio-format)). If your client batches small writes (Nagle's algorithm), turn that off with `TCP_NODELAY`; browsers and common WebSocket libraries already send each message right away. You can send faster than real time, for example from a file: the server accepts it and reads it at a bounded rate, and results keep following the audio.

Check warning on line 178 in api-reference/endpoint/websocket/asr-realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

api-reference/endpoint/websocket/asr-realtime.mdx#L178

Did you really mean 'Nagle's'?

### `finalize`

Expand All @@ -201,7 +202,7 @@

`audio` is fixed for the session: to change the audio format, open a new session. A `configure` with an unknown field, with an `audio` that differs from the session's, or with `context.history` gets `configured` with `ok: false`, and nothing in it is applied. An `audio` identical to the session's is accepted. As with `config`, send only the fields listed here; `configured.config` shows the settings in effect.

- A change to `ignore_timestamps` or `diarize` applies to turns that open after `configured`. A turn that is already open keeps the settings it started with, so its `eager_end_of_turn` and `committed` always agree.
- A change to `ignore_timestamps`, `diarize` or `tag_audio_events` applies to turns that open after `configured`. A turn that is already open keeps the settings it started with, so its `eager_end_of_turn` and `committed` always agree.
- When `mode` changes to or from `auto`, the server first settles its turn decisions on the audio already sent, so `configured` can take a few seconds.
- A turn's `user_turn` status follows the mode it opened in: a turn opened in `vad` or `manual` mode is always a user turn and gets no `user_turn_taken`. How an open turn ends follows the current mode.
- Switching away from `auto` settles an open turn that has no `user_turn_taken` yet: it becomes a user turn (`user_turn_taken` is sent) if there was speech in about the last second; otherwise it is committed at once with `user_turn: false`.
Expand Down Expand Up @@ -332,7 +333,7 @@
| `turn_index` | integer | The open turn, or, if no turn is open, the index the next turn will get. |
| `audio_ms` | integer | Where the acknowledgement is in the audio. |

`auto` mode only, while your agent is speaking (see [`agent_speaking`](#agent_speaking)): the user acknowledged ("mm-hm", "yeah", "right") without taking the turn. Keep talking. One message per acknowledgement. It always comes before the named turn's `committed`, and it can come before that turn's `speech_start`. A backchannel counts for the turn that contains it; that turn commits with `user_turn: false` and `turn_kind: "backchannel"` unless the user goes on to take the turn. A turn that opened in `vad` or `manual` mode never counts an acknowledgement. When your agent is not speaking, an acknowledgement takes the turn (`user_turn_taken`).

Check warning on line 336 in api-reference/endpoint/websocket/asr-realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

api-reference/endpoint/websocket/asr-realtime.mdx#L336

Did you really mean 'backchannel'?

### `committed`

Expand Down Expand Up @@ -413,6 +414,7 @@
- Every `draft` has `settled_text`, possibly empty, and the draft's `text` starts with it.
- `language` and `language_code` are both present or both absent.
- `committed.words` is present exactly when `ignore_timestamps` was `false` when the turn opened and the commit is not degraded.
- With `tag_audio_events` `false` when the turn opened, no text of the turn contains an opening tag bracket (`[`, U+FF3B or U+3010).

## Why a turn ended

Expand All @@ -434,27 +436,34 @@

## Speakers

`text` can contain speaker tags `<|speaker:N|>` (N = 0, 1, ...). Numbers are stable within a session and start over in a new one. Strip the tags if you only need the words, or send `results.diarize: false` to get text without them. With `diarize: false`, whitespace in the text, line breaks included, is collapsed to single spaces and trimmed; sound tags stay (see [Sound tags](#sound-tags)).
`text` can contain speaker tags `<|speaker:N|>` (N = 0, 1, ...). Numbers are stable within a session and start over in a new one. Strip the tags if you only need the words, or send `results.diarize: false` to get text without them. With `diarize: false`, whitespace in the text, line breaks included, is collapsed to single spaces and trimmed; sound tags stay unless `tag_audio_events` is `false` (see [Sound tags](#sound-tags)).

Whitespace right after a speaker tag is not significant and can vary, for example `<|speaker:0|>Can you` or `<|speaker:0|> Can you`. To get each speaker's text, split the text on the tags, then trim each segment. If you only need the words, remove the tags, then trim the text and collapse runs of whitespace into one space.

## Sound tags

`text` can contain sound tags in square brackets, such as `[laughter]`, `[laughs]`, `[sighs]` or `[pause]`, in every turn detection mode and whether `diarize` is on or off. The set is open: a tag can be any short description, sometimes several words long. A turn's text can be a tag alone. Language detection ignores tags.
`text` can contain sound tags in square brackets, such as `[laughter]`, `[laughs]`, `[sighs]` or `[pause]`, in every turn detection mode and whether `diarize` is on or off. The set is open: a tag can be any short description, sometimes several words long or in the language of the speech. A tag can also come in full-width square brackets (U+FF3B and U+FF3D) or lenticular brackets (U+3010 and U+3011) instead of `[` and `]`. A turn's text can be a tag alone.

Tags are sounds, not words. Strip them when you need only the words, for example for search, word counts, or deciding whether there is anything to answer: remove every match of the regular expression `\[[^\[\]]*\]`, then trim the text and collapse runs of whitespace into one space. If you keep them for a language model, tell it that they are sounds, and do not answer a turn that holds only tags.
Tags are sounds, not words. Strip them when you need only the words, for example for search, word counts, or deciding whether there is anything to answer: remove every match of the regular expression `\[[^\[\]]*\]|\uFF3B[^\uFF3B\uFF3D]*\uFF3D|\u3010[^\u3010\u3011]*\u3011`, then trim the text and collapse runs of whitespace into one space. If you keep them for a language model, tell it that they are sounds, and do not answer a turn that holds only tags.

Or send `results.tag_audio_events: false` to get no sound tags at all. Every tag, in any of the three kinds of brackets and whatever is inside it, is then removed from every text of the turn (`draft`, `eager_end_of_turn` and `committed`), as are the `words` entries that belong to a tag, and whitespace is collapsed and trimmed as with `diarize: false`:

- An opening bracket is closed only by its own kind: `[` by `]`, U+FF3B by U+FF3D, U+3010 by U+3011. A closing bracket ends the tag of the nearest opening bracket of its kind that is still open, and everything between them is removed with it, brackets of the other kinds included. A tag inside a tag of the same kind goes with the outer one. A closing bracket with no opening bracket of its kind open before it is ordinary text and stays.
- An opening bracket that nothing closes depends on the message. In a `draft` it is taken for the start of a tag that is still being written: it and the rest of that draft, speaker tags included, are not shown until a later draft closes it. In `eager_end_of_turn` and `committed`, the turn's final text, only that bracket is removed: the text after it is shown, and its `words` are kept.

Speaker tags still depend on `diarize` alone. A turn that holds only tags still gets its `committed`, without a language, and with a `text` that is empty with `diarize: false`, or holds only the turn's speaker tags with `diarize` on.

## Language

`draft`, `eager_end_of_turn` and `committed` carry the language of their own `text`. Use `language_code` (ISO 639-1) in your code; `language` is its English name, for display, with the same names as [`POST /v1/asr`](/api-reference/endpoint/openapi-v1/speech-to-text). The language is detected from that message's text alone, ignoring speaker tags, sound tags and interjections such as "okay", "hmm" or "uh". It is one of:
`draft`, `eager_end_of_turn` and `committed` carry the language of their own `text`. Use `language_code` (ISO 639-1) in your code; `language` is its English name, for display, with the same names as [`POST /v1/asr`](/api-reference/endpoint/openapi-v1/speech-to-text). The language is detected from that message's text alone, ignoring speaker tags, sound tags in `[` and `]`, and interjections such as "okay", "hmm" or "uh". With `tag_audio_events: false`, the label can also reflect text that is not shown: the rest of a draft after an unclosed opening bracket, the inside of a tag that holds another tag, or a tag in full-width or lenticular brackets. It is one of:

`ar`, `zh`, `cs`, `da`, `nl`, `en`, `fi`, `fr`, `de`, `el`, `hi`, `hu`, `id`, `it`, `ja`, `ko`, `mk`, `ms`, `fa`, `pl`, `pt`, `ro`, `ru`, `es`, `sv`, `tl`, `th`, `tr`, `vi`.

Both fields are omitted when the text is empty, too short to tell or ambiguous. Replies such as "Okay" or "Yes." usually get no label, and other text of only a few words often does not get one either; keep the last label you saw. Mixed-language text gets one label, the dominant language. Drafts are labeled one by one and may change; `committed` is final.

## Degraded commits

If the final pass for a turn fails, you receive `error` with `engine_error` and `recoverable: true`, and then a `committed` with `degraded: true`. Its `text` and language are those of the turn's last draft (empty and without a language when the turn had no draft, as with `results.drafts: false`), and it has no `words`. The session continues.
If the final pass for a turn fails, you receive `error` with `engine_error` and `recoverable: true`, and then a `committed` with `degraded: true`. Its `text` and language are those of the turn's last draft (empty and without a language when the turn had no draft, as with `results.drafts: false`), and it has no `words`. With `tag_audio_events: false`, an unclosed opening bracket in that draft is handled as in any `committed`: only the bracket is removed, so the text after it, and its language, can appear even if that draft showed none. The session continues.

## Errors and close codes

Expand Down
7 changes: 5 additions & 2 deletions developer-guide/sdk-guide/cookbook/realtime-transcription.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: "Transcribe a live audio stream in realtime"

Check warning on line 2 in developer-guide/sdk-guide/cookbook/realtime-transcription.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

developer-guide/sdk-guide/cookbook/realtime-transcription.mdx#L2

Did you really mean 'realtime'?
description: "Stream audio to transcribe-stream-1 over a WebSocket and print drafts, turn events and final transcripts as they arrive"
icon: "tower-broadcast"
tag: "Preview"
Expand All @@ -13,7 +13,7 @@

<Prerequisites />

The SDKs do not wrap the realtime speech-to-text endpoint yet, so this recipe speaks the WebSocket protocol directly. Install a WebSocket client and a MessagePack library:

Check warning on line 16 in developer-guide/sdk-guide/cookbook/realtime-transcription.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

developer-guide/sdk-guide/cookbook/realtime-transcription.mdx#L16

Did you really mean 'SDKs'?

Check warning on line 16 in developer-guide/sdk-guide/cookbook/realtime-transcription.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

developer-guide/sdk-guide/cookbook/realtime-transcription.mdx#L16

Did you really mean 'realtime'?

<CodeGroup>

Expand Down Expand Up @@ -236,7 +236,7 @@

</CodeGroup>

The audio goes in `data` as the raw PCM bytes, which must travel as MessagePack `bin`, not as base64 text: msgpack for Python packs `bytes` as `bin` by default (do not set `use_bin_type=False`), and `@msgpack/msgpack` packs a `Buffer` or `Uint8Array` as `bin`.

Check warning on line 239 in developer-guide/sdk-guide/cookbook/realtime-transcription.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

developer-guide/sdk-guide/cookbook/realtime-transcription.mdx#L239

Did you really mean 'msgpack'?

The server closes a session after 30 seconds without any client message, but a WebSocket ping counts as one, and Python's `websockets` sends a ping every 20 seconds by default, so the session stays open even if your audio stops; see [Capture audio well](/developer-guide/sdk-guide/cookbook/realtime-voice-agent#capture-audio-well) for a live source that can stall.

Expand Down Expand Up @@ -284,7 +284,7 @@
- **`draft`** is the whole turn so far, and replaces the previous draft. Drafts come only with `results.drafts: true`; use them for live captions.
- **`user_turn_taken`** says the server judged that the user is taking their turn, rather than noise or someone else nearby.
- **`eager_end_of_turn`** comes with `turn_detection.eager: true` when the turn has probably ended. Its text equals the final transcript unless `turn_resumed` follows, so an agent can start its reply early.
- **`committed`** is the final transcript of the turn. `user_turn: false` marks a non-user turn, such as background noise or a short acknowledgement while your agent is speaking. Speaker tags such as `<|speaker:0|>` are stable within the session; strip them and trim the text, or send `results.diarize: false`. Text can also contain [sound tags](/api-reference/endpoint/websocket/asr-realtime#sound-tags) such as `[laughter]`. `language_code` is omitted when the text is too short to tell, as for "Uh-huh." above.
- **`committed`** is the final transcript of the turn. `user_turn: false` marks a non-user turn, such as background noise or a short acknowledgement while your agent is speaking. Speaker tags such as `<|speaker:0|>` are stable within the session; strip them and trim the text, or send `results.diarize: false`. Text can also contain [sound tags](/api-reference/endpoint/websocket/asr-realtime#sound-tags) such as `[laughter]`; strip them too, or send `results.tag_audio_events: false`. `language_code` is omitted when the text is too short to tell, as for "Uh-huh." above.
- **`session_done`** follows `audio_done`, and the server then closes the connection with code 1000.

Ignore message types and fields you do not know: new ones can be added. Every message is described in the [WebSocket reference](/api-reference/endpoint/websocket/asr-realtime#server-messages).
Expand Down Expand Up @@ -403,7 +403,10 @@
// Speaker tags and sound tags out, whitespace collapsed.
const strip = text =>
(text || "")
.replace(/<\|[^|]*\|>|\[[^\[\]]*\]/g, "")
.replace(
/<\|[^|]*\|>|\[[^\[\]]*\]|\uFF3B[^\uFF3B\uFF3D]*\uFF3D|\u3010[^\u3010\u3011]*\u3011/g,
""
)
.replace(/\s+/g, " ")
.trim();
let ws,
Expand Down Expand Up @@ -490,10 +493,10 @@

Install `aiohttp`, put both files in one folder, run `python relay.py`, and open `http://localhost:8080`.

- The page captures audio at the device's own sample rate and sends that rate in `config`; the server converts it, so the page does not resample. The rate must be one of the supported rates.

Check warning on line 496 in developer-guide/sdk-guide/cookbook/realtime-transcription.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

developer-guide/sdk-guide/cookbook/realtime-transcription.mdx#L496

Did you really mean 'resample'?
- An `AudioWorklet` converts the samples to 16-bit PCM and posts 40 ms per message.
- **Stop** sends `audio_done`, so the server commits the last turn before it closes the connection.
- The relay forwards the API's close code to the page. In production, add your own user authentication, per-user session limits and input checks to the relay.

Check warning on line 499 in developer-guide/sdk-guide/cookbook/realtime-transcription.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

developer-guide/sdk-guide/cookbook/realtime-transcription.mdx#L499

Did you really mean 'API's'?

## Related

Expand Down
5 changes: 3 additions & 2 deletions developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "Build a realtime voice agent: streaming speech in, reply, speech out"

Check warning on line 2 in developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx#L2

Did you really mean 'realtime'?
description: "Use realtime speech to text to know when the user has finished, start replies early, keep talking through backchannels and stop on interruptions"

Check warning on line 3 in developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx#L3

Did you really mean 'realtime'?

Check warning on line 3 in developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx#L3

Did you really mean 'backchannels'?
icon: "headset"
tag: "Preview"
---
Expand All @@ -8,7 +8,7 @@
import Prerequisites from "/snippets/prerequisites.mdx";

<Note>
This guide is for building your own voice agent on the realtime speech-to-text

Check warning on line 11 in developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx#L11

Did you really mean 'realtime'?
API. To have Fish Audio run the whole agent for you, see
[Agents](/agents/overview).
</Note>
Expand All @@ -25,7 +25,7 @@
pip install "websockets>=14" msgpack
```

Read [Transcribe a live audio stream](/developer-guide/sdk-guide/cookbook/realtime-transcription) first if you have not used the realtime endpoint before.

Check warning on line 28 in developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx#L28

Did you really mean 'realtime'?

## How a conversation flows

Expand Down Expand Up @@ -72,7 +72,8 @@

URL = "wss://api.fish.audio/v1/asr/realtime"
CHUNK_MS = 40 # ms of audio per message; 20-40 ms suits live audio
TAGS = re.compile(r"<\|[^|]*\|>|\[[^\[\]]*\]") # speaker tags such as <|speaker:0|>, sound tags such as [laughter]
# Speaker tags such as <|speaker:0|>; sound tags such as [laughter], also in full-width brackets.
TAGS = re.compile(r"<\|[^|]*\|>|\[[^\[\]]*\]|\uFF3B[^\uFF3B\uFF3D]*\uFF3D|\u3010[^\u3010\u3011]*\u3011")
T0 = time.monotonic()


Expand Down Expand Up @@ -233,7 +234,7 @@

## Decide when to answer

Answer a turn when `committed.user_turn` is `true` and its text, without speaker tags and [sound tags](/api-reference/endpoint/websocket/asr-realtime#sound-tags), is not empty. Do not decide by `reason`: a turn can end for several reasons, and non-user turns end with `eot` too. If `user_turn` is ever missing, treat it as `true`.
Answer a turn when `committed.user_turn` is `true` and its text, without speaker tags and [sound tags](/api-reference/endpoint/websocket/asr-realtime#sound-tags) (already removed with `results.diarize: false` and `results.tag_audio_events: false`), is not empty. Do not decide by `reason`: a turn can end for several reasons, and non-user turns end with `eot` too. If `user_turn` is ever missing, treat it as `true`.

A turn with `user_turn: false` is not the user talking to your agent: usually background noise, a TV or another person nearby (`turn_kind: "other"`), or an acknowledgement while your agent was speaking (`turn_kind: "backchannel"`). Its text can be shown dimmed in a transcript, or left out.

Expand All @@ -260,7 +261,7 @@

Without `agent_speaking`, every acknowledgement takes the turn, and you have to tell "mm-hm" from an interruption yourself.

## Handle interruptions and backchannels

Check warning on line 264 in developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx#L264

Did you really mean 'backchannels'?

While your agent is speaking:

Expand All @@ -285,7 +286,7 @@
{
"type": "config",
"audio": { "format": "pcm_s16le", "sample_rate": 16000, "channels": 1 },
"turn_detection": { "mode": "auto", "eager": true },

Check warning on line 289 in developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx#L289

Did you really mean 'turn_detection'?
"context": {
"history": [
{
Expand All @@ -294,7 +295,7 @@
},
{ "role": "user", "text": "Hi, I'm calling about my prescription." }
],
"terms": ["Main Street Pharmacy", "Lisinopril"]

Check warning on line 298 in developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx#L298

Did you really mean 'Lisinopril'?
}
}
```
Expand All @@ -304,13 +305,13 @@
- **Keep echo cancellation on.** Without it, the agent's own voice reaches the microphone, opens turns and can be taken for the user. In a browser, request `echoCancellation: true` from `getUserMedia`. Some echo can remain even with echo cancellation. It is usually committed as a non-user turn, but it still opens turns (`speech_start`) while your agent speaks, which is why ducks need a cap. If the start of most replies is ducked, echo is leaking: check echo cancellation, or test with headphones.
- **Turn automatic gain control off where you can** (`autoGainControl: false` in a browser), so quiet background sound is not amplified between phrases.
- **Test with headphones first.** They keep the agent's voice out of the microphone, so you can check turn-taking before you tune echo cancellation.
- **Send audio as you capture it**, mono 16-bit PCM in 20–40 ms messages, at the device's own rate if it is one of the supported rates. The server converts the rate, so do not resample. Telephony audio at 8 kHz is supported; decode μ-law or A-law call audio to 16-bit PCM first.

Check warning on line 308 in developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx#L308

Did you really mean 'resample'?
- **Watch your audio source.** The server closes a session after 30 seconds without any client message (`idle_timeout`, close 4408), but a WebSocket ping counts as one, and Python's `websockets` sends a ping every 20 seconds by default. With that default, a session stays open even when your audio stops, and it counts against your concurrency limit until you close it or the 60-minute session limit ends it. If your audio source can stall, watch it in your own code and send `audio_done` or close the connection when it stops, or pass `ping_interval=None` to `websockets.connect` to leave idle detection to the server. Node's `ws` sends no pings by default.
- **Keep keys on your server.** In a browser, capture audio and relay it through your server, as in [Capture from a browser microphone](/developer-guide/sdk-guide/cookbook/realtime-transcription#capture-from-a-browser-microphone). Send `agent_speaking` on that same connection so it stays in order with the audio.

## Languages

Turn detection, including eager end of turn, backchannel detection and background awareness, works on speech in every language. In some languages, a turn can end a little later after the speaker stops. For faster turns, use `vad` mode (500 ms or more is a good start for `silence_timeout_ms`), or end turns yourself with `finalize`.

Check warning on line 314 in developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

developer-guide/sdk-guide/cookbook/realtime-voice-agent.mdx#L314

Did you really mean 'backchannel'?

## Connect your LLM and speech

Expand Down
2 changes: 1 addition & 1 deletion features/speech-to-text/realtime.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Realtime Speech to Text"

Check warning on line 2 in features/speech-to-text/realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

features/speech-to-text/realtime.mdx#L2

Did you really mean 'Realtime'?
sidebarTitle: "Realtime"
description: "Transcribe live audio turn by turn with transcribe-stream-1: turn detection that knows when the user has finished, backchannel and background awareness, and conversation context"

Check warning on line 4 in features/speech-to-text/realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

features/speech-to-text/realtime.mdx#L4

Did you really mean 'backchannel'?
icon: "microphone-lines"
tag: "Preview"
---
Expand Down Expand Up @@ -30,7 +30,7 @@
icon="headset"
href="/developer-guide/sdk-guide/cookbook/realtime-voice-agent"
>
Turn-taking, interruptions and backchannels.

Check warning on line 33 in features/speech-to-text/realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

features/speech-to-text/realtime.mdx#L33

Did you really mean 'backchannels'?
</Card>
</CardGroup>

Expand Down Expand Up @@ -64,7 +64,7 @@

To transcribe a recording you already have, use [`POST /v1/asr`](/features/speech-to-text) with `transcribe-1-pro` instead:

| | Realtime (`/v1/asr/realtime`) | File (`POST /v1/asr`) |

Check warning on line 67 in features/speech-to-text/realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

features/speech-to-text/realtime.mdx#L67

Did you really mean 'Realtime'?
| -------------- | -------------------------------------------------------------- | ---------------------------------------------------------- |
| Model | `transcribe-stream-1` (Preview) | `transcribe-1-pro` |
| Input | Live 16-bit PCM over a WebSocket | A complete audio file (WAV, MP3, Opus, and more) |
Expand All @@ -72,11 +72,11 @@
| Turn detection | Yes | No |
| Session length | Up to 60 minutes | Recordings up to 60 minutes |

The realtime endpoints of the [compatible APIs](/developer-guide/compat/capabilities) are separate from this endpoint. They use manual commit and have no server-side turn detection.

Check warning on line 75 in features/speech-to-text/realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

features/speech-to-text/realtime.mdx#L75

Did you really mean 'realtime'?

## Quick start

The SDKs do not wrap this endpoint; you connect with any WebSocket client and exchange MessagePack messages.

Check warning on line 79 in features/speech-to-text/realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

features/speech-to-text/realtime.mdx#L79

Did you really mean 'SDKs'?

<Steps>
<Step title="Connect">
Expand Down Expand Up @@ -105,7 +105,7 @@
{
"type": "config",
"audio": { "format": "pcm_s16le", "sample_rate": 16000, "channels": 1 },
"turn_detection": { "mode": "auto", "eager": true },

Check warning on line 108 in features/speech-to-text/realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

features/speech-to-text/realtime.mdx#L108

Did you really mean 'turn_detection'?
"results": { "drafts": true }
}
```
Expand Down Expand Up @@ -143,7 +143,7 @@

With `turn_detection.eager: true`, you receive `eager_end_of_turn` as soon as the turn has probably ended, before the final transcript. Its text is exactly the text `committed` will have, unless `turn_resumed` follows because the user went on. A voice agent can start its LLM on the eager text and save that time.

### Backchannel detection

Check warning on line 146 in features/speech-to-text/realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

features/speech-to-text/realtime.mdx#L146

Did you really mean 'Backchannel'?

Tell the server when your agent is speaking with `agent_speaking`. While it speaks, a short "mm-hm", "yeah", "right" or "uh-huh" from the user produces `user_backchannel`: the agent keeps talking. A real interruption produces `user_turn_taken`: the agent stops.

Expand All @@ -169,14 +169,14 @@
| Mode | A turn ends when | Use it for |
| ---------------- | ------------------------------------------------------------------ | -------------------------------------- |
| `auto` (default) | Turn detection judges that the user has finished. | Voice agents and conversations. |
| `vad` | A fixed silence has passed (`silence_timeout_ms`, default 700 ms). | Simple silence-based endpointing. |

Check warning on line 172 in features/speech-to-text/realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

features/speech-to-text/realtime.mdx#L172

Did you really mean 'endpointing'?
| `manual` | You send `finalize`. | Push-to-talk, or your own endpointing. |

Check warning on line 173 in features/speech-to-text/realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

features/speech-to-text/realtime.mdx#L173

Did you really mean 'endpointing'?

Eager end of turn, backchannel detection and background awareness are available in `auto` mode, when the session starts in `auto` (the default). In `vad` and `manual` mode, and after a later switch to `auto` from another mode, every turn is a user turn. See [Turn detection modes](/api-reference/endpoint/websocket/asr-realtime#turn-detection-modes) for details.

Check warning on line 175 in features/speech-to-text/realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

features/speech-to-text/realtime.mdx#L175

Did you really mean 'backchannel'?

## Languages

Smart Turn Detection, including eager end of turn, backchannel detection and background awareness, works on speech in every language. In some languages, a turn can end a little later after the speaker stops.

Check warning on line 179 in features/speech-to-text/realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

features/speech-to-text/realtime.mdx#L179

Did you really mean 'backchannel'?

`draft`, `eager_end_of_turn` and `committed` carry the language of their own text as `language_code` when it can be identified, one of 29 [supported codes](/api-reference/endpoint/websocket/asr-realtime#language).

Expand All @@ -194,11 +194,11 @@

### Audio

Send 16-bit little-endian PCM, mono, at 8000, 12000, 16000, 22050, 24000, 32000, 44100 or 48000 Hz. The server converts the sample rate, so you never need to resample. Compressed formats such as Opus or MP3 are not accepted on this endpoint.

Check warning on line 197 in features/speech-to-text/realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

features/speech-to-text/realtime.mdx#L197

Did you really mean 'resample'?

### Speakers

Transcripts contain speaker tags such as `<|speaker:0|>`, stable within a session. Strip them, or send `results.diarize: false` for plain text. Whitespace right after a tag can vary, so trim the text after you strip the tags. Transcripts can also contain sound tags such as `[laughter]` or `[pause]`; see [Sound tags](/api-reference/endpoint/websocket/asr-realtime#sound-tags).
Transcripts contain speaker tags such as `<|speaker:0|>`, stable within a session. Strip them, or send `results.diarize: false` for plain text. Whitespace right after a tag can vary, so trim the text after you strip the tags. Transcripts can also contain sound tags such as `[laughter]` or `[pause]`. Strip them too, or send `results.tag_audio_events: false` to remove them; see [Sound tags](/api-reference/endpoint/websocket/asr-realtime#sound-tags).

### Keys and browsers

Expand All @@ -210,7 +210,7 @@

### Pricing

`transcribe-stream-1` costs $0.36 per hour, billed per second of session duration as its own line item, Realtime ASR. See [ASR pricing](/developer-guide/models-pricing/pricing-and-rate-limits#automatic-speech-recognition-asr-models).

Check warning on line 213 in features/speech-to-text/realtime.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hanabiaiinc) - vale-spellcheck

features/speech-to-text/realtime.mdx#L213

Did you really mean 'Realtime'?

## Going further

Expand Down
Loading