Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4af479d
feat: reson8 plugin
phillip-kil Sep 9, 2026
697cf73
test: reson8 plugin
phillip-kil Sep 9, 2026
4d4f889
feat: register reson8 plugin
phillip-kil Sep 9, 2026
5c81c03
fix: only pcm_s16le encoding
phillip-kil Sep 9, 2026
a43ac11
chore: update docs and tests
phillip-kil Sep 9, 2026
8b1720a
fix: apply options at turn boundary
phillip-kil Sep 9, 2026
75ee71c
fix: wait for the final transcript before closing the socket
phillip-kil Sep 9, 2026
db68790
fix: keep aligned_transcript in step with the transcript options
phillip-kil Sep 9, 2026
62400ce
fix: defer option updates on unanswered audio, not just an open turn
phillip-kil Sep 9, 2026
0791538
fix: report a close that loses the final turn
phillip-kil Sep 9, 2026
8322a2d
fix: carry transcript timing across an internal reconnect
phillip-kil Sep 9, 2026
3963c16
fix: keep provider free text out of exception messages
phillip-kil Sep 9, 2026
c04ce49
fix: fail when finalisation runs out of time
phillip-kil Sep 9, 2026
3d8281f
fix: re-check reconnect safety when the redial happens
phillip-kil Sep 9, 2026
80dcbb7
fix: reject response bodies that are valid JSON but not objects
phillip-kil Sep 9, 2026
253de3e
fix: keep provider event fields out of log message bodies
phillip-kil Sep 9, 2026
139b33a
fix: close tracked streams when the recognizer closes
phillip-kil Sep 9, 2026
10b6cca
fix: only relay identifier-shaped error codes
phillip-kil Sep 9, 2026
a1aa849
fix: uv.lock
phillip-kil Sep 9, 2026
701db0f
fix: close out an open turn when a connection is torn down
phillip-kil Sep 9, 2026
2277e58
revert: apply option updates immediately again
phillip-kil Sep 9, 2026
3cffe97
fix: wrap send failures as retryable API errors
phillip-kil Sep 10, 2026
010f13c
docs: state the limit of the final-turn wait
phillip-kil Sep 10, 2026
e649ada
docs: note that instance updates override per-stream language
phillip-kil Sep 10, 2026
a2f24a2
fix: report the cause of a websocket ERROR frame
phillip-kil Sep 10, 2026
f8b74aa
fix: skip a turn payload we cannot map
phillip-kil Sep 10, 2026
1f0636e
fix: stop quoting rejected biasing entries
phillip-kil Sep 10, 2026
f99fd18
fix: fail a batch response we cannot map
phillip-kil Sep 10, 2026
c14d0cd
fix: skip a reconnect when nothing changed
phillip-kil Sep 10, 2026
4984986
fix: treat an overflowing timestamp as a malformed body
phillip-kil Sep 10, 2026
adcd7e3
fix: snapshot biasing sequences on construction
phillip-kil Sep 10, 2026
9fce1aa
docs: update doc links
phillip-kil Sep 10, 2026
5f3c382
fix: avoid double counting
phillip-kil Sep 10, 2026
21529bc
fix: stop rewriting the URL scheme, warn on plaintext instead
phillip-kil Sep 10, 2026
3f8529c
fix: hold briefly for a further turn before hanging up
phillip-kil Sep 10, 2026
33d8cb3
fix: separate finalisation state from the wake signal
phillip-kil Sep 10, 2026
6c3d722
fix: expiry shouldnt be an error
phillip-kil Sep 11, 2026
55c8aec
fix: project config
phillip-kil Sep 11, 2026
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
1 change: 1 addition & 0 deletions livekit-agents/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ palabra = ["livekit-plugins-palabra>=1.8.1"]
perplexity = ["livekit-plugins-perplexity>=1.8.1"]
protoface = ["livekit-plugins-protoface>=1.8.1"]
resemble = ["livekit-plugins-resemble>=1.8.1"]
reson8 = ["livekit-plugins-reson8>=1.8.1"]
respeecher = ["livekit-plugins-respeecher>=1.8.1"]
rime = ["livekit-plugins-rime>=1.8.1"]
runway = ["livekit-plugins-runway>=1.8.1"]
Expand Down
185 changes: 185 additions & 0 deletions livekit-plugins/livekit-plugins-reson8/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Reson8 plugin for LiveKit Agents

Support for [Reson8](https://reson8.dev) speech-to-text, with server-side turn detection.

More information is available in the docs for the [STT](https://docs.livekit.io/agents/integrations/stt/reson8/) integration.

## Installation

```bash
pip install livekit-plugins-reson8
```

## Pre-requisites

You'll need an API key from Reson8. It can be set as an environment variable: `RESON8_API_KEY`

## Usage

`reson8.STT` adapts to how LiveKit uses it:

- **Streaming** (`stream()`, used by voice agents) connects to the turn-aware
endpoint. Reson8 detects conversational turn boundaries server-side: it emits
a *preflight* transcript (an eager guess that the turn is over) that your agent
can start responding to. A later guess replaces it, and the last one becomes
the final transcript when the turn ends.
- **Batch** (`recognize()`) transcribes pre-recorded audio and returns the full
transcript.

```python
from livekit.agents import AgentSession
from livekit.plugins import openai, reson8

session = AgentSession(
stt=reson8.STT(), # streaming + turn detection, language auto-detected
llm=openai.LLM(),
tts=openai.TTS(),
# "stt" hands turn detection to Reson8 and lets the agent start
# generating on the preflight transcript instead of the confirmation.
turn_handling={
"turn_detection": "stt",
"preemptive_generation": {"enabled": True},
},
)
```

By default LiveKit runs its own turn detector. Set `turn_handling` as above to
hand turn-taking to Reson8 instead.

### Transcribing a file

```python
event = await reson8.STT().recognize(audio_buffer)
print(event.alternatives[0].text)
```

## Turn detection

Reson8 decides turn boundaries by confidence: it emits the preflight transcript
at `turn.eager_probability` and commits the turn at `turn.final_probability`.
The server default of `0.92` is tuned for conversational speech and is slow to
commit a one-word answer; lower it to commit sooner, at the risk of cutting off
longer utterances.

```python
stt = reson8.STT(turn=reson8.TurnOptions(final_probability=0.7))
```

`SpeechStream.flush()` commits the current turn immediately, keeping
`final_probability` intact. LiveKit never calls it for you.

See [Turns](https://docs.reson8.dev/speech-to-text/turns/) for how turn events
work server-side.

## Languages

Leave `language` unset to **auto-detect** the spoken language, or pin recognition
to one or more supported codes. You can pass a single code, a comma-string, or a
list — a list is normalized to Reson8's comma-joined form and any unsupported code
raises `ValueError` locally, before a request is made.

```python
reson8.STT() # auto-detects the spoken language
reson8.STT(language="en") # English only
reson8.STT(language="nl,de") # Dutch or German
reson8.STT(language=["nl", "de"]) # same, as a list
```

Reson8 supports `de`, `en`, `es`, `fr`, `fy` (Frisian), `it`, `nl`, `pl`, `pt`
and `sv` — available as the `reson8.SupportedLanguage` type and the
`reson8.SUPPORTED_LANGUAGES` tuple. See
[Languages](https://docs.reson8.dev/speech-to-text/features/languages/).

## Configuration

Settings are grouped into sections, each of which validates itself on
construction:

```python
stt = reson8.STT(
api_key="your-api-key", # or set RESON8_API_KEY
language="nl",
turn=reson8.TurnOptions(final_probability=0.7),
audio=reson8.AudioOptions(sample_rate=16000),
transcript=reson8.TranscriptOptions(words=True),
biasing=reson8.BiasingOptions(custom_model_id="my-model"),
)
```

### `TurnOptions`

The main lever on end-of-turn latency. `None` leaves the server's default.

| Field | Default | |
|---|---|---|
| `eager_probability` | `None` (server: `0.5`) | confidence at which the preflight transcript is emitted |
| `final_probability` | `None` (server: `0.92`) | confidence at which the turn commits |

### `AudioOptions`

Describes the audio sent to Reson8; it does not convert it. Streaming input is
resampled to `sample_rate`, but nothing remixes channels or transcodes samples,
so a pushed frame whose channel count disagrees with `num_channels` raises
rather than being relabelled.

| Field | Default | |
|---|---|---|
| `sample_rate` | `16000` | streaming input is resampled to this |
| `encoding` | `"pcm_s16le"` | the only value; `rtc.AudioFrame` is signed 16-bit PCM and is forwarded unchanged |
| `num_channels` | `1` | channel count of the frames you push, 1 to 10 |

### `TranscriptOptions`

| Field | Default | |
|---|---|---|
| `words` | `False` | word-level results, each with its own timing |
| `language` | `True` | the detected language code |
| `confidence` | `False` | per-word confidence, batch recognition only |
| `filler_mode` | `None` (server: `natural`) | `clean` removes filler words, `natural` lets the model decide, `verbatim` preserves them |

### `BiasingOptions`

Use `phrases` for a handful of terms on a single request, a `custom_model_id`
for a vocabulary that is larger or reused across requests, and `patterns` for
structured tokens whose shape you know up front.

Biasing is not free: phrases and patterns can *degrade* transcription of audio
that does not contain them, and stronger biasing introduces irrelevant terms.

| Field | Default | |
|---|---|---|
| `custom_model_id` | `None` | a custom model to bias toward, for a vocabulary too large for `phrases` or reused across requests |
| `phrases` | `None` | terms to bias toward, at most 250; needs no custom model |
| `strength` | `None` (server: `0.45`) | additive boost on the model's trained calibration. Raise only when expected terminology is not being recovered |
| `patterns` | `None` | shapes for short alphanumeric tokens to recover, e.g. `"AMZ[0-9]{6}"` or `"[0-9]{4,6}"` |

```python
# bias toward vocabulary the model would otherwise miss
stt = reson8.STT(biasing=reson8.BiasingOptions(phrases=["Reson8", "LiveKit"]))

# or recover a structured token, so its digits are not heard as words
stt = reson8.STT(biasing=reson8.BiasingOptions(patterns=["AMZ[0-9]{6}", "[0-9]{4,6}"]))
```

See [custom models](https://docs.reson8.dev/speech-to-text/features/custom-models/)
and [patterns](https://docs.reson8.dev/speech-to-text/features/patterns/).

`STT.update_options(...)` takes the same sections and changes them at runtime.
Reson8 reads its configuration from the query string, so a live stream applies
new settings by reconnecting immediately — which abandons the audio already
sent for the turn in progress, since Reson8 holds turn state server-side. Change
options between turns, or accept losing the one in progress. `AudioOptions` is
fixed for the life of a stream, since the input resampler is built when the
stream opens.

### Self-hosted deployments

`base_url` (or `RESON8_BASE_URL`) points the plugin at a Reson8 deployment other
than `https://api.reson8.dev`.

## Documentation

- [Reson8 API reference](https://docs.reson8.dev/api/speech-to-text/turns/)
- [Turns and turn detection](https://docs.reson8.dev/speech-to-text/turns/)
- [LiveKit Agents docs](https://docs.livekit.io/agents/)
- [Reson8 STT plugin guide](https://docs.livekit.io/agents/integrations/stt/reson8/)
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Copyright 2026 LiveKit, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Reson8 plugin for LiveKit Agents.

Support for speech-to-text with [Reson8](https://reson8.dev), including
server-side turn detection.

See https://docs.reson8.dev/integrations/livekit/ for more information.
"""

from livekit.agents import Plugin

from ._utils import SUPPORTED_LANGUAGES, Encoding, FillerMode, SupportedLanguage
from .log import logger
from .stt import (
STT,
AudioOptions,
BiasingOptions,
SpeechStream,
TranscriptOptions,
TurnOptions,
)
from .version import __version__

__all__ = [
"STT",
"SUPPORTED_LANGUAGES",
"AudioOptions",
"BiasingOptions",
"Encoding",
"FillerMode",
"SpeechStream",
"SupportedLanguage",
"TranscriptOptions",
"TurnOptions",
"__version__",
]


class Reson8Plugin(Plugin):
def __init__(self) -> None:
super().__init__(__name__, __version__, __package__, logger)


Plugin.register_plugin(Reson8Plugin())

_module = dir()
NOT_IN_ALL = [m for m in _module if m not in __all__]

__pdoc__ = {}

for n in NOT_IN_ALL:
__pdoc__[n] = False
Loading
Loading