Automatically hangup Twilio calls

This commit is contained in:
Mark Backman
2025-04-22 12:43:56 -04:00
parent 972d65f61b
commit 81f4768661
4 changed files with 44 additions and 3 deletions

View File

@@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added automatic hangup logic to the Twilio serializer. This feature hangs up
the Twilio call when an `EndFrame` is received. It is enabled by default and
is configurable via the `auto_hang_up` `InputParam`. To use this feature, set
up the following env vars: `TWILIO_ACCOUNT_SID` and `TWILIO_AUTH_TOKEN`.
- Added `SmartTurnMetricsData`, which contains end-of-turn prediction metrics, - Added `SmartTurnMetricsData`, which contains end-of-turn prediction metrics,
to the `MetricsFrame`. Using `MetricsFrame`, you can now retrieve prediction to the `MetricsFrame`. Using `MetricsFrame`, you can now retrieve prediction
confidence scores and processing time metrics from the smart turn analyzers. confidence scores and processing time metrics from the smart turn analyzers.

View File

@@ -4,15 +4,19 @@
# SPDX-License-Identifier: BSD 2-Clause License # SPDX-License-Identifier: BSD 2-Clause License
# #
import asyncio
import base64 import base64
import json import json
import os
from typing import Optional from typing import Optional
from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
from pipecat.audio.utils import create_default_resampler, pcm_to_ulaw, ulaw_to_pcm from pipecat.audio.utils import create_default_resampler, pcm_to_ulaw, ulaw_to_pcm
from pipecat.frames.frames import ( from pipecat.frames.frames import (
AudioRawFrame, AudioRawFrame,
EndFrame,
Frame, Frame,
InputAudioRawFrame, InputAudioRawFrame,
InputDTMFFrame, InputDTMFFrame,
@@ -29,9 +33,11 @@ class TwilioFrameSerializer(FrameSerializer):
class InputParams(BaseModel): class InputParams(BaseModel):
twilio_sample_rate: int = 8000 # Default Twilio rate (8kHz) twilio_sample_rate: int = 8000 # Default Twilio rate (8kHz)
sample_rate: Optional[int] = None # Pipeline input rate sample_rate: Optional[int] = None # Pipeline input rate
auto_hang_up: bool = True # Whether to automatically hang up on EndFrame/CancelFrame
def __init__(self, stream_sid: str, params: InputParams = InputParams()): def __init__(self, stream_sid: str, call_sid: str, params: InputParams = InputParams()):
self._stream_sid = stream_sid self._stream_sid = stream_sid
self._call_sid = call_sid
self._params = params self._params = params
self._twilio_sample_rate = self._params.twilio_sample_rate self._twilio_sample_rate = self._params.twilio_sample_rate
@@ -47,7 +53,9 @@ class TwilioFrameSerializer(FrameSerializer):
self._sample_rate = self._params.sample_rate or frame.audio_in_sample_rate self._sample_rate = self._params.sample_rate or frame.audio_in_sample_rate
async def serialize(self, frame: Frame) -> str | bytes | None: async def serialize(self, frame: Frame) -> str | bytes | None:
if isinstance(frame, StartInterruptionFrame): if self._params.auto_hang_up and isinstance(frame, (EndFrame)):
asyncio.create_task(self._hang_up_call())
elif isinstance(frame, StartInterruptionFrame):
answer = {"event": "clear", "streamSid": self._stream_sid} answer = {"event": "clear", "streamSid": self._stream_sid}
return json.dumps(answer) return json.dumps(answer)
elif isinstance(frame, AudioRawFrame): elif isinstance(frame, AudioRawFrame):
@@ -68,6 +76,30 @@ class TwilioFrameSerializer(FrameSerializer):
elif isinstance(frame, (TransportMessageFrame, TransportMessageUrgentFrame)): elif isinstance(frame, (TransportMessageFrame, TransportMessageUrgentFrame)):
return json.dumps(frame.message) return json.dumps(frame.message)
async def _hang_up_call(self):
"""Hang up the Twilio call using the REST API."""
try:
from twilio.rest import Client
account_sid = os.getenv("TWILIO_ACCOUNT_SID")
auth_token = os.getenv("TWILIO_AUTH_TOKEN")
if not account_sid or not auth_token:
logger.warning(
"Missing Twilio credentials (TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN), cannot hang up call"
)
return
# Create Twilio client and hang up the call using the correct CallSid
client = Client(account_sid, auth_token)
client.calls(self._call_sid).update(status="completed")
logger.info(f"Successfully terminated Twilio call {self._call_sid}")
except ImportError:
logger.warning("Twilio Python SDK not installed. Install with: pip install twilio")
except Exception as e:
logger.exception(f"Failed to hang up Twilio call: {e}")
async def deserialize(self, data: str | bytes) -> Frame | None: async def deserialize(self, data: str | bytes) -> Frame | None:
message = json.loads(data) message = json.loads(data)

View File

@@ -207,6 +207,7 @@ class FastAPIWebsocketOutputTransport(BaseOutputTransport):
async def stop(self, frame: EndFrame): async def stop(self, frame: EndFrame):
await super().stop(frame) await super().stop(frame)
await self._write_frame(frame)
await self._client.disconnect() await self._client.disconnect()
async def cancel(self, frame: CancelFrame): async def cancel(self, frame: CancelFrame):

View File

@@ -157,7 +157,8 @@ class WebsocketServerInputTransport(BaseInputTransport):
self, websocket: websockets.WebSocketServerProtocol, session_timeout: int self, websocket: websockets.WebSocketServerProtocol, session_timeout: int
): ):
"""Wait for session_timeout seconds, if the websocket is still open, """Wait for session_timeout seconds, if the websocket is still open,
trigger timeout event.""" trigger timeout event.
"""
try: try:
await asyncio.sleep(session_timeout) await asyncio.sleep(session_timeout)
if not websocket.closed: if not websocket.closed:
@@ -205,6 +206,8 @@ class WebsocketServerOutputTransport(BaseOutputTransport):
if isinstance(frame, StartInterruptionFrame): if isinstance(frame, StartInterruptionFrame):
await self._write_frame(frame) await self._write_frame(frame)
self._next_send_time = 0 self._next_send_time = 0
elif isinstance(frame, EndFrame):
await self._write_frame(frame)
async def send_message(self, frame: TransportMessageFrame | TransportMessageUrgentFrame): async def send_message(self, frame: TransportMessageFrame | TransportMessageUrgentFrame):
await self._write_frame(frame) await self._write_frame(frame)