openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
251 lines (214 loc) • 8.67 kB
Markdown
summary: "Telephony TTS, inbound policy, per-number routing, and call lifecycle timers"
read_when:
- You are choosing the TTS voice used on calls
- You are enabling inbound calls or per-number routing
- You need to tune the stale call reaper or call duration caps
title: "Voice call TTS and inbound calls"
sidebarTitle: "TTS and inbound calls"
Telephony text-to-speech, inbound policy and per-number routing, the spoken output contract, conversation startup, and the stale call reaper. Part of the [Voice call plugin](/plugins/voice-call) guide.
## TTS for calls
Voice Call uses the core `tts` configuration for streaming speech on
calls. You can override it under the plugin config with the **same shape** —
it deep-merges with `tts`.
```json5
{
tts: {
provider: "elevenlabs",
providers: {
elevenlabs: {
speakerVoiceId: "pMsXgVXv3BLzUgSXRplE",
modelId: "eleven_multilingual_v2",
},
},
},
}
```
<Warning>
**Microsoft speech is ignored for voice calls.** Telephony synthesis requires
a provider that implements telephony-target output; the Microsoft speech
provider does not, so it is skipped for calls and other providers in the
fallback chain are tried instead.
</Warning>
Behavior notes:
- Legacy `tts.<provider>` keys inside plugin config (`openai`, `elevenlabs`, `microsoft`, `edge`) are repaired by `openclaw doctor --fix`; committed config should use `tts.providers.<provider>`.
- Core TTS is used when Twilio media streaming is enabled; otherwise calls fall back to provider-native voices.
- If a Twilio media stream is already active, Voice Call does not fall back to TwiML `<Say>`. If telephony TTS is unavailable in that state, the playback request fails instead of mixing two playback paths.
- When telephony TTS falls back to a secondary provider, Voice Call logs a warning with the provider chain (`from`, `to`, `attempts`) for debugging.
- When Twilio barge-in or stream teardown clears the pending TTS queue, queued playback requests settle instead of hanging callers awaiting playback completion.
- Resumed caller speech discards older automatic replies that are still being generated. Twilio streaming reacts to speech-start and partial transcripts; carrier webhook calls react when a new speech event arrives. Explicit speech requests remain available, and already accepted agent work can finish without speaking an obsolete reply.
### TTS examples
<Tabs>
<Tab title="Core TTS only">
```json5
{
tts: {
provider: "openai",
providers: {
openai: { speakerVoice: "alloy" },
},
},
}
```
</Tab>
<Tab title="Override to ElevenLabs (calls only)">
```json5
{
plugins: {
entries: {
"voice-call": {
config: {
tts: {
provider: "elevenlabs",
providers: {
elevenlabs: {
apiKey: "elevenlabs_key",
speakerVoiceId: "pMsXgVXv3BLzUgSXRplE",
modelId: "eleven_multilingual_v2",
},
},
},
},
},
},
},
}
```
</Tab>
<Tab title="OpenAI model override (deep-merge)">
```json5
{
plugins: {
entries: {
"voice-call": {
config: {
tts: {
providers: {
openai: {
model: "gpt-4o-mini-tts",
speakerVoice: "marin",
},
},
},
},
},
},
},
}
```
</Tab>
</Tabs>
## Inbound calls
Inbound policy defaults to `disabled`. To enable inbound calls, set:
```json5
{
inboundPolicy: "allowlist",
allowFrom: ["+15550001234"],
inboundGreeting: "Hello! How can I help?",
}
```
<Warning>
`inboundPolicy: "allowlist"` is a low-assurance caller-ID screen. The plugin
normalizes the provider-supplied `From` value and compares it to `allowFrom`.
Webhook verification authenticates provider delivery and payload integrity,
but it does **not** prove PSTN/VoIP caller-number ownership. Treat
`allowFrom` as caller-ID filtering, not strong caller identity.
</Warning>
Auto-responses use the agent system. Tune with `responseModel`,
`responseSystemPrompt`, and `responseTimeoutMs`.
### Per-number routing
Use `numbers` when one Voice Call plugin receives calls for multiple phone
numbers and each number should behave like a different line. For example,
one number can use a casual personal assistant while another uses a business
persona, a different response agent, and a different TTS voice.
Routes are selected from the provider-supplied dialed `To` number. Keys must
be E.164 numbers. When a call arrives, Voice Call resolves the matching
route once, stores the matched route on the call record, and reuses that
effective config for the greeting, classic auto-response path, realtime
consult path, and TTS playback. If no route matches, the global Voice Call
config is used. Outbound calls do not use `numbers`; pass the outbound
target, message, and session explicitly when initiating the call.
Route overrides support exactly these fields:
- `inboundGreeting`
- `tts`
- `agentId`
- `responseModel`
- `responseSystemPrompt`
- `responseTimeoutMs`
The `tts` route value deep-merges over the global Voice Call `tts` config, so
you can usually override only the provider voice:
```json5
{
inboundGreeting: "Hello from the main line.",
responseSystemPrompt: "You are the default voice assistant.",
tts: {
provider: "openai",
providers: {
openai: { speakerVoice: "coral" },
},
},
numbers: {
"+15550001111": {
inboundGreeting: "Silver Fox Cards, how can I help?",
responseSystemPrompt: "You are a concise baseball card specialist.",
tts: {
providers: {
openai: { speakerVoice: "alloy" },
},
},
},
},
}
```
### Spoken output contract
For auto-responses, Voice Call appends a strict spoken-output contract to
the system prompt requiring a `{"spoken":"..."}` JSON reply. Voice Call
extracts speech text defensively:
- Ignores payloads marked as reasoning/error content.
- Parses direct JSON, fenced JSON, or inline `"spoken"` keys.
- Falls back to plain text and removes likely planning/meta lead-in paragraphs.
This keeps spoken playback focused on caller-facing text and avoids leaking
planning text into audio.
### Conversation startup behavior
For outbound `conversation` calls, first-message handling is tied to live
playback state:
- Barge-in queue clear and auto-response are suppressed only while the initial greeting is actively speaking.
- If initial playback fails, the call returns to `listening` and the initial message remains queued for retry.
- Initial playback for Twilio streaming starts on stream connect without extra delay.
- Barge-in aborts active playback and clears queued-but-not-yet-playing Twilio TTS entries. Cleared entries resolve as skipped, so follow-up response logic can continue without waiting on audio that will never play.
- Realtime voice conversations use the realtime stream's own opening turn. Voice Call does **not** post a legacy `<Say>` TwiML update for that initial message, so outbound `<Connect><Stream>` sessions stay attached.
### Twilio stream disconnect grace
When a Twilio classic streaming or realtime media stream disconnects, Voice
Call waits **2000 ms** before auto-ending the call:
- If the stream reconnects during that window, auto-end is canceled.
- If no stream re-registers after the grace period, the call is ended to prevent stuck active calls.
- Realtime bridge/session resources, queued audio, transcript ownership, and in-flight consult work close immediately. Only call/provider finalization waits for reconnect.
## Stale call reaper
Use `staleCallReaperSeconds` (default **120**) to end calls that are never
answered and never reach a live conversation state, for example notify-mode
calls where the provider never delivers a terminal webhook. Set it to `0` to
disable.
The reaper runs every 30 seconds and only ends calls that have no
`answeredAt` timestamp and are not already in a terminal or live
(`speaking`/`listening`) state, so answered conversations are never reaped
by this timer; `maxDurationSeconds` (default 300) is the separate cap that
ends answered calls that run too long.
For notify-style flows where carriers can be slow to deliver ring/answer
webhooks, raise `staleCallReaperSeconds` past the default so slow-but-normal
calls are not reaped early; `120`-`300` seconds is a reasonable production
range.
```json5
{
plugins: {
entries: {
"voice-call": {
config: {
maxDurationSeconds: 300,
staleCallReaperSeconds: 120,
},
},
},
},
}
```