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

# Speech-to-Text

> Capture user transcript, turn timing, and STT latency by attaching a Tuner adapter to your existing STT provider — your streaming loop stays untouched.

<Note>
  This guide uses [`tuner-stt-observer`](https://pypi.org/project/tuner-stt-observer/). It assumes you have a `TunerSession` set up — see the [Quickstart](/docs/api-and-integrations/custom-integration/quickstart) for the session lifecycle and credentials.
</Note>

`tuner-stt-observer` hooks into your STT provider's **native event system** to capture the user side of the conversation. It never proxies or modifies your audio stream — it listens to the same events your application already receives.

## Supported adapters

| Provider     | Adapter               | Status      |
| ------------ | --------------------- | ----------- |
| Deepgram     | `DeepgramAdapter`     | ✅ Supported |
| Speechmatics | `SpeechmaticsAdapter` | ✅ Supported |
| AssemblyAI   | —                     | SOON        |

Need a provider that isn't listed? Extend `BaseSTTAdapter` — see [Custom providers](#custom-providers) below.

## Install

```shell theme={null} theme={null}
pip install tuner-core tuner-stt-observer
```

Each provider adapter lives behind an optional extra so you only pull in the SDKs you actually use:

```shell theme={null} theme={null}
pip install "tuner-stt-observer[deepgram]"       # DeepgramAdapter
pip install "tuner-stt-observer[speechmatics]"   # SpeechmaticsAdapter
```

## Deepgram

`DeepgramAdapter` registers on Deepgram's `Open`, `Transcript`, and `UtteranceEnd` events. `speech_final` transcript fragments are buffered internally and flushed as a single joined utterance when `UtteranceEnd` fires — you get **one complete user turn per utterance**, not one per fragment.

Attach it to the Deepgram connection you already have:

```python theme={null} theme={null}
from tuner_stt_observer import DeepgramAdapter

stt = DeepgramAdapter(connection=dg_connection)   # your existing Deepgram connection
stt.on_utterance = handle_user_utterance          # your callback: complete user turn text
session.attach(stt)
```

Send audio to Deepgram exactly as you normally would — the adapter only listens on the connection's events, it never touches audio:

```python theme={null} theme={null}
await dg_connection.send(chunk)
```

<Warning>
  Your `LiveOptions` **must** set `utterance_end_ms` (e.g. `"1000"`) and `vad_events=True`. Without them, `UtteranceEnd` never fires and the fragment buffer never flushes — you'll see calls with no user turns.
</Warning>

<Tip>
  Barge-in isn't handled by this adapter — build it yourself with a local VAD or a provider event like Deepgram's `SpeechStarted`.
</Tip>

## Speechmatics

`SpeechmaticsAdapter` registers on `ADD_TRANSCRIPT` and `END_OF_UTTERANCE`. Speechmatics-rt **stacks** event handlers rather than replacing them, so the adapter coexists safely with any handlers you register yourself.

```python theme={null} theme={null}
from tuner_stt_observer import SpeechmaticsAdapter

stt = SpeechmaticsAdapter(client=stt_client)   # your existing Speechmatics client
session.attach(stt)

# Your own handlers still work — Speechmatics-rt stacks, not replaces.
# For barge-in, register your own partial-transcript handler:
stt_client.on(SMEvent.ADD_PARTIAL_TRANSCRIPT)(handle_barge_in)
```

## Custom providers

For providers other than Deepgram and Speechmatics, extend `BaseSTTAdapter` from `tuner_stt_observer` — its docstring documents the full contract (`_record_user_turn()`, `_record_stt_usage()`, timestamp conversion) with a worked example.

## What gets captured

| Signal                       | Deepgram                                             | Speechmatics                               |
| ---------------------------- | ---------------------------------------------------- | ------------------------------------------ |
| Complete user utterance text | ✓ (fragments joined on `UtteranceEnd`)               | ✓ (fragments joined on `END_OF_UTTERANCE`) |
| Turn start timestamp         | ✓                                                    | ✓                                          |
| Turn duration                | ✓                                                    | ✓                                          |
| STT latency                  | ✓ (last fragment's audio end → `Transcript` arrival) | ✓ (`END_OF_UTTERANCE` → flush)             |
| STT audio usage              | ✓                                                    | ✓                                          |

## Troubleshooting

<AccordionGroup>
  <Accordion title="Calls have no user turns (Deepgram)">
    * Your `LiveOptions` must set `utterance_end_ms` (e.g. `"1000"`) and `vad_events=True` — without them Deepgram never emits `UtteranceEnd` and the utterance buffer never flushes.
  </Accordion>

  <Accordion title="User turns arrive as fragments">
    * Fragments are joined per utterance — if you're seeing one turn per fragment, the utterance-end event is never firing. On Deepgram, check `utterance_end_ms`; on Speechmatics, confirm your session emits `END_OF_UTTERANCE`.
  </Accordion>
</AccordionGroup>

## What's Next?

<CardGroup cols={2}>
  <Card title="Quickstart" icon="rocket" href="/docs/api-and-integrations/custom-integration/quickstart">
    The session lifecycle, credentials, and every observer wired together.
  </Card>

  <Card title="Text-to-Speech" icon="volume-high" href="/docs/api-and-integrations/libraries/tuner-tts">
    Capture the agent side — synthesis latency and word-accurate interruption.
  </Card>

  <Card title="LangChain / LangGraph" icon="link" href="/docs/api-and-integrations/libraries/tuner-langchain">
    Node transitions, tool calls, and LLM latency in your call timeline.
  </Card>

  <Card title="Create Call API" icon="code" href="/docs/api-reference/create-call">
    The underlying API every observer feeds into at flush time.
  </Card>
</CardGroup>
