From 7eabaaa0efb6f9bb6b09f15c3ded4edc6101003e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Thu, 29 Jan 2026 11:42:49 -0800 Subject: [PATCH] FrameProcessors: do not deepcopy fields when broadcasting frames --- src/pipecat/processors/frame_processor.py | 20 ++++++++++---------- tests/test_frame_processor.py | 21 +++++++++------------ 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/pipecat/processors/frame_processor.py b/src/pipecat/processors/frame_processor.py index 1ad57e0d6..740c19ae6 100644 --- a/src/pipecat/processors/frame_processor.py +++ b/src/pipecat/processors/frame_processor.py @@ -14,7 +14,6 @@ management, and frame flow control mechanisms. import asyncio import dataclasses import traceback -from copy import deepcopy from dataclasses import dataclass from enum import Enum from typing import ( @@ -775,20 +774,21 @@ class FrameProcessor(BaseObject): """Broadcasts a frame of the specified class upstream and downstream. This method creates two instances of the given frame class using the - provided keyword arguments and pushes them upstream and downstream. + provided keyword arguments (without deep-copying them) and pushes them + upstream and downstream. Args: frame_cls: The class of the frame to be broadcasted. **kwargs: Keyword arguments to be passed to the frame's constructor. """ - await self.push_frame(frame_cls(**deepcopy(kwargs))) - await self.push_frame(frame_cls(**deepcopy(kwargs)), FrameDirection.UPSTREAM) + await self.push_frame(frame_cls(**kwargs)) + await self.push_frame(frame_cls(**kwargs), FrameDirection.UPSTREAM) async def broadcast_frame_instance(self, frame: Frame): """Broadcasts a frame instance upstream and downstream. - This method creates two new frame instances copying all fields from the - original frame except `id` and `name`, which get fresh values. + This method creates two new frame instances shallow-copying all fields + from the original frame except `id` and `name`, which get fresh values. Args: frame: The frame instance to broadcast. @@ -806,13 +806,13 @@ class FrameProcessor(BaseObject): if not f.init and f.name not in ("id", "name") } - new_frame = frame_cls(**deepcopy(init_fields)) - for k, v in deepcopy(extra_fields).items(): + new_frame = frame_cls(**init_fields) + for k, v in extra_fields.items(): setattr(new_frame, k, v) await self.push_frame(new_frame) - new_frame = frame_cls(**deepcopy(init_fields)) - for k, v in deepcopy(extra_fields).items(): + new_frame = frame_cls(**init_fields) + for k, v in extra_fields.items(): setattr(new_frame, k, v) await self.push_frame(new_frame, FrameDirection.UPSTREAM) diff --git a/tests/test_frame_processor.py b/tests/test_frame_processor.py index 3f8c8ae70..2df35a10e 100644 --- a/tests/test_frame_processor.py +++ b/tests/test_frame_processor.py @@ -259,11 +259,11 @@ class TestFrameProcessor(unittest.IsolatedAsyncioTestCase): self.assertEqual(up_frame.value, 42) self.assertEqual(up_frame.items, ["a", "b"]) - # Verify the items lists are separate instances (not shared references) - self.assertIsNot(down_frame.items, up_frame.items) + # Verify the items lists are shared references (no deep copy) + self.assertIs(down_frame.items, up_frame.items) async def test_broadcast_frame_instance(self): - """Test that broadcast_frame_instance copies all fields except id and name.""" + """Test that broadcast_frame_instance shallow-copies all fields except id and name.""" downstream_frames: List[Frame] = [] upstream_frames: List[Frame] = [] original_frame: List[Frame] = [] @@ -298,7 +298,7 @@ class TestFrameProcessor(unittest.IsolatedAsyncioTestCase): pipeline = Pipeline([up_capture, broadcaster, down_capture]) - # Create a frame with mutable fields to test deep copying + # Create a frame with mutable fields to test shallow copying test_frame = BroadcastTestFrame(text="test", value=99, items=["x", "y", "z"]) frames_to_send = [test_frame] @@ -342,11 +342,8 @@ class TestFrameProcessor(unittest.IsolatedAsyncioTestCase): self.assertEqual(up_frame.pts, 12345) self.assertEqual(up_frame.metadata, {"key": "value", "nested": {"a": 1}}) - # Verify mutable fields are deep copied (not shared references) - self.assertIsNot(down_frame.items, orig.items) - self.assertIsNot(up_frame.items, orig.items) - self.assertIsNot(down_frame.items, up_frame.items) - self.assertIsNot(down_frame.metadata, orig.metadata) - self.assertIsNot(up_frame.metadata, orig.metadata) - self.assertIsNot(down_frame.metadata, up_frame.metadata) - self.assertIsNot(down_frame.metadata["nested"], up_frame.metadata["nested"]) + # Verify mutable fields are shallow-copied (shared references) + self.assertIs(down_frame.items, orig.items) + self.assertIs(up_frame.items, orig.items) + self.assertIs(down_frame.metadata, orig.metadata) + self.assertIs(up_frame.metadata, orig.metadata)