
本文详解为何无法在 websocket 流处理中直接调用 response.say(),以及如何通过 twilio rest api 动态更新进行中的通话 twiml,实现实时语音播报。
本文详解为何无法在 websocket 流处理中直接调用 response.say(),以及如何通过 twilio rest api 动态更新进行中的通话 twiml,实现实时语音播报。
在使用 Twilio 的 <StartStream> 与 WebSocket 实时语音流(如结合 Kaldi 语音识别)开发智能语音交互应用时,一个常见误区是:试图在流处理回调(如 AcceptWaveform 触发后)中直接构造并发送 VoiceResponse().say(...) —— 这不会产生任何语音输出,因为该 VoiceResponse 对象并未被发送至 Twilio,也未关联到当前通话上下文。
❌ 错误做法解析
你提供的代码片段中:
response = VoiceResponse()
# ...
if rec.AcceptWaveform(audio):
r = json.loads(rec.Result())
print(CL + r['text'] + '\n', end='', flush=True)
response.say('Sample response message') # ← 仅构建对象,未发送!此处 response.say(...) 只是本地创建了一个 TwiML 响应对象,但既未返回给 Twilio(流事件不接受 TwiML 响应),也未触发任何远程操作。Twilio 的 WebSocket 流(event: media)是单向下行数据通道,仅用于接收音频帧,不可用于下发指令或语音响应。
✅ 正确方案:动态更新进行中的通话(Modify In-Progress Call)
要让通话对方实时听到语音反馈(例如识别到关键词后播报“已收到”),必须调用 Twilio REST API 的 Calls Resource Update endpoint,向当前通话注入新的 TwiML。该操作会立即中断当前播放/静音状态,并执行新 TwiML 中的 <Say>、<Play> 等动词。
? 实现步骤
- 获取通话 SID(Call SID):在初始拨号或接听 Webhook 中,Twilio 会通过 CallSid 参数传递该值(如 request.form['CallSid']),务必将其存储(如存入内存字典、Redis 或 session)并与 WebSocket 连接关联;
- 在流处理中触发更新:当识别成功(rec.AcceptWaveform)需播报时,调用 client.calls(call_sid).update();
- 提供合法 TwiML 字符串:确保 XML 格式正确、编码安全(推荐使用 twilio.twiml.VoiceResponse 构建后转字符串)。
✅ Python 示例(完整可运行逻辑)
from twilio.rest import Client
from twilio.twiml.voice_response import VoiceResponse
import os
# 初始化 Twilio 客户端(建议从环境变量读取)
client = Client(
os.environ['TWILIO_ACCOUNT_SID'],
os.environ['TWILIO_AUTH_TOKEN']
)
# 假设 call_sid 已通过某种方式与当前 WebSocket 关联(例如:ws.id → call_sid 映射)
# 实际项目中建议使用 threading.local / asyncio contextvars / Redis 缓存
CALL_SID_MAP = {} # {websocket_id: 'CAxxx'}
def stream(ws):
rec = KaldiRecognizer(model, 16000)
while True:
message = ws.receive()
if not message:
break
packet = json.loads(message)
if packet['event'] == 'start':
# 从 start payload 中提取 callSid(若 Twilio 透传)
call_sid = packet.get('callSid') or CALL_SID_MAP.get(id(ws))
if call_sid:
print(f'Streaming started for call {call_sid}')
else:
print('Warning: callSid not available')
elif packet['event'] == 'media':
audio = base64.b64decode(packet['media']['payload'])
audio = audioop.ulaw2lin(audio, 2)
audio = audioop.ratecv(audio, 2, 1, 8000, 16000, None)[0]
if rec.AcceptWaveform(audio):
r = json.loads(rec.Result())
recognized_text = r.get('text', '').strip()
print(CL + recognized_text + '\n', end='', flush=True)
# ✅ 关键:动态更新通话 TwiML
try:
# 构建安全 TwiML
response = VoiceResponse()
response.say(
f'已收到:{recognized_text}',
voice='Polly.Joanna', # 可选:指定 AWS Polly 语音
language='en-US'
)
# 发起 REST API 调用(阻塞,生产环境建议异步)
client.calls(call_sid).update(
twiml=str(response)
)
print(f'[INFO] TwiML update sent for call {call_sid}')
except Exception as e:
print(f'[ERROR] Failed to update call {call_sid}: {e}')⚠️ 重要注意事项
- CallSid 必须准确且有效:确保 call_sid 是当前活跃通话的 SID(非 Conference SID 或其他资源 ID),且通话状态为 in-progress;
- TwiML 更新有频率限制:Twilio 对单通电话的 update 调用有速率限制(约 1 次/秒),避免高频识别后密集触发;
- 语音中断行为:每次 update 会立即中断当前播放(包括背景音乐、等待音等),并从新 TwiML 开始执行,适合短提示;
- 错误处理必加:网络超时、无效 SID、认证失败等均会导致异常,需捕获并记录日志;
- 异步优化建议:在高并发场景下,应将 client.calls(...).update() 移至线程池或使用 asyncio.to_thread() 避免阻塞 WebSocket 主循环。
✅ 总结
Response.say() 不是“即发即播”的函数,而是 TwiML 文档的构造工具;实时语音流(WebSocket)仅负责音频传输,无控制权。真正实现“边听边说”,必须借助 Twilio REST API 的通话动态更新能力——这是 Twilio 官方推荐且唯一可靠的方案。掌握 Calls.update(twiml=...) 的正确用法,是构建 Twilio 智能语音机器人不可或缺的核心技能。










