From e24776f487ffeb50059755a22a6235b0e1eddadf Mon Sep 17 00:00:00 2001 From: Salman Altuwayjiri Date: Wed, 27 May 2026 00:50:39 +0300 Subject: [PATCH 1/5] perf(serializer): dict-based frame handler dispatch in serialize() `serialize` ran on every outbound frame and reflected on the frame's class to locate its handler: handler = getattr(self, f"_frame_{type(frame).__name__.lower()}", None) if callable(handler): result = handler(frame) if inspect.isawaitable(result): return await result return result Each call paid for an f-string allocation, a `str.lower()`, a `getattr`, a `callable` check, and an `inspect.isawaitable` walk - per frame. For outbound audio this fires dozens of times per second per call. Switch to two pre-built dispatch tables keyed by frame class, populated in `__init__`: self._sync_frame_handlers: dict[type, ...] # EndFrame, CancelFrame, ... self._async_frame_handlers: dict[type, ...] # OutputAudioRawFrame `serialize` becomes a single `type(frame)` lookup against each table. Because the sync/async split is known at registration time we no longer need `inspect.isawaitable` on the hot path - sync handlers return their result directly, async ones are awaited. The old reflective form matched only the exact class name (it used `type(frame).__name__`, not `isinstance`), so an exact-type lookup is behavior-preserving. Unhandled frames still produce a `logger.trace` and return None. Also switches the unhandled-frame trace log to loguru's positional-arg form so the format string isn't expanded when TRACE is filtered out. --- src/pipecat_asterisk/serializer/serializer.py | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/pipecat_asterisk/serializer/serializer.py b/src/pipecat_asterisk/serializer/serializer.py index e125030..98e9760 100644 --- a/src/pipecat_asterisk/serializer/serializer.py +++ b/src/pipecat_asterisk/serializer/serializer.py @@ -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 @@ -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]] = { + 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. @@ -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. From b0fb96ba041774bade586eeb1d199ac0e5a22876 Mon Sep 17 00:00:00 2001 From: Salman Altuwayjiri Date: Wed, 27 May 2026 00:52:27 +0300 Subject: [PATCH 2/5] perf(serializer): dict-based event handler dispatch in _handle_event() `_handle_event` previously located its handler via reflection: handler = getattr(self, f"_ev_{message_type.lower()}", None) if callable(handler): return handler(message) Same pattern as the frame dispatch refactor: f-string allocation + `str.lower()` + `getattr` + `callable` check, per event. Events from chan_websocket are rare compared to audio frames (one MEDIA_START per call, occasional DTMF, etc.), so this is mostly a consistency change rather than a measured hot-path win. But it's the same shape as the frame-handler refactor, so the dispatch table fits naturally alongside the sync/async frame tables and the explicit registration makes the supported event set easier to see at a glance. A new `_event_handlers: dict[str, ...]` is populated in `__init__` and keyed by the exact event-name string Asterisk sends. The lookup is a single dict get. Unknown events fall through to the same `logger.info` "unhandled event" branch as before. Both the warning and info log calls now use loguru's positional-arg form so the format string isn't expanded when the level is filtered out. Also drops the unused `Any` and `cast` imports left over from the previous reflective form. --- src/pipecat_asterisk/serializer/serializer.py | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/pipecat_asterisk/serializer/serializer.py b/src/pipecat_asterisk/serializer/serializer.py index 98e9760..d6449cd 100644 --- a/src/pipecat_asterisk/serializer/serializer.py +++ b/src/pipecat_asterisk/serializer/serializer.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: BSD-2-Clause # -from typing import Any, Awaitable, Callable, Optional, cast +from typing import Awaitable, Callable, Optional from loguru import logger from pipecat.audio.dtmf.types import KeypadEntry from pipecat.audio.utils import create_stream_resampler @@ -94,28 +94,41 @@ def __init__(self, sample_rate: int = 0): OutputAudioRawFrame: self._frame_outputaudiorawframe, } + # Event handlers are keyed by Asterisk event-name string, matching + # the values produced by the protocol parser. All handlers are sync + # so one table is enough; same goal as the frame tables - skip the + # per-event `getattr` + `f"_ev_{name.lower()}"` lookup. + self._event_handlers: dict[str, Callable[[dict], Frame | None]] = { + "MEDIA_START": self._ev_media_start, + "MEDIA_XOFF": self._ev_media_xoff, + "MEDIA_XON": self._ev_media_xon, + "DTMF_END": self._ev_dtmf_end, + "QUEUE_DRAINED": self._ev_queue_drained, + } + def _handle_event(self, message: dict) -> Frame | None: - """Call the event handler if the handler is defined in the class, otherwise return None. + """Dispatch an Asterisk event through the precomputed handler table. - The handler methods should be named as "_ev_{event_name.lower()}" and should take the event message as a dictionary and return a Frame or None. + The event-handler table is built once in ``__init__`` and keyed by + the exact event-name string Asterisk sends. Unknown events fall + through to the same warning path as the original reflective form. Args: message: The event message as a dictionary. """ - message_type = message.get("event", None) + message_type = message.get("event") if message_type is None: logger.warning( - f"Received Asterisk WebSocket message without 'event' field: {message}" + "Received Asterisk WebSocket message without 'event' field: {}", + message, ) return None - handler = getattr(self, f"_ev_{message_type.lower()}", None) - if callable(handler): - typed_handler = cast(Callable[[dict], Frame | None], handler) - return typed_handler(message) - else: - logger.info(f"Received unhandled Asterisk WebSocket event: {message}") - return None + handler = self._event_handlers.get(message_type) + if handler is not None: + return handler(message) + logger.info("Received unhandled Asterisk WebSocket event: {}", message) + return None ### Asterisk Event handlers ### From c34ac4f438c5182b736561903320f4eaf820bd59 Mon Sep 17 00:00:00 2001 From: Salman Altuwayjiri Date: Wed, 27 May 2026 00:53:23 +0300 Subject: [PATCH 3/5] perf(transport): lazy log formatting + isinstance check in write_audio_frame Two small cleanups on the outbound write path: - All `logger.warning` / `logger.error` / `logger.trace` / `logger.debug` / `logger.info` calls in `_media_start_handler` and `write_audio_frame` used eager f-strings. Python builds the formatted string before the call even reaches loguru, so the work happens on every call even when the level is filtered out at the sink. Switch to loguru's positional-arg form so a filtered call drops the formatting work entirely. This matches the same change made on the hot-path log lines in flow_controller in PR #3. - Replace `type(payload) == bytes` with `isinstance(payload, bytes)` in the post-serialize guard. `isinstance` is the conventional Python check, slightly faster in CPython 3.12+, and correctly accepts `bytes` subclasses if any ever appear in this code path. No behavioral changes - just smaller per-frame cost when logging is filtered and a more idiomatic type check. --- src/pipecat_asterisk/transport/transport.py | 42 ++++++++++++++------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/pipecat_asterisk/transport/transport.py b/src/pipecat_asterisk/transport/transport.py index 9360e3e..05fe5b1 100644 --- a/src/pipecat_asterisk/transport/transport.py +++ b/src/pipecat_asterisk/transport/transport.py @@ -57,25 +57,32 @@ async def _media_start_handler(self, frame: InputTransportMessageFrame): if ptime <= 0 or psize <= 0: logger.error( - f"Invalid ptime ({ptime}) or psize ({psize}) in MEDIA_START event {frame.message}. Cannot initialize flow controller." + "Invalid ptime ({}) or psize ({}) in MEDIA_START event {}. Cannot initialize flow controller.", + ptime, + psize, + frame.message, ) return self._flow_controller = FlowController(ptime, psize, self._client) logger.debug( - f"Initialized flow controller with ptime={ptime} ms, psize={psize} bytes. Remote buffer low water mark: {self._flow_controller._remote_buffer_low_water} bytes, high water mark: {self._flow_controller._remote_buffer_high_water} bytes." + "Initialized flow controller with ptime={} ms, psize={} bytes. Remote buffer low water mark: {} bytes, high water mark: {} bytes.", + ptime, + psize, + self._flow_controller._remote_buffer_low_water, + self._flow_controller._remote_buffer_high_water, ) # Send START_MEDIA_BUFFERING command to Asterisk WebSocket channel to enable audio buffering on the Asterisk side if self._client.is_closing or not self._client.is_connected: logger.warning( - f"Cannot send START_MEDIA_BUFFERING command because the WebSocket client is closing or already closed." + "Cannot send START_MEDIA_BUFFERING command because the WebSocket client is closing or already closed." ) return if not self._params.serializer: logger.error( - f"Cannot send START_MEDIA_BUFFERING command because no serializer is set in the transport parameters." + "Cannot send START_MEDIA_BUFFERING command because no serializer is set in the transport parameters." ) return try: @@ -85,11 +92,14 @@ async def _media_start_handler(self, frame: InputTransportMessageFrame): if cmd: await self._client.send(cmd) logger.info( - f"Sent START_MEDIA_BUFFERING command to Asterisk WebSocket channel to enable audio buffering." + "Sent START_MEDIA_BUFFERING command to Asterisk WebSocket channel to enable audio buffering." ) except Exception as e: logger.error( - f"{self} exception sending START_MEDIA_BUFFERING: {e.__class__.__name__} ({e})" + "{} exception sending START_MEDIA_BUFFERING: {} ({})", + self, + e.__class__.__name__, + e, ) async def process_frame(self, frame: Frame, direction: FrameDirection): @@ -126,19 +136,19 @@ async def write_audio_frame(self, frame: OutputAudioRawFrame) -> bool: if self._client.is_closing or not self._client.is_connected: logger.warning( - f"Cannot write audio frame because the WebSocket client is closing or already closed." + "Cannot write audio frame because the WebSocket client is closing or already closed." ) return False if not self._params.serializer: logger.error( - f"Serializer is not set in transport parameters. Cannot write audio frame." + "Serializer is not set in transport parameters. Cannot write audio frame." ) return False if self._flow_controller is None: logger.error( - f"Flow controller is not initialized. Cannot write audio frame." + "Flow controller is not initialized. Cannot write audio frame." ) return False @@ -151,21 +161,27 @@ async def write_audio_frame(self, frame: OutputAudioRawFrame) -> bool: try: payload = await self._params.serializer.serialize(frame) if payload: - if type(payload) == bytes: + if isinstance(payload, bytes): self._flow_controller(payload) return True else: logger.error( - f"Serialized audio frame is not bytes. Got {type(payload)} instead. Cannot write audio frame." + "Serialized audio frame is not bytes. Got {} instead. Cannot write audio frame.", + type(payload), ) return False else: logger.trace( - f"Serializer returned None or empty payload. Cannot write audio frame." + "Serializer returned None or empty payload. Cannot write audio frame." ) return False except Exception as e: - logger.error(f"{self} exception sending data: {e.__class__.__name__} ({e})") + logger.error( + "{} exception sending data: {} ({})", + self, + e.__class__.__name__, + e, + ) return False From 137e7c91bb1796708ab282b11f79ee3a534a4b67 Mon Sep 17 00:00:00 2001 From: Salman Altuwayjiri Date: Wed, 27 May 2026 00:55:03 +0300 Subject: [PATCH 4/5] test(serializer): add unit tests for dispatch tables Adds the first tests for AsteriskFrameSerializer plus `pytest` and `pytest-asyncio` in the dev dependency group (this is the first test file in the repo, so the pytest config block is also new). 15 cases covering the public surface of the new dispatch: Frame dispatch: - Every sync handler (EndFrame, CancelFrame, InterruptionFrame, AsteriskCommandFrame) is reachable through `serialize()` and produces the expected wire-format command. - The async handler (OutputAudioRawFrame) is awaited and returns the audio bytes through the no-resampling fast path. - Sync handlers return a plain string, never a coroutine - a regression guard for the sync/async split that lets us drop the per-frame `inspect.isawaitable` check. - Unhandled frame types (e.g. StartFrame) fall through to None. Event dispatch: - DTMF_END produces InputDTMFFrame. - QUEUE_DRAINED produces InputTransportMessageFrame carrying the message. - MEDIA_XOFF / MEDIA_XON dispatch to their handlers (which log and return None - we check the dispatch found them, not the log output). - Messages without an `event` field return None. - Unknown event types return None. Dispatch-table integrity: - All event-handler keys are uppercase (matching chan_websocket's wire format). - Sync and async frame-handler tables have no overlapping keys (would be ambiguous dispatch). Run with `uv run pytest`. --- pyproject.toml | 6 ++ tests/__init__.py | 0 tests/test_serializer.py | 187 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_serializer.py diff --git a/pyproject.toml b/pyproject.toml index 7fafb78..a8ecb77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,5 +28,11 @@ build-backend = "uv_build" dev = [ "dotenv>=0.9.9", "pipecat-ai[google,silero]>=1.1.0", + "pytest>=8.0", + "pytest-asyncio>=0.24", "uvicorn>=0.46.0", ] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_serializer.py b/tests/test_serializer.py new file mode 100644 index 0000000..80a6e94 --- /dev/null +++ b/tests/test_serializer.py @@ -0,0 +1,187 @@ +"""Unit tests for AsteriskFrameSerializer dispatch tables. + +These tests lock in the behavior of the dict-based frame/event dispatch +introduced to replace the previous `getattr(...)` reflection. The goal is +to make sure every handler registered in `__init__` is reachable through +the public `serialize` / `_handle_event` entry points, and that unknown +inputs fall through to the same "unhandled" branches as before. +""" + +import pytest + +from pipecat.frames.frames import ( + CancelFrame, + EndFrame, + InterruptionFrame, + InputDTMFFrame, + InputTransportMessageFrame, + StartFrame, +) + +from pipecat_asterisk.serializer.serializer import ( + AsteriskCommandFrame, + AsteriskFrameSerializer, +) + + +# --------------------------------------------------------------------------- +# serialize() — sync handlers +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "frame, expected_cmd", + [ + (EndFrame(), "HANGUP"), + (CancelFrame(), "HANGUP"), + (InterruptionFrame(), "FLUSH_MEDIA"), + (AsteriskCommandFrame("START_MEDIA_BUFFERING"), "START_MEDIA_BUFFERING"), + (AsteriskCommandFrame("REPORT_QUEUE_DRAINED"), "REPORT_QUEUE_DRAINED"), + ], +) +async def test_serialize_dispatches_sync_handlers(frame, expected_cmd): + """Every entry in `_sync_frame_handlers` must be reachable via serialize(). + + We don't pin the exact wire format here (that's the protocol layer's + contract); we only check that the produced string contains the command + word, so this stays robust if the protocol formatting changes. + """ + serializer = AsteriskFrameSerializer() + result = await serializer.serialize(frame) + assert isinstance(result, str) + assert expected_cmd in result + + +async def test_serialize_returns_none_for_unhandled_frame(): + """Frames not registered in either dispatch table fall through to None. + + `StartFrame` is intentionally not in `_sync_frame_handlers` or + `_async_frame_handlers` — `setup()` handles it separately. + """ + serializer = AsteriskFrameSerializer() + result = await serializer.serialize( + StartFrame(audio_in_sample_rate=16000, audio_out_sample_rate=16000) + ) + assert result is None + + +# --------------------------------------------------------------------------- +# serialize() — async handler (OutputAudioRawFrame) +# --------------------------------------------------------------------------- + + +async def test_serialize_dispatches_async_audio_handler(): + """OutputAudioRawFrame goes through the async handler table. + + The handler short-circuits and returns the raw bytes unchanged when + the pipeline rate matches the Asterisk rate, so this also exercises + the no-resampling fast path. + """ + from pipecat.frames.frames import OutputAudioRawFrame + + serializer = AsteriskFrameSerializer() + # Match rates so the handler returns audio without going through a + # resampler that isn't initialized in this test. + serializer._pipeline_out_sample_rate = 16000 + serializer._asterisk_sample_rate = 16000 + + audio = b"\x00\x01" * 320 # 640 bytes — one slin16 frame + frame = OutputAudioRawFrame(audio=audio, sample_rate=16000, num_channels=1) + result = await serializer.serialize(frame) + assert result == audio + + +# --------------------------------------------------------------------------- +# serialize() — sync handlers must NOT be awaited +# --------------------------------------------------------------------------- + + +async def test_serialize_sync_handler_result_is_not_a_coroutine(): + """Regression guard for the sync/async split. + + The previous implementation called `inspect.isawaitable` on every + return value. After this refactor sync handlers are looked up in a + separate table and must return a plain string, never a coroutine. + """ + import inspect as _inspect + + serializer = AsteriskFrameSerializer() + result = await serializer.serialize(EndFrame()) + assert not _inspect.isawaitable(result) + assert isinstance(result, str) + + +# --------------------------------------------------------------------------- +# _handle_event() — registered events +# --------------------------------------------------------------------------- + + +def test_handle_event_dispatches_dtmf_end(): + serializer = AsteriskFrameSerializer() + frame = serializer._handle_event({"event": "DTMF_END", "digit": "5"}) + assert isinstance(frame, InputDTMFFrame) + + +def test_handle_event_dispatches_queue_drained(): + serializer = AsteriskFrameSerializer() + msg = {"event": "QUEUE_DRAINED"} + frame = serializer._handle_event(msg) + assert isinstance(frame, InputTransportMessageFrame) + assert frame.message == msg + + +def test_handle_event_dispatches_xoff_and_xon_to_none(): + """MEDIA_XOFF and MEDIA_XON handlers log but return None. + + The handlers are registered, so the dispatch must find them and not + fall through to the "unhandled" branch. + """ + serializer = AsteriskFrameSerializer() + assert serializer._handle_event({"event": "MEDIA_XOFF"}) is None + assert serializer._handle_event({"event": "MEDIA_XON"}) is None + + +# --------------------------------------------------------------------------- +# _handle_event() — missing / unknown event +# --------------------------------------------------------------------------- + + +def test_handle_event_missing_event_field_returns_none(): + serializer = AsteriskFrameSerializer() + assert serializer._handle_event({"not_event": "foo"}) is None + + +def test_handle_event_unknown_event_returns_none(): + serializer = AsteriskFrameSerializer() + assert serializer._handle_event({"event": "SOMETHING_NEW"}) is None + + +# --------------------------------------------------------------------------- +# Dispatch table integrity +# --------------------------------------------------------------------------- + + +def test_event_handler_table_keys_are_uppercase_event_names(): + """The dispatch keys must match Asterisk's wire-format event names exactly. + + The chan_websocket protocol uses uppercase event names like + `MEDIA_START`, `DTMF_END`, `QUEUE_DRAINED`. A lowercase key would + silently fall through the dispatch. + """ + serializer = AsteriskFrameSerializer() + assert all(k == k.upper() for k in serializer._event_handlers) + assert "MEDIA_START" in serializer._event_handlers + assert "DTMF_END" in serializer._event_handlers + assert "QUEUE_DRAINED" in serializer._event_handlers + + +def test_frame_handler_tables_have_no_overlap(): + """A frame class registered in both sync and async tables would + produce ambiguous dispatch (sync wins by lookup order). Catch that + here so the registration stays unambiguous. + """ + serializer = AsteriskFrameSerializer() + overlap = set(serializer._sync_frame_handlers) & set( + serializer._async_frame_handlers + ) + assert overlap == set() From a4955303f8b5019348f62e7648f9dfb2b841c808 Mon Sep 17 00:00:00 2001 From: Salman Altuwayjiri Date: Fri, 12 Jun 2026 04:43:32 +0300 Subject: [PATCH 5/5] style: revert lazy log formatting to f-strings per review convention Keeps the dict dispatch, isinstance check, and static-string cleanups; only the variable-bearing log calls go back to f-strings, matching the maintainer's feedback on #3 --- src/pipecat_asterisk/serializer/serializer.py | 9 +++---- src/pipecat_asterisk/transport/transport.py | 26 ++++--------------- 2 files changed, 8 insertions(+), 27 deletions(-) diff --git a/src/pipecat_asterisk/serializer/serializer.py b/src/pipecat_asterisk/serializer/serializer.py index d6449cd..4d6c87b 100644 --- a/src/pipecat_asterisk/serializer/serializer.py +++ b/src/pipecat_asterisk/serializer/serializer.py @@ -120,14 +120,13 @@ def _handle_event(self, message: dict) -> Frame | None: message_type = message.get("event") if message_type is None: logger.warning( - "Received Asterisk WebSocket message without 'event' field: {}", - message, + f"Received Asterisk WebSocket message without 'event' field: {message}" ) return None handler = self._event_handlers.get(message_type) if handler is not None: return handler(message) - logger.info("Received unhandled Asterisk WebSocket event: {}", message) + logger.info(f"Received unhandled Asterisk WebSocket event: {message}") return None ### Asterisk Event handlers ### @@ -445,9 +444,7 @@ async def serialize(self, frame: Frame) -> str | bytes | None: 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, + f"Received unhandled frame type in Asterisk WebSocket serializer: {frame_type}. Frame: {frame}" ) return None diff --git a/src/pipecat_asterisk/transport/transport.py b/src/pipecat_asterisk/transport/transport.py index 05fe5b1..f511b29 100644 --- a/src/pipecat_asterisk/transport/transport.py +++ b/src/pipecat_asterisk/transport/transport.py @@ -57,21 +57,14 @@ async def _media_start_handler(self, frame: InputTransportMessageFrame): if ptime <= 0 or psize <= 0: logger.error( - "Invalid ptime ({}) or psize ({}) in MEDIA_START event {}. Cannot initialize flow controller.", - ptime, - psize, - frame.message, + f"Invalid ptime ({ptime}) or psize ({psize}) in MEDIA_START event {frame.message}. Cannot initialize flow controller." ) return self._flow_controller = FlowController(ptime, psize, self._client) logger.debug( - "Initialized flow controller with ptime={} ms, psize={} bytes. Remote buffer low water mark: {} bytes, high water mark: {} bytes.", - ptime, - psize, - self._flow_controller._remote_buffer_low_water, - self._flow_controller._remote_buffer_high_water, + f"Initialized flow controller with ptime={ptime} ms, psize={psize} bytes. Remote buffer low water mark: {self._flow_controller._remote_buffer_low_water} bytes, high water mark: {self._flow_controller._remote_buffer_high_water} bytes." ) # Send START_MEDIA_BUFFERING command to Asterisk WebSocket channel to enable audio buffering on the Asterisk side @@ -96,10 +89,7 @@ async def _media_start_handler(self, frame: InputTransportMessageFrame): ) except Exception as e: logger.error( - "{} exception sending START_MEDIA_BUFFERING: {} ({})", - self, - e.__class__.__name__, - e, + f"{self} exception sending START_MEDIA_BUFFERING: {e.__class__.__name__} ({e})" ) async def process_frame(self, frame: Frame, direction: FrameDirection): @@ -166,8 +156,7 @@ async def write_audio_frame(self, frame: OutputAudioRawFrame) -> bool: return True else: logger.error( - "Serialized audio frame is not bytes. Got {} instead. Cannot write audio frame.", - type(payload), + f"Serialized audio frame is not bytes. Got {type(payload)} instead. Cannot write audio frame." ) return False else: @@ -176,12 +165,7 @@ async def write_audio_frame(self, frame: OutputAudioRawFrame) -> bool: ) return False except Exception as e: - logger.error( - "{} exception sending data: {} ({})", - self, - e.__class__.__name__, - e, - ) + logger.error(f"{self} exception sending data: {e.__class__.__name__} ({e})") return False