# Turn detection

> Decide when the user has finished speaking, with local models, with VAD only, or on the provider's side.

Source: https://polvallverdu.github.io/pipedog/docs/concepts/turn-detection/



A voice agent has to know when the user starts speaking, to stop talking, and when they've
finished, to answer. pipedog does this in one of three ways. They differ in what runs on your
server: two local models, one, or none.

| Mode                   | Start of turn   | End of turn     | Runs locally             |
| ---------------------- | --------------- | --------------- | ------------------------ |
| Local models (default) | Silero VAD      | Smart Turn      | ONNX runtime, two models |
| VAD only               | Silero VAD      | silence         | ONNX runtime, one model  |
| Provider               | the STT service | the STT service | nothing                  |

Each mode is a choice of [turn strategies](https://polvallverdu.github.io/pipedog/docs/concepts/turn-strategies.md) on the user aggregator.
This page covers the three common setups; the strategies page covers every strategy and option.

## Local models [#local-models]

Silero VAD hears speech; Smart Turn listens to the end of each utterance and decides whether the user
is done or only paused. This is Pipecat's default, and pipedog's:

```ts
const aggregators = new LLMContextAggregatorPair(context, {
  userParams: new LLMUserAggregatorParams({ vadAnalyzer: new SileroVADAnalyzer() }),
});
```

The default turn strategies use Smart Turn from `@pipedog/audio-smart-turn`, so install it next to
`@pipedog/audio-silero`. Without it, turns end on silence (the VAD-only mode), and pipedog logs a
warning once. In a bundled app, pass the analyzer to the strategy yourself; see
[Bundling](https://polvallverdu.github.io/pipedog/docs/deployment/bundling.md).

Both models share one inference session per process: the first call loads them, and later calls
reuse them. Measured on Bun with `onnxruntime-node`, the first call takes about 100 MB for the runtime
and both models, and each further call about 1.3 MB.

## VAD only [#vad-only]

Leave out `@pipedog/audio-smart-turn`, or end turns on silence explicitly:

```ts
const userParams = new LLMUserAggregatorParams({
  vadAnalyzer: new SileroVADAnalyzer(),
  userTurnStrategies: new UserTurnStrategies({
    stop: [new SpeechTimeoutUserTurnStopStrategy()],
  }),
});
```

This answers sooner after a pause than Smart Turn does, so it interrupts users who pause to think more
often.

## Another VAD: TEN VAD [#another-vad-ten-vad]

Silero is the default VAD. [TEN VAD](https://polvallverdu.github.io/pipedog/docs/audio/ten-vad.md) (`TenVADAnalyzer`, pipedog only) is an
opt-in alternative that runs TEN's WebAssembly build, which you obtain from TEN and pass in, under
TEN's own license. It takes 16 kHz audio only. Pass it as `vadAnalyzer` wherever Silero goes.

## Remote Smart Turn over HTTP [#remote-smart-turn-over-http]

If you can't run the model on the bot's host (a Worker, a small container), run Smart Turn as a
service and let the bot send it the audio. `HttpSmartTurnAnalyzer` posts each candidate turn to an
endpoint and reads the verdict back. It needs only `fetch`, so it works on every runtime, Cloudflare
Workers included.

| Runtime            | Works | What it needs                                                                                                                                                                                                                        |
| ------------------ | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Bun                | ✅ yes | Local Smart Turn v3: onnxruntime-node or onnxruntime-web. HttpSmartTurnAnalyzer: only fetch and a Smart Turn endpoint.                                                                                                               |
| Node.js            | ✅ yes | Local Smart Turn v3: onnxruntime-node or onnxruntime-web. HttpSmartTurnAnalyzer: only fetch and a Smart Turn endpoint.                                                                                                               |
| Deno               | ✅ yes | Local Smart Turn v3: onnxruntime-node or onnxruntime-web. HttpSmartTurnAnalyzer: only fetch and a Smart Turn endpoint.                                                                                                               |
| Cloudflare Workers | ✅ yes | Local Smart Turn v3: onnxruntime-node or onnxruntime-web. HttpSmartTurnAnalyzer: only fetch and a Smart Turn endpoint. Local Smart Turn: pass the model's bytes (there is no filesystem). HttpSmartTurnAnalyzer needs nothing extra. |

Generated from `pipecat.runtimes` and `pipecat.compat` in `packages/audio-smart-turn/package.json`: ✅ means the compat smoke test passes on that runtime.

```ts
// Smart Turn as a service: each candidate turn is POSTed to the endpoint.
const userParams = new LLMUserAggregatorParams({
  vadAnalyzer: new SileroVADAnalyzer(),
  userTurnStrategies: new UserTurnStrategies({
    start: [new VADUserTurnStartStrategy(), new TranscriptionUserTurnStartStrategy()],
    stop: [
      new TurnAnalyzerUserTurnStopStrategy({
        turnAnalyzer: new HttpSmartTurnAnalyzer({
          url: "https://smart-turn.example.com/predict",
          headers: { Authorization: `Bearer ${process.env.SMART_TURN_KEY}` },
        }),
      }),
    ],
  }),
});
```

It goes where the local analyzer does, in a `TurnAnalyzerUserTurnStopStrategy` (see
[turn strategies](https://polvallverdu.github.io/pipedog/docs/concepts/turn-strategies.md)); `params: new SmartTurnParams({ stopSecs, ... })` takes the same settings. It makes the same request as Pipecat's: a `POST` of
the audio (up to `maxDurationSecs`, as a NumPy `.npy` float32 vector at the analyzer's sample rate) with
`Content-Type: application/octet-stream`, and expects JSON with a `prediction` (1 when the user is
done) and a `probability`. Any failure, a non-200 status, a timeout after `stopSecs` or a body that
isn't JSON, is logged and counts as "not done", as upstream does. `fetch` replaces the `aiohttp`
session; pass your own as the `fetch` option.

Pipecat's older `LocalSmartTurnAnalyzerV2` and the Core ML variant need PyTorch or Core ML and are
not ported: Smart Turn v3, the default above, replaces them.

## Smart Turn on Workers AI [#smart-turn-on-workers-ai]

Workers AI hosts Smart Turn v2 (`@cf/pipecat-ai/smart-turn-v2`). `WorkersAISmartTurnAnalyzer`, from
`@pipedog/cloudflare`, works like `HttpSmartTurnAnalyzer` with the model on Cloudflare: through the
Worker's `AI` binding, or anywhere with an account ID and API token. It sends the candidate turn as
base64 float32 samples and reads Workers AI's `is_complete` and `probability`. A failed request
counts as "not done"; one that takes longer than `stopSecs` ends the turn.

```ts
// Smart Turn v2 on Workers AI: the AI binding in a Worker, or accountId and apiToken elsewhere.
const userParams = new LLMUserAggregatorParams({
  vadAnalyzer: new SileroVADAnalyzer(),
  userTurnStrategies: new UserTurnStrategies({
    start: [new VADUserTurnStartStrategy(), new TranscriptionUserTurnStartStrategy()],
    stop: [
      new TurnAnalyzerUserTurnStopStrategy({
        turnAnalyzer: new WorkersAISmartTurnAnalyzer({ ai: env.AI }),
      }),
    ],
  }),
});
```

Smart Turn v2 was trained on 16 kHz audio, so run the pipeline input at 16 kHz. The local Smart Turn
v3.2 above is newer and also runs on Workers (pass the model's bytes there), at the cost of the
bundle size and memory described in [Voice agents on Workers](https://polvallverdu.github.io/pipedog/docs/deployment/cloudflare-workers/#turn-detection).
On Workers AI, Deepgram Flux ([`WorkersAIFluxSTTService`](https://polvallverdu.github.io/pipedog/docs/services/stt/cloudflare.md)) detects
turns itself and needs neither.

## Provider [#provider]

Some speech-to-text services detect turns themselves. Use one of them and pass no VAD analyzer: the
user aggregator follows the service's turn signals, and nothing runs locally.

```ts
const stt = new OpenAIRealtimeSTTService({
  apiKey: process.env.OPENAI_API_KEY ?? "",
  turnDetection: { type: "server_vad" },
});
// No vadAnalyzer: the STT service's turn signals drive the turns.
const aggregators = new LLMContextAggregatorPair(context);
```

This is the mode for constrained hosts. The
[Durable Objects example](https://polvallverdu.github.io/pipedog/docs/deployment/cloudflare-workers.md) uses it: its Worker is 818 KB gzipped, against 13.7 MB with
the local models, which is over Cloudflare's 10 MB limit for a Worker.

Speech-to-text services with their own turn detection:

* `OpenAIRealtimeSTTService` (`@pipedog/openai/stt`) with `turnDetection: { type: "server_vad" }`.
* `DeepgramFluxSTTService` (`@pipedog/deepgram/flux/stt`): Deepgram's turn-taking model.
* `CartesiaTurnsSTTService` (`@pipedog/cartesia/turns/stt`).

Each of these recommends its own turn strategies to the user aggregator
([`ExternalUserTurnStrategies`](https://polvallverdu.github.io/pipedog/docs/concepts/turn-strategies/#externaluserturnstrategies)), so
passing no VAD analyzer is all it takes. To keep the bot talking when the provider hears the user,
pass `shouldInterrupt: false` to the service rather than configuring the strategies yourself.

## Going further [#going-further]

<Cards>
  <Card title="Turn strategies" href="/docs/concepts/turn-strategies">
    Every start and stop strategy, the presets, and how to write your own.
  </Card>

  <Card title="User muting" href="/docs/concepts/user-muting">
    Ignore the user during the greeting, during tool calls, or whenever the bot speaks.
  </Card>

  <Card title="Turn events & idle users" href="/docs/concepts/turn-events">
    React to turns, prompt a silent user, and measure turns and response latency.
  </Card>

  <Card title="Interruptions" href="/docs/concepts/interruptions-and-cancellation">
    What happens when the user talks over the bot, and how to control it.
  </Card>
</Cards>
