Skip to content

feat(twilio): play recordings instead of Say on a realtime call - #1479

Merged
yileicn merged 3 commits into
SciSharp:masterfrom
li-zhou-dev:features/twilio-realtime-recorded-speech
Nov 4, 2026
Merged

yileicn merged 3 commits into
SciSharp:masterfrom
li-zhou-dev:features/twilio-realtime-recorded-speech

Conversation

@li-zhou-dev

@li-zhou-dev li-zhou-dev commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

With Say on, the reconnect prompt and the hang-up and transfer lines of a realtime call were spoken with Say, while the model speaks with its own voice, so the caller heard two voices in one call.

  • AppendSpeech(preferRecording) plays a prerecorded prompt as recorded even when its text is configured in PromptTexts. The prompts queued before the realtime stream and the prompt of a stream reconnect use it.
  • SaveSpeechAsync(preferAudio) synthesizes the mp3 even when Say is on. The hang-up and transfer functions ask for it when the call is a realtime call (TwilioService.IsRealtimeCall).
  • TwilioInboundController records the mode once, by the label of the agent that took the call, as the call state twilio_realtime_call, so a routing to an agent without the label does not change it.

Calls that are not realtime behave as before.

With Say on, the reconnect prompt and the hang-up and transfer lines of a
realtime call were spoken with Say, while the model speaks with its own voice,
so the caller heard two voices in one call.

- AppendSpeech(preferRecording) plays a prerecorded prompt as recorded even
  when its text is configured in PromptTexts. The prompts queued before the
  realtime stream and the prompt of a stream reconnect use it.
- SaveSpeechAsync(preferAudio) synthesizes the mp3 even when Say is on. The
  hang-up and transfer functions ask for it when their agent has the realtime
  label (TwilioService.IsRealtimeAgentAsync).

Calls that are not realtime behave as before.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@qodo-code-review

qodo-code-review Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Code Review by Qodo

Grey Divider

Sorry, something went wrong

We weren't able to complete the code review on our side. Please try again manually by commenting /agentic_review on this PR.

Grey Divider

Qodo Logo

@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Play recorded prompts and synthesized speech on realtime Twilio calls

🐞 Bug fix ✨ Enhancement 🕐 20-40 Minutes

Grey Divider

AI Description

• Play recorded prompts before realtime streams and during reconnects, even when Say is enabled.
• Synthesize hang-up and transfer lines for realtime agents instead of using Twilio Say.
• Preserve existing speech behavior for non-realtime calls.
Diagram

graph TD
  Prompts["Stream prompts"] --> Append["AppendSpeech"] --> Play["TwiML Play"]
  Actions["Hang-up or transfer"] --> Label{"Realtime agent?"} --> Save["SaveSpeechAsync"] -->|standard text| Say["TwiML Say"]
  Save -->|realtime MP3| Play
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Track speech mode in call state
  • ➕ Reflects the mode chosen when the call started, even if the current agent changes.
  • ➕ Avoids fetching agent metadata when preparing a hang-up or transfer line.
  • ➖ Requires call-state propagation and lifecycle handling.
  • ➖ Adds complexity for calls without established state.

Recommendation: The label-based approach matches inbound realtime routing and keeps this fix localized. If calls can switch agents, verify that CurrentAgentId still represents the call's stream mode; otherwise, call-level state would be safer.

Files changed (4) +33 / -11

Enhancement (1) +28 / -8
TwilioService.csAdd realtime speech playback preferences +28/-8

Add realtime speech playback preferences

• Adds opt-in overrides for playing recorded prompts and synthesizing MP3s when Say is enabled. Applies recorded playback to prompts queued before bidirectional streams and adds an agent-label lookup for hang-up and transfer decisions.

src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs

Bug fix (3) +5 / -3
TwilioReconnectController.csPlay the recorded reconnect prompt +1/-1

Play the recorded reconnect prompt

• Requests recorded playback for the reconnect prompt instead of allowing configured prompt text to trigger Say.

src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioReconnectController.cs

HangupPhoneCallFn.csSynthesize realtime hang-up speech +2/-1

Synthesize realtime hang-up speech

• Checks whether the current agent is realtime and requests an MP3 for its generated ending line.

src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HangupPhoneCallFn.cs

TransferPhoneCallFn.csSynthesize realtime transfer speech +2/-1

Synthesize realtime transfer speech

• Checks whether the current agent is realtime and requests an MP3 for its transfer announcement.

src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/TransferPhoneCallFn.cs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Transfer and hang-up messages can still use Say during an active realtime call because audio preference is derived from the current agent’s label rather than the active call/session mode.

1 open issue
What changed in this PR

This PR updates Twilio speech handling to avoid switching to Twilio Say for prompts and closing messages during realtime calls.

Changes:

  • Prefer recordings for prompts before realtime streams and during reconnects.
  • Allow speech synthesis even when SayGeneratedSpeech is enabled.
  • Use the current agent’s realtime label to request synthesized hang-up and transfer messages.
File Description
src/​Plugins/​BotSharp.Plugin.Twilio/​Services/​TwilioService.cs Adds audio preferences and realtime-agent detection.
src/​Plugins/​BotSharp.Plugin.Twilio/​OutboundPhoneCallHandler/​Functions/​TransferPhoneCallFn.cs Requests audio for realtime-agent transfer messages.
src/​Plugins/​BotSharp.Plugin.Twilio/​OutboundPhoneCallHandler/​Functions/​HangupPhoneCallFn.cs Requests audio for realtime-agent ending messages.
src/​Plugins/​BotSharp.Plugin.Twilio/​Controllers/​TwilioReconnectController.cs Prefers recordings for reconnect prompts.

Review effort: Balanced

li-zhou-dev and others added 2 commits November 4, 2026 14:06
The hang-up and transfer lines asked for a recording when the current agent
carried the realtime label. After a routing to an agent without the label the
stream stays, but the lines were spoken with Say again.

TwilioInboundController records the mode once, by the label of the agent that
took the call, as the call state twilio_realtime_call. TwilioService.IsRealtimeCall
reads it, so the routing no longer matters and no agent has to be looked up.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
TwilioInboundController wrote twilio_realtime_call only for a realtime agent and
never cleared it, so a conversation later served by an agent without the label
kept reading as realtime. It is now written as true or false on each call.

Also puts back two trailing spaces in ReturnBidirectionalMediaStreamsInstructions
that an earlier commit of this branch removed, so the diff holds no whitespace-only
lines.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@yileicn

yileicn commented Nov 4, 2026

Copy link
Copy Markdown
Member

reviewed

@yileicn
yileicn merged commit c37bc17 into SciSharp:master Nov 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants