Skip to content
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 38 additions & 14 deletions src/pipecat_asterisk/serializer/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
# SPDX-License-Identifier: BSD-2-Clause
#

import inspect
from typing import Awaitable, Callable, Optional, cast
from typing import Any, Awaitable, Callable, Optional, cast
from loguru import logger
from pipecat.audio.dtmf.types import KeypadEntry
from pipecat.audio.utils import create_stream_resampler
Expand Down Expand Up @@ -77,6 +76,24 @@ def __init__(self, sample_rate: int = 0):
sample_rate
) # What sample rate is used in Asterisk websocket channel. If 0, will be populated during setup or from MEDIA_START event

# Pre-built dispatch tables for `serialize`. Keyed by frame class so
# `serialize` does a single dict lookup per frame instead of
# `getattr(self, f"_frame_{type(frame).__name__.lower()}")` + an
# `inspect.isawaitable` check on the hot path. Async handlers are
# kept in a separate table so we know at lookup time whether to
# `await` the result.
self._sync_frame_handlers: dict[type, Callable[[Frame], str | bytes | None]] = {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The same idea as here

We can parse the handlers once in init to avoid overhead on every frame, save them into sync/async dicts. And splitting the handlers into sync and async is a good point; I didn't think about it

AsteriskCommandFrame: self._frame_asteriskcommandframe,
EndFrame: self._frame_endframe,
CancelFrame: self._frame_cancelframe,
InterruptionFrame: self._frame_interruptionframe,
}
self._async_frame_handlers: dict[
type, Callable[[Frame], Awaitable[str | bytes | None]]
] = {
OutputAudioRawFrame: self._frame_outputaudiorawframe,
}

def _handle_event(self, message: dict) -> Frame | None:
"""Call the event handler if the handler is defined in the class, otherwise return None.

Expand Down Expand Up @@ -395,24 +412,31 @@ async def setup(self, frame: StartFrame):
async def serialize(self, frame: Frame) -> str | bytes | None:
"""Convert a frame to its serialized representation suitable for Asterisk WebSocket channel.

Looks up the frame's exact type in the precomputed sync/async
dispatch tables built in ``__init__``. This avoids the per-frame
``getattr`` + f-string + ``inspect.isawaitable`` overhead the old
reflective form paid, which matters because audio frames go through
this method dozens of times per second.

Args:
frame: The frame to serialize.

Returns:
Serialized frame data as string, bytes, or None if serialization fails.
"""
handler = getattr(self, f"_frame_{type(frame).__name__.lower()}", None)
if callable(handler):
result = handler(frame)
if inspect.isawaitable(result):
return cast(str | bytes | None, await result)
else:
return cast(str | bytes | None, result)
else:
logger.trace(
f"Received unhandled frame type in Asterisk WebSocket serializer: {type(frame)}. Frame: {frame}"
)
return None
frame_type = type(frame)
sync_handler = self._sync_frame_handlers.get(frame_type)
if sync_handler is not None:
return sync_handler(frame)
async_handler = self._async_frame_handlers.get(frame_type)
if async_handler is not None:
return await async_handler(frame)
logger.trace(
"Received unhandled frame type in Asterisk WebSocket serializer: {}. Frame: {}",
frame_type,
frame,
)
return None

async def deserialize(self, data: str | bytes) -> Frame | None:
"""Convert serialized data from Asterisk's websocket channel to a frame object.
Expand Down