From ede702f34985cd45a3c7c877d320822efed85245 Mon Sep 17 00:00:00 2001 From: David Elner Date: Thu, 1 Oct 2026 10:06:38 -0400 Subject: [PATCH 01/18] Added: Common audio recording with progressive export --- py/noxfile.py | 9 + py/pyproject.toml | 7 + py/src/braintrust/audio/README.md | 20 +++ py/src/braintrust/audio/__init__.py | 6 + py/src/braintrust/audio/alignment.py | 109 ++++++++++++ py/src/braintrust/audio/attachments.py | 40 +++++ py/src/braintrust/audio/budget.py | 26 +++ py/src/braintrust/audio/options.py | 21 +++ py/src/braintrust/audio/recording.py | 173 +++++++++++++++++++ py/src/braintrust/audio/segments.py | 165 ++++++++++++++++++ py/src/braintrust/audio/test_attachments.py | 25 +++ py/src/braintrust/audio/test_dependencies.py | 36 ++++ py/src/braintrust/audio/test_recording.py | 84 +++++++++ py/src/braintrust/audio/test_segments.py | 167 ++++++++++++++++++ py/src/braintrust/audio/test_worker.py | 67 +++++++ py/src/braintrust/audio/worker.py | 53 ++++++ py/uv.lock | 49 +++++- 17 files changed, 1056 insertions(+), 1 deletion(-) create mode 100644 py/src/braintrust/audio/README.md create mode 100644 py/src/braintrust/audio/__init__.py create mode 100644 py/src/braintrust/audio/alignment.py create mode 100644 py/src/braintrust/audio/attachments.py create mode 100644 py/src/braintrust/audio/budget.py create mode 100644 py/src/braintrust/audio/options.py create mode 100644 py/src/braintrust/audio/recording.py create mode 100644 py/src/braintrust/audio/segments.py create mode 100644 py/src/braintrust/audio/test_attachments.py create mode 100644 py/src/braintrust/audio/test_dependencies.py create mode 100644 py/src/braintrust/audio/test_recording.py create mode 100644 py/src/braintrust/audio/test_segments.py create mode 100644 py/src/braintrust/audio/test_worker.py create mode 100644 py/src/braintrust/audio/worker.py diff --git a/py/noxfile.py b/py/noxfile.py index dc06bb9d1..a4a48b4f3 100644 --- a/py/noxfile.py +++ b/py/noxfile.py @@ -786,6 +786,13 @@ def test_pytest_plugin(session, version): _run_tests(session, f"{WRAPPER_DIR}/pytest_plugin/test_plugin.py") +@nox.session() +def test_audio(session): + _install_test_deps(session) + _install_group_locked(session, "test-audio") + _run_tests(session, "braintrust/audio") + + @nox.session() def test_core(session): _install_test_deps(session) @@ -956,6 +963,8 @@ def _run_core_tests(session): SRC_DIR, ignore_paths=[ WRAPPER_DIR, + "braintrust/audio/test_recording.py", + "braintrust/audio/test_segments.py", *_integration_subdirs_to_ignore(), CONTRIB_DIR, DEVSERVER_DIR, diff --git a/py/pyproject.toml b/py/pyproject.toml index fdfbe9af3..2279ce578 100644 --- a/py/pyproject.toml +++ b/py/pyproject.toml @@ -45,6 +45,7 @@ braintrust = "braintrust.wrappers.pytest_plugin.plugin" braintrust = "braintrust.integrations.harbor:HarborPlugin" [project.optional-dependencies] +audio = ["numpy>=1.26", "soundfile>=0.13.1"] cli = ["boto3", "python-dotenv", "uv", "starlette", "uvicorn"] # TODO: remove the doc extra in the next major version. doc = [] @@ -195,6 +196,12 @@ test-livekit-agents = [ "opentelemetry-sdk<1.39", ] +test-audio = [ + {include-group = "test"}, + "numpy>=1.26", + "soundfile>=0.13.1", +] + test-pipecat = [ {include-group = "test"}, # pipecat-ai 1.3.0 imports websockets but does not install it transitively. diff --git a/py/src/braintrust/audio/README.md b/py/src/braintrust/audio/README.md new file mode 100644 index 000000000..02d9fdf47 --- /dev/null +++ b/py/src/braintrust/audio/README.md @@ -0,0 +1,20 @@ +# Shared audio recording components + +Internal SDK components, alongside the shared logger and other `braintrust` packages. Framework integrations depend on this package; it imports no framework. No new public setup API is introduced. + +```text +Framework integration audio/ logger.py + native lifecycle hooks recording: PCM/encoding + transport/sample association → alignment: sample ranges + turn/provider metadata budget: retained bytes + worker: bounded jobs + span + recording descriptors ─────────────────────────→ Attachment / flush +``` + +`CallRecording.capture(channel, pcm, sample_rate, channels, ...)` accepts signed 16-bit little-endian PCM and local observation times. The current two-channel recorder assumes continuous input on channel 0 and intermittent output on channel 1, produces 24 kHz stereo, and returns sample ranges on that clock. Integrations must verify those assumptions and preserve sample provenance. It is not a universal clock-reconciliation or media-playback layer. + +NumPy and SoundFile are imported only during encoding. Both WAV and Ogg currently use them; `braintrust[audio]` installs the optional dependencies. SoundFile uses CFFI and native libsndfile; Ogg requires a libsndfile build with Opus support. No FFmpeg subprocess is used. Simple existing PCM-to-WAV attachment helpers elsewhere in the SDK remain dependency-free. + +Capture budgets and worker admission are shared across integrations in the process. Source PCM and encoded attachments have separate process-wide budgets. Encoded bytes remain budgeted for the lifetime of the exporter-owned attachment. Neither budget alone bounds total RSS or encoder scratch space. Frame hooks, capture opt-in, metadata filtering, span parentage, and framework shutdown remain the integration's responsibility. Attachment transport stays in the existing SDK logger. + +Run `nox -s test_audio` for encoding, resource, and dependency-boundary tests. Dependency-free tests also run in `test_core`; that session excludes the codec-dependent recording tests. diff --git a/py/src/braintrust/audio/__init__.py b/py/src/braintrust/audio/__init__.py new file mode 100644 index 000000000..16a6c1c0c --- /dev/null +++ b/py/src/braintrust/audio/__init__.py @@ -0,0 +1,6 @@ +"""Shared audio configuration; codecs load only when recording is enabled.""" + +from .options import RecordingOptions + + +__all__ = ["RecordingOptions"] diff --git a/py/src/braintrust/audio/alignment.py b/py/src/braintrust/audio/alignment.py new file mode 100644 index 000000000..adced8d81 --- /dev/null +++ b/py/src/braintrust/audio/alignment.py @@ -0,0 +1,109 @@ +"""Bounded sample ranges and recording selections shared by voice integrations.""" + +from collections import deque + + +def merge_ranges(ranges): + result = [] + for start, end in sorted(ranges): + if end <= start: + continue + if result and start <= result[-1][1]: + result[-1][1] = max(result[-1][1], end) + else: + result.append([start, end]) + return result + + +class Alignment: + def __init__(self, root, recording): + self.root = root + self.recording = recording + self.owners = {} + self.contexts = {} + self.count = 0 + self.omitted = 0 + + def add(self, owner, ranges, channel): + if self.recording.reason: + return + item = self.owners.setdefault(owner.span_id, (owner, channel, [])) + for interval in ranges: + if item[2] and item[2][-1][0] <= interval[0] <= item[2][-1][1]: + item[2][-1][1] = max(item[2][-1][1], interval[1]) + continue + if self.count >= 32000: + self.omitted += 1 + continue + item[2].append(list(interval)) + self.count += 1 + + def publish(self): + segments = getattr(self.recording, "completed", None) + if segments is None: + segments = ( + [{"id": "call", "start_ms": 0, "end_ms": max(self.recording.ends), "state": "ready"}] + if not self.recording.reason and self.recording.packets + else [] + ) + for owner, channel, ranges in self.owners.values(): + selections = [] + for segment in segments: + if segment["state"] != "ready": + continue + lower, upper = round(segment["start_ms"] * 24), round(segment["end_ms"] * 24) + for start, end in merge_ranges(ranges): + start, end = max(start, lower), min(end, upper) + if end > start: + selections.append( + { + "recording_span_id": self.root.span_id, + "recording_id": segment["id"], + "start_offset_ms": (start - lower) / 24, + "end_offset_ms": (end - lower) / 24, + "channel_index": channel, + } + ) + if selections: + owner.log( + metadata={ + "audio.selections": selections, + "audio.selection": selections[0] if len(selections) == 1 else None, + } + ) + self.root.log(metadata={"braintrust.alignment.ranges_omitted": self.omitted}) + + +class InputRanges: + def __init__(self): + self.runs = deque() + self.size = 0 + + def append(self, size, interval): + self.runs.append([size, interval, size, 0]) + self.size += size + + def trim(self, retained): + while self.size > retained and self.runs: + run = self.runs[0] + count = min(self.size - retained, run[0]) + run[0] -= count + run[3] += count + self.size -= count + if not run[0]: + self.runs.popleft() + + def drain(self): + ranges = [] + for count, interval, original, offset in self.runs: + if interval: + length = interval["end"] - interval["start"] + ranges.append( + [ + interval["start"] + round(offset * length / original), + interval["start"] + round((offset + count) * length / original), + ] + ) + self.runs.clear() + self.size = 0 + return merge_ranges(ranges) diff --git a/py/src/braintrust/audio/attachments.py b/py/src/braintrust/audio/attachments.py new file mode 100644 index 000000000..237c3a7a6 --- /dev/null +++ b/py/src/braintrust/audio/attachments.py @@ -0,0 +1,40 @@ +"""Bound encoded recordings while queued or retained by the exporter.""" + +import weakref + +from braintrust.logger import Attachment + +from .budget import ByteBudget +from .worker import RecordingBusy + + +encoded_budget = ByteBudget(32 * 1024 * 1024) + + +class RecordingAttachment(Attachment): + def __init__(self, *, data, filename, content_type): + self._retained_bytes = 0 + if not encoded_budget.reserve(len(data)): + raise RecordingBusy("encoded_audio_capacity") + self._retained_bytes = len(data) + super().__init__(data=data, filename=filename, content_type=content_type) + + def _init_uploader(self): + # The base uploader closes over its owner. Avoid keeping uploaded bytes + # and their budget reservation alive until cyclic GC happens to run. + return Attachment._init_uploader(weakref.proxy(self)) + + def __del__(self): + encoded_budget.release(getattr(self, "_retained_bytes", 0)) + self._retained_bytes = 0 + + +def prepare_recording(name, function, *args): + encoded = function(*args) + if encoded is not None: + encoded["attachment"] = RecordingAttachment( + data=encoded["data"], + filename=f"{name}.{encoded['extension']}", + content_type=encoded["mime_type"], + ) + return encoded diff --git a/py/src/braintrust/audio/budget.py b/py/src/braintrust/audio/budget.py new file mode 100644 index 000000000..4d4b1dccd --- /dev/null +++ b/py/src/braintrust/audio/budget.py @@ -0,0 +1,26 @@ +"""Conservative process-wide bound on retained recording source bytes.""" + +import threading + + +class ByteBudget: + def __init__(self, maximum=64 * 1024 * 1024): + self.maximum = maximum + self.used = 0 + self.lock = threading.Lock() + + def reserve(self, size): + with self.lock: + if self.used + size > self.maximum: + return False + self.used += size + return True + + def release(self, size): + with self.lock: + self.used -= size + if self.used < 0: + raise RuntimeError("Unbalanced audio budget release") + + +source_budget = ByteBudget() diff --git a/py/src/braintrust/audio/options.py b/py/src/braintrust/audio/options.py new file mode 100644 index 000000000..1bbffa672 --- /dev/null +++ b/py/src/braintrust/audio/options.py @@ -0,0 +1,21 @@ +"""Public recording configuration; no codec or framework dependencies.""" + +import math +from dataclasses import dataclass + + +@dataclass(frozen=True) +class RecordingOptions: + """Per-call limits. A rotation threshold is not a total recording limit.""" + + segment_duration_seconds: float = 60 + max_duration_seconds: float = 1800 + max_buffer_bytes: int = 32 * 1024 * 1024 + flush_fraction: float = 0.5 + + def __post_init__(self): + for value in (self.segment_duration_seconds, self.max_duration_seconds): + if not math.isfinite(value) or value <= 0: + raise ValueError("recording durations must be positive and finite") + if self.max_buffer_bytes <= 0 or not 0 < self.flush_fraction < 1: + raise ValueError("recording buffer must be positive and flush_fraction between zero and one") diff --git a/py/src/braintrust/audio/recording.py b/py/src/braintrust/audio/recording.py new file mode 100644 index 000000000..3facf4908 --- /dev/null +++ b/py/src/braintrust/audio/recording.py @@ -0,0 +1,173 @@ +"""Bounded PCM capture; mixing and encoding are called only from worker threads.""" + +import io +import math +import time +from dataclasses import dataclass + +from .budget import source_budget + + +@dataclass(frozen=True) +class Packet: + channel: int + start_ms: float + pcm: bytes + sample_rate: int + channels: int + + +def encode_audio(chunks, sample_rate, channels, audio_format="ogg"): + import numpy as np + + started = time.perf_counter() + cpu_started = time.thread_time() + pcm = b"".join(chunks) + samples = np.frombuffer(pcm, dtype=" 1000: + self.omit("frame_size_limit") + elif start + duration > self.max_duration_ms: + self.omit("duration_limit") + elif self.bytes + len(pcm) > self.max_bytes: + self.omit("capture_byte_limit") + elif len(self.packets) >= self.max_packets: + self.omit("packet_limit") + elif not source_budget.reserve(len(pcm)): + self.omit("process_capture_byte_limit") + else: + self.packets.append(Packet(channel, start, pcm, sample_rate, channels)) + self.bytes += len(pcm) + self.ends[channel] = start + duration + self.started[channel] = True + # Coordinates match encode() placement, including resampling rounding. + return { + "start": round(start * 24), + "end": round(start * 24) + round(duration * 24), + } + + def encode(self, audio_format="ogg"): + if self.reason or not self.packets: + return None + + import numpy as np + + started = time.perf_counter() + cpu_started = time.thread_time() + sample_rate = 24000 + sample_count = math.ceil(max(self.ends) * sample_rate / 1000) + samples = np.zeros((sample_count, 2), dtype=np.int16) + for packet in self.packets: + source = np.frombuffer(packet.pcm, dtype=" self.max_bytes: + self.omit("capture_byte_limit") + return None + interval = super().capture( + channel, pcm, sample_rate, channels, observed_ns=observed_ns, observed_unix_ms=observed_unix_ms + ) + if interval is None: + return None + # Never rewrite audio that was already sealed/exported. + if interval["start"] < round(self.start_ms * 24): + self.omit("capture_clock_discontinuity") + packet = self.packets.pop() + self.bytes -= len(packet.pcm) + source_budget.release(len(packet.pcm)) + return None + latest = self.packets[-1].start_ms + watermark = min(self.ends[0], latest) if self.started[0] else latest + watermark = max(self.start_ms, watermark - 1000) + cut = self.start_ms + self.options.segment_duration_seconds * 1000 + if watermark >= cut: + self._rotate(cut) + elif self.bytes >= self.max_bytes * self.options.flush_fraction and watermark > self.start_ms: + self._rotate(watermark) + return interval + + def _rotate(self, cut): + if not self.packets: + return + self.pending = {task for task in self.pending if not task.done()} + if len(self.pending) >= 2: + self.omit("recording_export_capacity") + return + segment = CallRecording() + segment.start_ms = self.start_ms + segment.segment_id = f"call-{self.sequence:04d}" + segment.origin_unix_ms = self.origin_unix_ms + later = [] + for packet in self.packets: + size = len(packet.pcm) // (2 * packet.channels) + before = min(size, max(0, round((cut - packet.start_ms) * packet.sample_rate / 1000))) + if before: + data = packet.pcm if before == size else packet.pcm[: before * 2 * packet.channels] + segment.packets.append( + Packet(packet.channel, packet.start_ms - self.start_ms, data, packet.sample_rate, packet.channels) + ) + segment.bytes += len(data) + if before < size: + data = packet.pcm if not before else packet.pcm[before * 2 * packet.channels :] + later.append( + Packet( + packet.channel, + packet.start_ms + before / packet.sample_rate * 1000, + data, + packet.sample_rate, + packet.channels, + ) + ) + segment.ends = [cut - self.start_ms, cut - self.start_ms] + segment.seal() + self.packets = later + self.bytes -= segment.bytes + self.start_ms = cut + self.sequence += 1 + self.inflight.append(segment) + task = asyncio.create_task(self._export(segment)) + self.pending.add(task) + + def completed(task): + self.pending.discard(task) + if segment in self.inflight: + segment.clear() + self.inflight.remove(segment) + + task.add_done_callback(completed) + + async def _export(self, segment): + descriptor = { + "id": segment.segment_id, + "start_ms": segment.start_ms, + "end_ms": segment.start_ms + max(segment.ends), + "state": "pending", + } + + def encode(): + try: + return prepare_recording(segment.segment_id, segment.encode, self.audio_format) + finally: + # Only the encoder may release a segment handed to its thread. + segment.clear() + + try: + encoded = await encode_in_worker(encode) + descriptor["state"] = "ready" + self.completed.append(descriptor) + if self.on_segment: + await self.on_segment(segment, encoded) + except Exception as error: # noqa: BLE001 - recording cannot stop speech + descriptor.update(state="omitted", reason=type(error).__name__) + if descriptor not in self.completed: + self.completed.append(descriptor) + self.omit("segment_export_failed") + finally: + # encode_in_worker drains native work before propagating cancellation. + segment.clear() + self.inflight.remove(segment) + + async def finish(self): + if self._finish_task is None: + self._finish_task = asyncio.create_task(self._finish()) + await asyncio.shield(self._finish_task) + + async def _finish(self): + self.seal() + if self.pending: + await asyncio.gather(*tuple(self.pending)) + if self.packets: + self._rotate(max(self.ends)) + if self.pending: + await asyncio.gather(*tuple(self.pending)) diff --git a/py/src/braintrust/audio/test_attachments.py b/py/src/braintrust/audio/test_attachments.py new file mode 100644 index 000000000..6bd1eee73 --- /dev/null +++ b/py/src/braintrust/audio/test_attachments.py @@ -0,0 +1,25 @@ +"""Encoded audio stays budgeted for as long as the exporter retains it.""" + +import gc + +import pytest + +from .attachments import RecordingAttachment, encoded_budget, prepare_recording +from .worker import RecordingBusy + + +def test_encoded_budget_follows_attachment_lifetime(monkeypatch): + previous = encoded_budget.used + monkeypatch.setattr(encoded_budget, "maximum", previous + 4) + attachment = RecordingAttachment(data=b"1234", filename="clip.wav", content_type="audio/wav") + with pytest.raises(RecordingBusy): + RecordingAttachment(data=b"x", filename="clip.wav", content_type="audio/wav") + assert encoded_budget.used == previous + 4 + del attachment + # Completed uploads must not wait for cyclic GC to return capacity. + assert encoded_budget.used == previous + gc.collect() + + +def test_prepare_recording_preserves_disabled_result(): + assert prepare_recording("call", lambda: None) is None diff --git a/py/src/braintrust/audio/test_dependencies.py b/py/src/braintrust/audio/test_dependencies.py new file mode 100644 index 000000000..ab5618bc2 --- /dev/null +++ b/py/src/braintrust/audio/test_dependencies.py @@ -0,0 +1,36 @@ +"""The shared capture path must work without optional codecs or frameworks.""" + +import subprocess +import sys + + +def test_capture_and_disabled_encode_without_optional_packages(): + code = """ +import importlib.abc +import sys +import braintrust + +class BlockOptional(importlib.abc.MetaPathFinder): + def find_spec(self, fullname, path=None, target=None): + if fullname.split(".")[0] in {"pipecat", "numpy", "soundfile"}: + raise AssertionError("Unexpected optional import: " + fullname) + +sys.meta_path.insert(0, BlockOptional()) +from braintrust.audio.recording import CallRecording +from braintrust.audio.alignment import InputRanges +from braintrust.audio import worker, RecordingOptions +from braintrust.audio.segments import SegmentedRecording +import asyncio +asyncio.run(SegmentedRecording(enabled=False, options=RecordingOptions()).finish()) +assert worker._executor is None +recording = CallRecording() +interval = recording.capture(0, b"\\0\\0" * 480, 24000, 1, observed_ns=0) +ranges = InputRanges() +ranges.append(960, interval) +assert ranges.drain() == [[0, 480]] +recording.clear() +assert recording.encode() is None +assert CallRecording(enabled=False).encode() is None +assert worker._executor is None +""" + subprocess.run([sys.executable, "-c", code], check=True, capture_output=True, text=True) diff --git a/py/src/braintrust/audio/test_recording.py b/py/src/braintrust/audio/test_recording.py new file mode 100644 index 000000000..7d2224843 --- /dev/null +++ b/py/src/braintrust/audio/test_recording.py @@ -0,0 +1,84 @@ +import io +import unittest + +import numpy as np +import soundfile as sf # pylint: disable=import-error + +from .recording import CallRecording, encode_audio + + +def frame(samples, rate=16000): + return dict( + pcm=np.asarray(samples, dtype=" Date: Thu, 1 Oct 2026 10:08:05 -0400 Subject: [PATCH 02/18] Added: Pipecat voice tracing and opt-in recording --- py/noxfile.py | 2 +- py/pyproject.toml | 1 + .../integrations/pipecat/__init__.py | 12 +- .../integrations/pipecat/patchers.py | 12 +- .../integrations/pipecat/tracing.py | 56 +- .../integrations/pipecat/voice/README.md | 31 + .../integrations/pipecat/voice/__init__.py | 6 + .../integrations/pipecat/voice/alignment.py | 143 ++++ .../integrations/pipecat/voice/discovery.py | 30 + .../pipecat/voice/fixtures/README.md | 5 + .../voice/fixtures/realtime-responses.json | 165 ++++ .../integrations/pipecat/voice/hooks.py | 119 +++ .../pipecat/voice/instrumentation.py | 777 ++++++++++++++++++ .../integrations/pipecat/voice/realtime.py | 264 ++++++ .../integrations/pipecat/voice/recording.py | 22 + .../pipecat/voice/test_alignment.py | 148 ++++ .../integrations/pipecat/voice/test_hooks.py | 36 + .../pipecat/voice/test_instrumentation.py | 249 ++++++ .../pipecat/voice/test_realtime.py | 74 ++ .../pipecat/voice/test_recorded_realtime.py | 61 ++ .../pipecat/voice/test_recording.py | 34 + .../integrations/pipecat/voice/test_setup.py | 81 ++ .../integrations/pipecat/voice/test_turns.py | 171 ++++ .../pipecat/voice/test_user_capture.py | 150 ++++ .../integrations/pipecat/voice/turns.py | 134 +++ .../pipecat/voice/user_capture.py | 322 ++++++++ py/uv.lock | 2 + 27 files changed, 3103 insertions(+), 4 deletions(-) create mode 100644 py/src/braintrust/integrations/pipecat/voice/README.md create mode 100644 py/src/braintrust/integrations/pipecat/voice/__init__.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/alignment.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/discovery.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/fixtures/README.md create mode 100644 py/src/braintrust/integrations/pipecat/voice/fixtures/realtime-responses.json create mode 100644 py/src/braintrust/integrations/pipecat/voice/hooks.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/instrumentation.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/realtime.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/recording.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/test_alignment.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/test_hooks.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/test_realtime.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/test_recorded_realtime.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/test_recording.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/test_setup.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/test_turns.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/test_user_capture.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/turns.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/user_capture.py diff --git a/py/noxfile.py b/py/noxfile.py index a4a48b4f3..597e56559 100644 --- a/py/noxfile.py +++ b/py/noxfile.py @@ -467,7 +467,7 @@ def test_pipecat(session, version): # loaded from Nox's virtualenv when it is beneath the current directory. _run_tests( session, - f"{INTEGRATION_DIR}/pipecat/test_pipecat.py", + f"{INTEGRATION_DIR}/pipecat" if version == LATEST else f"{INTEGRATION_DIR}/pipecat/test_pipecat.py", version=version, run_from_temp_dir=True, ) diff --git a/py/pyproject.toml b/py/pyproject.toml index 2279ce578..e67947b82 100644 --- a/py/pyproject.toml +++ b/py/pyproject.toml @@ -204,6 +204,7 @@ test-audio = [ test-pipecat = [ {include-group = "test"}, + "soundfile>=0.13.1", # pipecat-ai 1.3.0 imports websockets but does not install it transitively. "websockets==15.0.1", ] diff --git a/py/src/braintrust/integrations/pipecat/__init__.py b/py/src/braintrust/integrations/pipecat/__init__.py index 4fed34203..81b7ca25f 100644 --- a/py/src/braintrust/integrations/pipecat/__init__.py +++ b/py/src/braintrust/integrations/pipecat/__init__.py @@ -1,5 +1,6 @@ """Braintrust integration for Pipecat AI.""" +from braintrust.audio import RecordingOptions from braintrust.logger import NOOP_SPAN, current_span, init_logger from .integration import PipecatIntegration @@ -24,6 +25,8 @@ def setup_pipecat( capture_user_audio_attachments: bool | None = None, capture_agent_audio_attachments: bool | None = None, trace_turns: bool = True, + audio_format: str = "ogg", + recording_options: RecordingOptions | None = None, ) -> bool: """Set up Braintrust tracing for Pipecat ``PipelineWorker`` instances. @@ -35,13 +38,20 @@ def setup_pipecat( When arguments are omitted, the generic environment variables ``BRAINTRUST_CAPTURE_USER_AUDIO_ATTACHMENTS`` and ``BRAINTRUST_CAPTURE_AGENT_AUDIO_ATTACHMENTS`` apply. Explicit arguments - take precedence. + take precedence. Supported voice pipelines also emit native aggregator turns, + recording descriptors, and sample selections. ``audio_format`` selects Ogg/Opus + or PCM WAV for these recordings. ``recording_options`` configures progressive + segment duration, maximum recording duration, and retained PCM limits; install ``braintrust[audio]`` to encode them. """ + if audio_format not in {"ogg", "wav"}: + raise ValueError("audio_format must be ogg or wav") set_default_observer_options( capture_audio_attachments=capture_audio_attachments, capture_user_audio_attachments=capture_user_audio_attachments, capture_agent_audio_attachments=capture_agent_audio_attachments, trace_turns=trace_turns, + audio_format=audio_format, + recording_options=recording_options, ) span = current_span() diff --git a/py/src/braintrust/integrations/pipecat/patchers.py b/py/src/braintrust/integrations/pipecat/patchers.py index 929cae964..85e08f1d0 100644 --- a/py/src/braintrust/integrations/pipecat/patchers.py +++ b/py/src/braintrust/integrations/pipecat/patchers.py @@ -1,5 +1,6 @@ """Pipecat integration patchers.""" +import logging from typing import Any from braintrust.integrations.base import FunctionWrapperPatcher @@ -12,6 +13,7 @@ "capture_user_audio_attachments": None, "capture_agent_audio_attachments": None, "trace_turns": True, + "audio_format": "ogg", } @@ -35,7 +37,15 @@ def traced_pipeline_worker_init(wrapped: Any, _instance: Any, args: tuple[Any, . """Inject the Braintrust Pipecat observer into PipelineWorker construction.""" kwargs = dict(kwargs) kwargs["observers"] = _with_braintrust_observer(kwargs.get("observers")) - return wrapped(*args, **kwargs) + result = wrapped(*args, **kwargs) + pipeline = kwargs.get("pipeline", args[0] if args else None) + for observer in kwargs["observers"]: + if isinstance(observer, BraintrustPipecatObserver): + try: + observer._bind_pipeline(pipeline) + except Exception: # noqa: BLE001 - optional discovery cannot break worker construction + logging.getLogger(__name__).warning("Pipecat voice discovery unavailable; using frame tracing") + return result class PipelineWorkerInitPatcher(FunctionWrapperPatcher): diff --git a/py/src/braintrust/integrations/pipecat/tracing.py b/py/src/braintrust/integrations/pipecat/tracing.py index 554c70048..7f98018ab 100644 --- a/py/src/braintrust/integrations/pipecat/tracing.py +++ b/py/src/braintrust/integrations/pipecat/tracing.py @@ -1,8 +1,10 @@ """Observer-based tracing for Pipecat pipelines.""" import json +import logging from typing import Any +from braintrust.audio import RecordingOptions from braintrust.integrations.utils import ( _is_not_given, _normalize_chat_messages, @@ -78,6 +80,8 @@ def __init__( capture_user_audio_attachments: bool | None = None, capture_agent_audio_attachments: bool | None = None, trace_turns: bool = True, + audio_format: str = "ogg", + recording_options: RecordingOptions | None = None, **kwargs: Any, ) -> None: self._uses_native_frame_deduplication = _USES_NATIVE_FRAME_DEDUPLICATION @@ -93,6 +97,11 @@ def __init__( capture_agent_audio_attachments=capture_agent_audio_attachments, ) self.capture_audio_attachments = self.capture_user_audio_attachments or self.capture_agent_audio_attachments + if audio_format not in {"ogg", "wav"}: + raise ValueError("audio_format must be ogg or wav") + self.audio_format = audio_format + self.recording_options = recording_options + self._voice = None self.trace_turns = trace_turns self._parent = _current_parent_export() self._pipeline_span: Any | None = None @@ -117,10 +126,44 @@ def __init__( self._user_audio: bytearray | None = None self._user_audio_metadata: dict[str, Any] = {} + def _bind_pipeline(self, pipeline: Any) -> None: + if self._voice is not None or not self.trace_turns: + return + import importlib.metadata + + if importlib.metadata.version("pipecat-ai") != "1.12.0": + return + from .voice.discovery import discover + from .voice.instrumentation import NativeObserver + + configuration = discover(pipeline) + if configuration is None: + return + root = start_span(name="pipecat.pipeline", type="task", set_current=False, parent=self._parent) + voice = NativeObserver( + root, + root=root, + capture_user_audio=self.capture_user_audio_attachments, + capture_agent_audio=self.capture_agent_audio_attachments, + audio_format=self.audio_format, + recording_options=self.recording_options, + ) + try: + voice.bind(**configuration) + except Exception: # noqa: BLE001 - unsupported hooks preserve the existing observer + voice.hooks.close() + root.end() + logging.getLogger(__name__).warning("Pipecat voice hooks unavailable; using frame tracing") + return + self._voice = voice + async def on_pipeline_started(self) -> None: - self._ensure_pipeline_span() + if self._voice is None: + self._ensure_pipeline_span() async def on_process_frame(self, data: Any) -> None: + if self._voice is not None: + return frame = getattr(data, "frame", None) processor = getattr(data, "processor", None) is_terminal_at_sink = type(frame).__name__ in _TERMINAL_FRAME_TYPES and _is_pipeline_sink_processor(processor) @@ -129,9 +172,20 @@ async def on_process_frame(self, data: Any) -> None: await self._handle_frame(frame, processor=processor) async def on_push_frame(self, data: Any) -> None: + if self._voice is not None: + try: + await self._voice.on_push_frame(data) + except Exception: # noqa: BLE001 - tracing cannot stop frame delivery + logging.getLogger(__name__).warning("Pipecat frame observation failed") + return await self._handle_frame(getattr(data, "frame", None), processor=getattr(data, "source", None)) async def cleanup(self) -> None: + if self._voice is not None: + try: + await self._voice.finish() + except Exception: # noqa: BLE001 - export failure cannot stop pipeline cleanup + logging.getLogger(__name__).warning("Pipecat voice finalization failed") self._close_all_open_spans() await super().cleanup() diff --git a/py/src/braintrust/integrations/pipecat/voice/README.md b/py/src/braintrust/integrations/pipecat/voice/README.md new file mode 100644 index 000000000..b06107d97 --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/voice/README.md @@ -0,0 +1,31 @@ +# Pipecat voice instrumentation + +The existing public setup installs one observer and discovers a supported voice pipeline. Shared recording, sample ranges, worker admission, and byte budgets live in [`braintrust.audio`](../../../audio/README.md). + +```python +from braintrust import init_logger +from braintrust.integrations.pipecat import setup_pipecat + +init_logger(project="voice") +setup_pipecat(capture_audio_attachments=True, audio_format="ogg") +# Construct and run the existing PipelineWorker normally. +# Its observer cleanup finalizes recordings before flushing. +``` + +Install `braintrust[audio]` for Ogg/Opus or WAV encoding. Audio defaults off. Existing user/agent capture arguments and environment variables still apply independently. With audio disabled, no PCM retention, sample-tracking hooks, encoder jobs, or attachments are created. Transcripts and native metadata remain observable. + +| Contract | Behavior | +|---|---| +| Native voice path | Pipecat 1.12.0, one input/output transport, universal user/assistant aggregators, segmented STT or OpenAI Realtime. Other pipeline shapes/versions retain existing frame tracing. | +| Public interfaces | `setup_pipecat`, automatic integration setup, `wrap_pipeline_worker`, and an explicitly supplied `BraintrustPipecatObserver` share the same injection path. `trace_turns=False` retains the existing observer behavior. | +| Trace | Native aggregator turns, actual STT operations, model responses, tool executions, and generated-audio lifecycles. Tool-only realtime responses can be pipeline children. | +| Audio | Combined stereo call; segmented-STT caller clips on user turns; generated clips on TTS spans. Selections reference successfully captured samples. | +| Input alignment | Segmented STT input provenance or successful OpenAI input writes plus server-VAD item offsets. Clears/reconnects invalidate unsupported mappings. | +| Output alignment | Equal-rate mono PCM without a mixer. Other paths retain recordings without asserting unsupported selections. | +| Memory | Configurable 32 MiB retained call PCM by default; separate 8 MiB caller/generated source limits; 64 MiB shared source budget; 32 MiB encoded-attachment budget. These are retained-data bounds, not total RSS bounds. | +| Recording limits | Rotate call segments every 60 seconds or at half the call buffer limit; stop capture at 30 minutes by default. Configure these with `RecordingOptions`. Completed segments and valid prefixes survive limits. | +| Admission | One encoder running, one queued; bounded packet/clip metadata. Overload produces an omission reason. | +| Cleanup | Seal capture, restore installed hooks, end operation spans, encode off-loop, update attachments, then flush. Repeated finalization shares one task; cancelling a waiter does not cancel its work. | +| Privacy | Standard span customizers apply to native metadata, messages, tool arguments/results, and attachment updates. Audio bytes are excluded from event metadata. | + +Run `nox -s test_audio 'test_pipecat(latest)' 'test_pipecat(1.3.0)'`. Recorded OpenAI response objects cover tool requests and continuations; transport/queue tests cover interruption, padding, failed writes, and recording opt-out. The `voice` modules are integration internals, not another customer setup API. diff --git a/py/src/braintrust/integrations/pipecat/voice/__init__.py b/py/src/braintrust/integrations/pipecat/voice/__init__.py new file mode 100644 index 000000000..6f1173e50 --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/voice/__init__.py @@ -0,0 +1,6 @@ +"""Internal native voice tracing installed by the Pipecat integration.""" + +from .instrumentation import NativeObserver as VoiceObserver + + +__all__ = ["VoiceObserver"] diff --git a/py/src/braintrust/integrations/pipecat/voice/alignment.py b/py/src/braintrust/integrations/pipecat/voice/alignment.py new file mode 100644 index 000000000..2c12313ae --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/voice/alignment.py @@ -0,0 +1,143 @@ +"""Pipecat output queue provenance hooks.""" + +from collections import deque +from contextvars import ContextVar + + +def instrument_output(output, alignment, frame_context=None, hooks=None): + """Track equal-rate mono PCM through native chunking without copying audio. + + Unsupported resampling/mixing deliberately has no asserted association. + """ + from .hooks import Hooks + + hooks = hooks or Hooks() + original_start = hooks.original(output, "start") + original_write = hooks.original(output, "write_audio_frame") + frame_parts = {} + active = ContextVar("pipecat_output_context", default=None) + + async def start(frame): + result = await original_start(frame) + for sender in output._media_senders.values(): + install_sender(sender) + return result + + def install_sender(sender): + runs, pending = deque(), deque() + supported = [True] + handle_audio = hooks.original(sender, "handle_audio_frame") + handle_stop = hooks.original(sender, "handle_tts_stopped") + buffer_audio = hooks.original(sender, "_buffer_audio") + take_chunk = hooks.original(sender, "_take_audio_chunk") + clear_buffer = hooks.original(sender, "_clear_audio_buffer") + create_audio_task = hooks.original(sender, "_create_audio_task") + + async def handle(frame): + context = frame_context(frame) if frame_context else getattr(frame, "context_id", None) + if frame.sample_rate != sender._sample_rate or frame.num_channels != 1 or sender._mixer: + context = None + supported[0] = False + token = active.set(context) + try: + return await handle_audio(frame) + finally: + active.reset(token) + + async def stop(frame): + context = frame_context(frame) if frame_context else frame.context_id + token = active.set(context if supported[0] else None) + try: + return await handle_stop(frame) + finally: + active.reset(token) + + def buffer(audio, *, uninterruptible): + result = buffer_audio(audio, uninterruptible=uninterruptible) + if audio: + runs.append([len(audio), active.get()]) + return result + + def take(): + audio, uninterruptible = take_chunk() + left, offset, parts = len(audio), 0, [] + while left and runs: + count, context = runs[0] + amount = min(count, left) + parts.append((offset, amount, context)) + runs[0][0] -= amount + if not runs[0][0]: + runs.popleft() + left -= amount + offset += amount + pending.append(parts) + return audio, uninterruptible + + bound_queue = [None] + + def bind_queue(): + if bound_queue[0] is sender._audio_queue: + return + bound_queue[0] = sender._audio_queue + queue_put = hooks.original(sender._audio_queue, "put") + + async def put(frame): + if hasattr(frame, "audio") and pending: + parts = pending.popleft() + if len(frame_parts) < 16000: + frame_parts[frame.id] = parts + else: + alignment.omitted += len(parts) + return await queue_put(frame) + + hooks.set(sender._audio_queue, "put", put) + + def create(): + result = create_audio_task() + bind_queue() + return result + + def clear(): + runs.clear() + pending.clear() + supported[0] = True + return clear_buffer() + + hooks.set(sender, "handle_audio_frame", handle) + hooks.set(sender, "handle_tts_stopped", stop) + hooks.set(sender, "_buffer_audio", buffer) + hooks.set(sender, "_take_audio_chunk", take) + hooks.set(sender, "_clear_audio_buffer", clear) + hooks.set(sender, "_create_audio_task", create) + bind_queue() + + async def write(frame): + import time + + observed = time.monotonic_ns() + parts = frame_parts.pop(frame.id, []) + result = await original_write(frame) + if result: + try: + interval = alignment.recording.capture( + 1, frame.audio, frame.sample_rate, frame.num_channels, observed_ns=observed + ) + except Exception: # noqa: BLE001 - instrumentation must preserve transport success + alignment.recording.omit("capture_error") + interval = None + if interval: + for offset, size, context in parts: + # This hook supports only the demo's mono, 24 kHz output. + if frame.sample_rate == 24000 and frame.num_channels == 1: + ranges = [ + [ + interval["start"] + offset // 2, + interval["start"] + (offset + size) // 2, + ] + ] + for owner in alignment.contexts.get(context, []): + alignment.add(owner, ranges, 1) + return result + + hooks.set(output, "start", start) + hooks.set(output, "write_audio_frame", write) diff --git a/py/src/braintrust/integrations/pipecat/voice/discovery.py b/py/src/braintrust/integrations/pipecat/voice/discovery.py new file mode 100644 index 000000000..1ebe0eb96 --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/voice/discovery.py @@ -0,0 +1,30 @@ +"""Discover a single native voice path without integration-specific setup.""" + +from types import SimpleNamespace + + +def _is(processor, name): + return any(cls.__name__ == name and cls.__module__.startswith("pipecat.") for cls in type(processor).__mro__) + + +def discover(pipeline): + # Parallel/nested paths need explicit ownership; don't guess between them. + processors = list(getattr(pipeline, "processors", ())) + if any(getattr(processor, "processors", ()) for processor in processors): + return None + names = ("BaseInputTransport", "BaseOutputTransport", "LLMUserAggregator", "LLMAssistantAggregator") + matches = [[processor for processor in processors if _is(processor, name)] for name in names] + if any(len(match) != 1 for match in matches): + return None + source, destination, user, assistant = (match[0] for match in matches) + stts = [processor for processor in processors if _is(processor, "SegmentedSTTService")] + realtime = [processor for processor in processors if _is(processor, "OpenAIRealtimeLLMService")] + if len(stts) + len(realtime) != 1: + return None + return dict( + transport=SimpleNamespace(input=lambda: source, output=lambda: destination), + user_aggregator=user, + assistant_aggregator=assistant, + stt=stts[0] if stts else None, + realtime_service=realtime[0] if realtime else None, + ) diff --git a/py/src/braintrust/integrations/pipecat/voice/fixtures/README.md b/py/src/braintrust/integrations/pipecat/voice/fixtures/README.md new file mode 100644 index 000000000..759efe020 --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/voice/fixtures/README.md @@ -0,0 +1,5 @@ +# Recorded realtime responses + +`realtime-responses.json` contains the native OpenAI response objects observed during the synthetic DEMO-1042 call on September 30, 2026, using Pipecat 1.12.0 and `gpt-realtime`. It includes the greeting, tool request, and spoken continuation. These are exported provider response objects, not complete WebSocket traffic. No credentials or audio bytes are included. + +Trace row: `bbffcf29-8862-4ee1-bd78-8365c1a20382`, project `voice-sdk-instrumentation-pipecat`. diff --git a/py/src/braintrust/integrations/pipecat/voice/fixtures/realtime-responses.json b/py/src/braintrust/integrations/pipecat/voice/fixtures/realtime-responses.json new file mode 100644 index 000000000..d36a47e0f --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/voice/fixtures/realtime-responses.json @@ -0,0 +1,165 @@ +[ + { + "id": "resp_ETxuDuzc0wFmc6WwqlJby", + "max_output_tokens": "inf", + "object": "realtime.response", + "output": [ + { + "arguments": null, + "call_id": null, + "content": [ + { + "detail": null, + "image_url": null, + "text": null, + "transcript": "Your order DEMO1042 is in transit and will arrive on Friday.", + "type": "output_audio" + } + ], + "id": "item_ETxuDdyYHUQT1BktvY6lP", + "name": null, + "object": null, + "output": null, + "role": "assistant", + "status": "completed", + "type": "message" + } + ], + "output_audio_format": null, + "output_modalities": [ + "audio" + ], + "status": "completed", + "status_details": null, + "temperature": null, + "usage": { + "input_token_details": { + "audio_tokens": 41, + "cached_tokens": 0, + "cached_tokens_details": { + "audio_tokens": 0, + "text_tokens": 0 + }, + "image_tokens": 0, + "text_tokens": 172 + }, + "input_tokens": 213, + "output_token_details": { + "audio_tokens": 90, + "cached_tokens": 0, + "cached_tokens_details": null, + "image_tokens": 0, + "text_tokens": 27 + }, + "output_tokens": 117, + "total_tokens": 330 + }, + "voice": null + }, + { + "id": "resp_ETxuCbwvBj1gmXxeY2P3m", + "max_output_tokens": "inf", + "object": "realtime.response", + "output": [ + { + "arguments": "{ \n \"order_id\": \"DEMO1042\" \n} \n", + "call_id": "call_Y73czteQZUvPMqpZ", + "content": null, + "id": "item_ETxuCYmv77MS3KVEnlFJZ", + "name": "lookup_order", + "object": null, + "output": null, + "role": null, + "status": "completed", + "type": "function_call" + } + ], + "output_audio_format": null, + "output_modalities": [ + "audio" + ], + "status": "completed", + "status_details": null, + "temperature": null, + "usage": { + "input_token_details": { + "audio_tokens": 123, + "cached_tokens": 128, + "cached_tokens_details": { + "audio_tokens": 64, + "text_tokens": 64 + }, + "image_tokens": 0, + "text_tokens": 126 + }, + "input_tokens": 249, + "output_token_details": { + "audio_tokens": 0, + "cached_tokens": 0, + "cached_tokens_details": null, + "image_tokens": 0, + "text_tokens": 26 + }, + "output_tokens": 26, + "total_tokens": 275 + }, + "voice": null + }, + { + "id": "resp_ETxu27KFneNxBeTo98mr5", + "max_output_tokens": "inf", + "object": "realtime.response", + "output": [ + { + "arguments": null, + "call_id": null, + "content": [ + { + "detail": null, + "image_url": null, + "text": null, + "transcript": "Hi there! Could you please share your order number so I can look it up for you?", + "type": "output_audio" + } + ], + "id": "item_ETxu2jkKOXn66fRwNQzsr", + "name": null, + "object": null, + "output": null, + "role": "assistant", + "status": "completed", + "type": "message" + } + ], + "output_audio_format": null, + "output_modalities": [ + "audio" + ], + "status": "completed", + "status_details": null, + "temperature": null, + "usage": { + "input_token_details": { + "audio_tokens": 0, + "cached_tokens": 0, + "cached_tokens_details": { + "audio_tokens": 0, + "text_tokens": 0 + }, + "image_tokens": 0, + "text_tokens": 85 + }, + "input_tokens": 85, + "output_token_details": { + "audio_tokens": 82, + "cached_tokens": 0, + "cached_tokens_details": null, + "image_tokens": 0, + "text_tokens": 31 + }, + "output_tokens": 113, + "total_tokens": 198 + }, + "voice": null + } +] diff --git a/py/src/braintrust/integrations/pipecat/voice/hooks.py b/py/src/braintrust/integrations/pipecat/voice/hooks.py new file mode 100644 index 000000000..4a650fccd --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/voice/hooks.py @@ -0,0 +1,119 @@ +"""Per-pipeline reversible method and callback installation.""" + +import inspect +import logging +from contextvars import ContextVar +from functools import wraps + + +class Hooks: + def __init__(self): + self.methods = [] + self.handlers = [] + self.invocations = {} + + def original(self, target, name): + original = getattr(target, name) + if inspect.isasyncgenfunction(original): + return original + state = self.invocations.setdefault((id(target), name), ContextVar(name, default=None)) + + def invoke(*args, **kwargs): + current = state.get() + if current is not None: + current["called"] = True + try: + result = original(*args, **kwargs) + except Exception as error: + if current is not None: + current["error"] = error + raise + if current is not None: + current["result"] = result + return result + + @wraps(original) + async def invoke_async(*args, **kwargs): + current = state.get() + if current is not None: + current["called"] = True + try: + result = await original(*args, **kwargs) + except Exception as error: + if current is not None: + current["error"] = error + raise + if current is not None: + current["result"] = result + return result + + return invoke_async if inspect.iscoroutinefunction(original) else invoke + + def guard(self, target, name, replacement): + state = self.invocations.get((id(target), name)) + if state is None or inspect.isasyncgenfunction(replacement): + return replacement + original = getattr(target, name) + warned = False + + def failure(current): + nonlocal warned + if "error" in current: + raise current["error"] + if not warned: + logging.getLogger(__name__).warning("Pipecat observation failed in %s", name) + warned = True + + @wraps(replacement) + async def guarded_async(*args, **kwargs): + current = {} + token = state.set(current) + try: + return await replacement(*args, **kwargs) + except Exception: + failure(current) + if current.get("called"): + return current.get("result") + return await original(*args, **kwargs) + finally: + state.reset(token) + + @wraps(replacement) + def guarded(*args, **kwargs): + current = {} + token = state.set(current) + try: + return replacement(*args, **kwargs) + except Exception: + failure(current) + if current.get("called"): + return current.get("result") + return original(*args, **kwargs) + finally: + state.reset(token) + + return guarded_async if inspect.iscoroutinefunction(replacement) else guarded + + def set(self, target, name, replacement): + original = target.__dict__.get(name) + present = name in target.__dict__ + replacement = self.guard(target, name, replacement) + setattr(target, name, replacement) + self.methods.append((target, name, replacement, original, present)) + + def event(self, target, name, handler): + target.add_event_handler(name, handler) + self.handlers.append((target, name, handler)) + + def close(self): + for target, name, handler in reversed(self.handlers): + target.remove_event_handler(name, handler) + self.handlers.clear() + for target, name, replacement, original, present in reversed(self.methods): + if target.__dict__.get(name) is replacement: + if present: + setattr(target, name, original) + else: + delattr(target, name) + self.methods.clear() + self.invocations.clear() diff --git a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py new file mode 100644 index 000000000..002079a62 --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py @@ -0,0 +1,777 @@ +"""Experimental native Pipecat observer and opt-in recording support.""" + +import asyncio +import dataclasses +import io +import json +import time +import wave +from collections import Counter, deque + +from braintrust.audio.alignment import Alignment +from braintrust.audio.attachments import prepare_recording +from braintrust.audio.budget import source_budget +from braintrust.audio.recording import encode_audio +from braintrust.audio.segments import SegmentedRecording +from braintrust.audio.worker import RecordingBusy, encode_in_worker +from pipecat.observers.base_observer import BaseObserver # pylint: disable=import-error + +from .turns import Turns + + +# High-frequency payloads are handled by their owning operation or recorder. +# Only selected diagnostic/lifecycle facts become events. +EVENT_TYPES = { + "StartFrame", + "EndFrame", + "CancelFrame", + "ClientConnectedFrame", + "OutputTransportReadyFrame", + "BotStartedSpeakingFrame", + "BotStoppedSpeakingFrame", + "UserStartedSpeakingFrame", + "UserStoppedSpeakingFrame", + "VADUserStartedSpeakingFrame", + "VADUserStoppedSpeakingFrame", + "InterruptionFrame", + "UserTurnInferenceCompletedFrame", + "EagerEndOfTurnCancelFrame", + "UserMuteStartedFrame", + "UserMuteStoppedFrame", + "STTMuteFrame", + "ErrorFrame", + "FatalErrorFrame", + "MetricsFrame", + "SpeechControlParamsFrame", + "STTMetadataFrame", + "LLMServiceMetadataFrame", + "LLMUpdateSettingsFrame", + "TTSUpdateSettingsFrame", + "STTUpdateSettingsFrame", + "VADParamsUpdateFrame", +} +OPERATION_TYPES = { + "LLMContextFrame", + "LLMFullResponseStartFrame", + "LLMFullResponseEndFrame", + "LLMTextFrame", + "TranscriptionFrame", + "FunctionCallsStartedFrame", + "FunctionCallInProgressFrame", + "FunctionCallResultFrame", + "FunctionCallCancelFrame", + "TTSStartedFrame", + "TTSStoppedFrame", + "TTSTextFrame", +} + + +def native_value(value): + if isinstance(value, bytes): + return None + if dataclasses.is_dataclass(value): + return { + field.name: native_value(getattr(value, field.name)) + for field in dataclasses.fields(value) + if field.name not in {"audio", "image"} and not field.name.startswith("_") + } + if hasattr(value, "model_dump"): + return native_value(value.model_dump(mode="json")) + if isinstance(value, dict): + return {str(key): native_value(item) for key, item in value.items() if key not in {"audio", "image"}} + if isinstance(value, (tuple, list)): + return [native_value(item) for item in value] + if value is None or isinstance(value, (str, int, float, bool)): + return value + return str(value) + + +def encode_wav(chunks, sample_rate, channels): + output = io.BytesIO() + with wave.Wave_write(output) as wav: + wav.setnchannels(channels) + wav.setsampwidth(2) + wav.setframerate(sample_rate) + for chunk in chunks: + wav.writeframesraw(chunk) + return output.getvalue() + + +class NativeObserver(BaseObserver): + def __init__( + self, + logger, + *, + retain_audio=False, + max_audio_bytes=8 * 1024 * 1024, + audio_format="ogg", + capture_user_audio=None, + capture_agent_audio=None, + root=None, + recording_options=None, + ): + if audio_format not in {"ogg", "wav"}: + raise ValueError("AUDIO_FORMAT must be ogg or wav") + super().__init__(observe_every_push=False) + from .hooks import Hooks + + self.hooks = Hooks() + self.capture_user_audio = retain_audio if capture_user_audio is None else capture_user_audio + self.capture_agent_audio = retain_audio if capture_agent_audio is None else capture_agent_audio + retain_audio = self.capture_user_audio or self.capture_agent_audio + self.logger = logger + self.audio_format = audio_format + self.call_recording = SegmentedRecording( + enabled=retain_audio, + options=recording_options, + audio_format=audio_format, + on_segment=self._publish_call_segment, + ) + self.call_descriptors = {} + self.clip_tasks = set() + self.frame_contexts = {} + self.realtime = None + self.input_processor = None + self.capture_transport = False + self.root = ( + root if root is not None else logger.start_span(name="pipecat.pipeline", type="task", set_current=False) + ) + self.turns = Turns(self.root, self.hooks) + self.turns.on_completed = self._turn_completed + self.alignment = Alignment(self.root, self.call_recording) + self.user_capture = None + self.llm_turn = None + self.user_aggregator = None + self.assistant_aggregator = None + self.context_reply_to = None + self.context_tool_results = [] + self.result_origins = {} + self.event_groups = {} + self.filtered_events = Counter() + self.duplicate_events = 0 + self.clock_anchored = False + self.llm = None + self.llm_text = [] + self.user = None + self.tools = {} + self.requests = {} + self.tts = {} + self.events = [] + self.omitted_events = 0 + self.event_bytes = 0 + self.retain_audio = retain_audio + self.max_audio_bytes = max_audio_bytes + self.audio_bytes = 0 + self.recordings = [] + self.recordings_omitted = 0 + self.finished = False + self._finish_task = None + self.tool_names = [] + self.started_tools = set() + self.last_context = None + self.seen = set() + self.seen_order = deque() + + def __del__(self): + retained = getattr(self, "audio_bytes", 0) + if retained: + source_budget.release(retained) + self.audio_bytes = 0 + + def bind(self, *, transport, user_aggregator, assistant_aggregator, stt=None, realtime_service=None): + """Bind one supported pipeline before it starts; does not alter routing.""" + import importlib.metadata + + if importlib.metadata.version("pipecat-ai") != "1.12.0": + raise ValueError("Experimental voice hooks currently support Pipecat 1.12.0") + if getattr(self, "_bound", False): + raise ValueError("Voice observer is already bound to a pipeline") + if (stt is None) == (realtime_service is None): + raise ValueError("Bind either segmented STT or an OpenAI realtime service") + self.turns.install(user_aggregator, assistant_aggregator) + self.user_aggregator, self.assistant_aggregator = user_aggregator, assistant_aggregator + self.capture_transport = True + if stt is not None: + from .user_capture import UserCapture + + self.input_processor = transport.input() + self.user_capture = UserCapture(self, stt, user_aggregator, native_value) + else: + from .realtime import RealtimeCapture + + self.realtime = RealtimeCapture(self, realtime_service, user_aggregator) + if self.capture_agent_audio: + from .alignment import instrument_output + + instrument_output(transport.output(), self.alignment, self.frame_context, self.hooks) + self._bound = True + return self + + def frame_context(self, frame): + return self.frame_contexts.get(frame.id, getattr(frame, "context_id", None)) + + async def on_push_frame(self, data): + if not data.first_push or self.finished: + return + frame = data.frame + sibling = getattr(frame, "broadcast_sibling_id", None) + if frame.id in self.seen or (sibling is not None and sibling in self.seen): + self.duplicate_events += 1 + return + if len(self.seen_order) >= 4096: + self.seen.discard(self.seen_order.popleft()) + self.seen.add(frame.id) + self.seen_order.append(frame.id) + if ( + self.capture_user_audio + and not self.user_capture + and data.source is self.input_processor + and type(frame).__name__ + in { + "InputAudioRawFrame", + "UserAudioRawFrame", + } + ): + try: + self.call_recording.capture(0, frame.audio, frame.sample_rate, frame.num_channels) + except Exception: # noqa: BLE001 - capture/export failures must not break the call + self.call_recording.omit("capture_error") + kind = type(frame).__name__ + # Audio data stays out of metadata; retain its format on the owning TTS span. + if kind == "TTSAudioRawFrame": + context = self.frame_context(frame) + state = self.tts.get(context) + if state: + audio_format = (frame.sample_rate, frame.num_channels) + state["last_pts"] = frame.pts + if state.get("observed_format") != audio_format: + state["observed_format"] = audio_format + state["span"].log( + metadata={ + "pipecat.sample_rate": frame.sample_rate, + "pipecat.num_channels": frame.num_channels, + "pipecat.frame.pts": frame.pts, + } + ) + if state["chunks"] and (state["rate"], state["channels"]) != audio_format: + state["omitted"] = True + state["reason"] = "audio_format_changed" + size = sum(map(len, state["chunks"])) + source_budget.release(size) + self.audio_bytes -= size + state["chunks"].clear() + if self.capture_agent_audio and not state["omitted"]: + if self.audio_bytes + len(frame.audio) <= self.max_audio_bytes and source_budget.reserve( + len(frame.audio) + ): + state["chunks"].append(frame.audio) + self.audio_bytes += len(frame.audio) + state["rate"], state["channels"] = ( + frame.sample_rate, + frame.num_channels, + ) + else: + state["omitted"] = True + state["reason"] = ( + "capture_byte_limit" + if self.audio_bytes + len(frame.audio) > self.max_audio_bytes + else "process_capture_byte_limit" + ) + source_budget.release(sum(map(len, state["chunks"]))) + self.audio_bytes -= sum(map(len, state["chunks"])) + state["chunks"].clear() + return + if "AudioRawFrame" in kind: + return + if (self.user_capture or self.realtime) and kind in { + "VADUserStartedSpeakingFrame", + "VADUserStoppedSpeakingFrame", + "TranscriptionFrame", + }: + # These facts are attributed through the actual STT/aggregator path. + return + if kind not in EVENT_TYPES and kind not in OPERATION_TYPES: + self.filtered_events[kind] += 1 + return + fields = native_value(frame) if kind != "LLMContextFrame" else {} + event_owner = self.root + if kind == "StartFrame": + self.root.log( + metadata={ + f"pipecat.{name}": getattr(frame, name) + for name in ( + "audio_in_sample_rate", + "audio_out_sample_rate", + "enable_metrics", + "enable_usage_metrics", + ) + } + ) + elif kind == "LLMContextFrame": + self.last_context = native_value(frame.context.get_messages()) + self.context_tool_results = [] + self.context_reply_to = None + if data.source is self.user_aggregator and self.turns.last_user: + self.context_reply_to = self.turns.last_user["span"].span_id + elif data.source is self.assistant_aggregator: + # Only newly observed tool results establish a continuation. + # Old tool messages remaining in context do not create links. + ids = { + m.get("tool_call_id") for m in self.last_context if isinstance(m, dict) and m.get("role") == "tool" + } + self.context_tool_results = sorted(ids & self.result_origins.keys()) + origins = {self.result_origins.pop(i) for i in self.context_tool_results} + if len(origins) == 1: + self.context_reply_to = origins.pop() + elif kind in {"UserStartedSpeakingFrame", "VADUserStartedSpeakingFrame"}: + if kind == "UserStartedSpeakingFrame": + self.turns.start("user", kind) + elif not self.user: + self.user = self.root.start_span( + name="pipecat.user_speaking", + type="task", + set_current=False, + internal={"instrumentation": "pipecat-auto"}, + metadata={"pipecat.start_frame": kind}, + ) + event_owner = self.user + elif kind in {"UserStoppedSpeakingFrame", "VADUserStoppedSpeakingFrame"}: + if kind == "VADUserStoppedSpeakingFrame" and self.user: + event_owner = self.user + self.user.log(metadata={"pipecat.end_frame": kind}) + self.user.end() + self.user = None + elif kind == "TranscriptionFrame": + # STT can trigger the next user turn rather than belong to an open + # one. The aggregator supplies authoritative turn text separately. + span = self.root.start_span( + name="pipecat.stt_transcription", + type="task", + set_current=False, + internal={"instrumentation": "pipecat-auto"}, + metadata={f"pipecat.{key}": value for key, value in fields.items()}, + ) + span.log(input={"text": frame.text}) + span.end() + elif kind == "LLMFullResponseStartFrame": + if self.realtime and self.llm is not None: + # Pipecat brackets both response creation and item arrival. + # One still-open response must not create another turn. + self.filtered_events["repeated_response_start"] += 1 + return + if self.realtime: + turn = self.realtime.user_turns.get(self.realtime.user_item) + self.context_reply_to = turn["span"].span_id if turn else None + self.context_tool_results = self.realtime.pending_tool_results[:] + self.realtime.pending_tool_results.clear() + self.result_origins.clear() + self.llm_text = [] + # A new model operation may arrive before the previous aggregator + # callback task finishes. Keep each native response lifecycle distinct. + self.turns.assistant = None + self.llm_turn = self.turns.start("assistant", kind, self.context_reply_to) + self.llm = self.llm_turn["span"].start_span( + name="pipecat.llm_response", + type="llm", + input=self.last_context, + metadata={ + **self.turns.metadata(self.llm_turn), + "braintrust.continuation.tool_call_ids": self.context_tool_results, + }, + set_current=False, + internal={"instrumentation": "pipecat-auto"}, + ) + elif kind == "LLMTextFrame" and self.llm: + self.llm_text.append(frame.text) + elif kind == "FunctionCallsStartedFrame": + if self.llm_turn: + self.llm_turn["tool_calls"] = [ + { + "id": call.tool_call_id, + "type": "function", + "function": { + "name": call.function_name, + "arguments": json.dumps(native_value(call.arguments)), + }, + } + for call in frame.function_calls + ] + self.turns.log_message(self.llm_turn, "") + for call in frame.function_calls: + self.requests.setdefault( + call.tool_call_id, + ( + self.llm or self.root, + self.turns.metadata(self.llm_turn) + or ({"braintrust.turn.reply_to": self.context_reply_to} if self.context_reply_to else {}), + ), + ) + if self.llm: + self.llm.log(metadata={"pipecat.function_calls": native_value(frame.function_calls)}) + elif kind == "FunctionCallInProgressFrame": + if frame.tool_call_id in self.started_tools: + return + self.started_tools.add(frame.tool_call_id) + owner, correlation = self.requests.get(frame.tool_call_id, (self.root, {})) + self.tools[frame.tool_call_id] = owner.start_span( + name=frame.function_name, + type="tool", + input=frame.arguments, + metadata={ + **correlation, + "pipecat.tool_call_id": frame.tool_call_id, + "pipecat.function_name": frame.function_name, + "pipecat.arguments": frame.arguments, + "pipecat.group_id": frame.group_id, + "pipecat.cancel_on_interruption": frame.cancel_on_interruption, + }, + set_current=False, + internal={"instrumentation": "pipecat-auto"}, + ) + if len(self.tool_names) < 512: + self.tool_names.append(frame.function_name) + elif kind in {"FunctionCallResultFrame", "FunctionCallCancelFrame"}: + tool = self.tools.pop(frame.tool_call_id, None) + if tool: + if kind == "FunctionCallResultFrame": + request = self.requests.pop(frame.tool_call_id, None) + if request: + self.result_origins[frame.tool_call_id] = request[1].get("braintrust.turn.reply_to") + tool.log( + output=native_value(frame.result), + metadata={"pipecat.result": native_value(frame.result)}, + ) + if frame.error: + tool.log(error=frame.error) + else: + tool.log(metadata={"pipecat.cancelled": True}) + tool.end() + elif kind == "LLMFullResponseEndFrame" and self.llm: + self.llm.log( + output=[{"role": "assistant", "content": "".join(self.llm_text)}], + metadata={"pipecat.text": "".join(self.llm_text)}, + ) + self.llm.end() + self.llm = None + elif kind == "TTSStartedFrame": + context = self.frame_context(frame) + if context in self.tts: + return + turn = self.turns.assistant or self.turns.start("assistant", kind) + span = turn["span"].start_span( + name="pipecat.tts_response", + type="task", + metadata={ + **self.turns.metadata(turn), + "pipecat.context_id": getattr(frame, "context_id", None), + **({"openai.response.id": context} if self.realtime and context else {}), + "pipecat.append_to_context": frame.append_to_context, + }, + set_current=False, + internal={"instrumentation": "pipecat-auto"}, + ) + self.tts[context] = { + "span": span, + "chunks": [], + "omitted": False, + "text": [], + } + if len(self.alignment.contexts) < 16000: + self.alignment.contexts[context] = [span, turn["span"]] + else: + self.alignment.omitted += 1 + elif kind == "TTSTextFrame": + state = self.tts.get(self.frame_context(frame)) + if state: + state["text"].append(frame.text) + state["span"].log(metadata={"pipecat.text": "".join(state["text"])}) + elif kind == "TTSStoppedFrame": + state = self.tts.pop(self.frame_context(frame), None) + if state: + state["span"].end() + self.complete_recording(state) + elif kind == "InterruptionFrame": + for state in self.tts.values(): + state["span"].log(metadata={"pipecat.end_frame": kind}) + state["span"].end() + self.complete_recording(state) + self.tts.clear() + elif kind in {"ErrorFrame", "FatalErrorFrame"}: + self.root.log(error=str(frame.error)) + + if kind in EVENT_TYPES: + if kind.startswith("User"): + turn = self.turns.user + event_owner = turn["span"] if turn else self.root + elif kind.startswith("Bot") or kind == "InterruptionFrame": + turn = self.turns.assistant + event_owner = turn["span"] if turn else self.root + # The current LLM is not evidence that a concurrent service metric + # belongs to it. Keep metrics at the root with native processor IDs. + self.capture_event(data, fields, event_owner) + + def capture_event(self, data, fields, owner): + if not self.clock_anchored: + self.root.log( + metadata={ + "braintrust.clock": { + "observer_timestamp_ns": data.timestamp, + "observed_unix_ms": time.time() * 1000, + "basis": "observer_callback", + } + } + ) + self.clock_anchored = True + event = { + "pipecat.frame.type": type(data.frame).__name__, + "pipecat.frame.id": data.frame.id, + "pipecat.observer.timestamp": data.timestamp, + "pipecat.observer.source": data.source.name, + "pipecat.observer.destination": data.destination.name, + "pipecat.observer.direction": data.direction.name, + "pipecat.frame": fields, + } + size = len(json.dumps(event, ensure_ascii=False).encode("utf-8")) + if len(self.events) < 256 and self.event_bytes + size < 65536: + self.events.append(event) + self.event_bytes += size + group = self.event_groups.setdefault(owner.span_id, (owner, [])) + group[1].append(event) + else: + self.omitted_events += 1 + + def complete_recording(self, state): + if "last_pts" in state: + state["span"].log(metadata={"pipecat.frame.pts": state["last_pts"]}) + if len(self.recordings) < 256: + self.recordings.append(state) + task = asyncio.create_task(self._publish_tts_clip(state)) + self.clip_tasks.add(task) + task.add_done_callback(self.clip_tasks.discard) + return + self.recordings_omitted += 1 + state["span"].log( + metadata={ + "audio.recordings": [ + { + "id": "tts-clip", + "state": "omitted", + "reason": "recording_count_limit", + "sources": [{"boundary": "tts_output"}], + } + ] + } + ) + size = sum(map(len, state["chunks"])) + state["chunks"].clear() + source_budget.release(size) + self.audio_bytes -= size + + async def cleanup(self): + await self.finish() + await super().cleanup() + + async def finish(self): + if self._finish_task is None: + self.finished = True + self._finish_task = asyncio.create_task(self._finish()) + await asyncio.shield(self._finish_task) + + async def _finish(self): + self.call_recording.seal() + self.hooks.close() + try: + await self._finalize() + finally: + self.hooks.close() + # A shielded segment drain can outlive cancellation of this finalizer. + # Its active tail still belongs to that drain until it seals/exports it. + drain = self.call_recording._finish_task + if drain is None or drain.done(): + self.call_recording.clear() + for state in self.recordings: + state["chunks"].clear() + source_budget.release(self.audio_bytes) + self.audio_bytes = 0 + if self.user_capture: + self.user_capture.release() + + async def _finalize(self): + self.turns.finish() + for span in [self.llm, self.user, *self.tools.values()]: + if span: + span.end() + for state in self.tts.values(): + state["span"].end() + state["omitted"] = True + state["reason"] = "pipeline_closed_before_tts_stop" + self.complete_recording(state) + for owner, events in self.event_groups.values(): + owner.log(metadata={"pipecat.events": events}) + self.root.log( + metadata={ + "braintrust.capture.events_omitted": self.omitted_events, + "braintrust.capture.recordings_omitted": self.recordings_omitted, + "braintrust.capture.filtered_event_counts": dict(self.filtered_events), + "braintrust.capture.duplicate_events": self.duplicate_events, + "braintrust.capture.retained_events": len(self.events), + } + ) + self.root.end() + if self.user_capture: + await self.user_capture.finish() + if self.realtime: + self.realtime.finish() + # Trace lifetime ends before encoding; flush only after attachment updates. + if self.clip_tasks: + await asyncio.gather(*tuple(self.clip_tasks)) + if self.capture_transport: + await self.call_recording.finish() + self._publish_call_manifest() + self.alignment.publish() + await asyncio.to_thread(self.logger.flush) + + async def _publish_tts_clip(self, state): + span = state["span"] + recording = {"id": "tts-clip", "sources": [{"boundary": "tts_output"}]} + if state["chunks"] and not state["omitted"]: + try: + encoded = await encode_in_worker( + prepare_recording, + "tts-clip", + encode_audio, + state["chunks"], + state["rate"], + state["channels"], + self.audio_format, + ) + except Exception as error: # noqa: BLE001 - capture/export failures must not break the call + span.log( + metadata={ + "audio.recordings": [ + { + **recording, + "state": "omitted", + "reason": str(error) if isinstance(error, RecordingBusy) else "encoding_failed", + } + ] + } + ) + source_budget.release(sum(map(len, state["chunks"]))) + self.audio_bytes -= sum(map(len, state["chunks"])) + state["chunks"].clear() + return + recording.update( + state="ready", + attachment={ + "span_id": span.span_id, + "ref": "/output/0/content/1/file/file_data", + }, + mime_type=encoded["mime_type"], + duration_ms=encoded["duration_ms"], + channel_count=encoded["channel_count"], + ) + span.log( + output=[ + { + "role": "assistant", + "content": [ + {"type": "text", "text": "".join(state["text"])}, + { + "type": "file", + "file": { + "file_data": encoded["attachment"], + "filename": f"tts-clip.{encoded['extension']}", + }, + }, + ], + } + ], + metadata={"braintrust.recording.processing": encoded["processing"]}, + ) + else: + recording.update( + state="omitted", + reason=state.get("reason", "no_audio_observed") if self.capture_agent_audio else "disabled", + ) + span.log(metadata={"audio.recordings": [recording]}) + source_budget.release(sum(map(len, state["chunks"]))) + self.audio_bytes -= sum(map(len, state["chunks"])) + state["chunks"].clear() + + await asyncio.to_thread(self.logger.flush) + + def _call_sources(self): + return [ + *( + [{"boundary": "model_input" if self.realtime else "transport_input", "channel_index": 0}] + if self.capture_user_audio + else [] + ), + *([{"boundary": "transport_output", "channel_index": 1}] if self.capture_agent_audio else []), + ] + + async def _publish_call_segment(self, segment, encoded): + if encoded is None: + return + recording_id = segment.segment_id + self.call_descriptors[recording_id] = { + "id": recording_id, + "recording_group_id": "call", + "state": "ready", + "attachment": {"span_id": self.root.span_id, "ref": f"/input/audio/{recording_id}"}, + "mime_type": encoded["mime_type"], + "duration_ms": encoded["duration_ms"], + "channel_count": 2, + "sources": self._call_sources(), + "timeline": { + "origin_unix_ms": self.call_recording.origin_unix_ms, + "recording_start_offset_ms": segment.start_ms, + "basis": "input_sample_clock_and_output_write_observation", + }, + } + self.root.log(input={"audio": {recording_id: encoded["attachment"]}}) + self._publish_call_manifest() + self.alignment.publish() + await asyncio.to_thread(self.logger.flush) + + def _publish_call_manifest(self): + for segment in self.call_recording.completed: + if segment["state"] == "omitted": + self.call_descriptors[segment["id"]] = { + "id": segment["id"], + "recording_group_id": "call", + "state": "omitted", + "reason": segment["reason"], + "sources": self._call_sources(), + "timeline": { + "origin_unix_ms": self.call_recording.origin_unix_ms, + "recording_start_offset_ms": segment["start_ms"], + "basis": "input_sample_clock_and_output_write_observation", + }, + "gaps": [ + { + "start_offset_ms": segment["start_ms"], + "end_offset_ms": segment["end_ms"], + "reason": segment["reason"], + } + ], + } + descriptors = list(self.call_descriptors.values()) + reason = self.call_recording.reason + if reason or not descriptors: + descriptors.append( + { + "id": "call", + "recording_group_id": "call", + "state": "omitted", + "reason": reason or "no_audio_observed", + "truncated": reason not in (None, "disabled"), + "sources": self._call_sources(), + } + ) + self.root.log(metadata={"audio.recordings": descriptors}) + + def _turn_completed(self, turn): + if turn["role"] == "user" and self.user_capture: + self.user_capture.queue_completed(turn) diff --git a/py/src/braintrust/integrations/pipecat/voice/realtime.py b/py/src/braintrust/integrations/pipecat/voice/realtime.py new file mode 100644 index 000000000..3ed87350a --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/voice/realtime.py @@ -0,0 +1,264 @@ +"""Pinned OpenAI Realtime hooks: native event IDs and sent-sample provenance. + +Server VAD + uninterrupted PCM input only. Buffer clears invalidate selections. +No STT operation is fabricated for asynchronous provider transcription events. +""" + +from contextvars import ContextVar + +from .instrumentation import native_value + + +class RealtimeCapture: + def __init__(self, observer, service, aggregator): + self.observer = observer + self.items = {} + self.consumed = [] + self.associations = [] + self.sent_samples = 0 + self.sent_ranges = [] + self.invalid = False + self.active = ContextVar("realtime_response", default=None) + self.service = service + self.user_item = None + self.user_turns = {} + self.pending_tool_results = [] + original_tool_result = observer.hooks.original(service, "_send_tool_result") + + async def tool_result(tool_call_id, result): + self.pending_tool_results.append(tool_call_id) + return await original_tool_result(tool_call_id, result) + + observer.hooks.set(service, "_send_tool_result", tool_result) + + async def user_started(aggregator, strategy): + if self.user_item and observer.turns.user: + self.user_turns[self.user_item] = observer.turns.user + if len(self.user_turns) > 256: + self.user_turns.pop(next(iter(self.user_turns))) + self.invalid = True + observer.turns.user["span"].log(metadata={"openai.item_id": self.user_item}) + + observer.hooks.event(aggregator, "on_user_turn_started", user_started) + original_item = observer.hooks.original(service, "_handle_evt_conversation_item_added") + + async def item_added(evt): + if evt.item.type == "function_call" and observer.llm is None: + turn = self.user_turns.get(self.user_item) + observer.context_reply_to = turn["span"].span_id if turn else None + observer.llm_turn = None + observer.llm_text = [] + observer.llm = observer.root.start_span( + name="pipecat.llm_response", + type="llm", + set_current=False, + internal={"instrumentation": "pipecat-auto"}, + metadata={ + "openai.item_id": evt.item.id, + "openai.call_id": evt.item.call_id, + "braintrust.turn.reply_to": observer.context_reply_to, + }, + ) + return await original_item(evt) + + observer.hooks.set(service, "_handle_evt_conversation_item_added", item_added) + original_push = observer.hooks.original(service, "push_frame") + + async def push(frame, *args, **kwargs): + context = self.active.get() + if context and type(frame).__name__ in { + "TTSStartedFrame", + "TTSStoppedFrame", + "TTSAudioRawFrame", + "TTSTextFrame", + }: + if len(observer.frame_contexts) < 16000: + observer.frame_contexts[frame.id] = context + else: + self.invalid = True + return await original_push(frame, *args, **kwargs) + + observer.hooks.set(service, "push_frame", push) + for name in ("_handle_evt_audio_delta", "_handle_evt_audio_transcript_delta", "_handle_evt_response_done"): + original = observer.hooks.original(service, name) + + async def response(evt, original=original): + response_id = getattr(evt, "response_id", None) or getattr(getattr(evt, "response", None), "id", None) + token = self.active.set(response_id) + response_span = observer.llm + if response_span and response_id: + response_span.log(metadata={"openai.response.id": response_id}) + if getattr(evt, "response", None) is not None: + response_span.log(metadata={"openai.response": native_value(evt.response)}) + try: + result = await original(evt) + if response_span and getattr(evt, "response", None) is not None: + calls = [item for item in evt.response.output if item.type == "function_call"] + if calls: + response_span.log( + output=[ + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": item.call_id, + "type": "function", + "function": {"name": item.name, "arguments": item.arguments}, + } + for item in calls + ], + } + ] + ) + state = observer.tts.get(response_id) + if state: + state["span"].log( + metadata={ + "openai.response.id": response_id, + "openai.item_id": getattr(evt, "item_id", None), + } + ) + return result + finally: + self.active.reset(token) + + observer.hooks.set(service, name, response) + + for name, field in ( + ("_handle_evt_speech_started", "audio_start_ms"), + ("_handle_evt_speech_stopped", "audio_end_ms"), + ): + original = observer.hooks.original(service, name) + + async def speech(evt, original=original, field=field): + if field == "audio_start_ms": + self.user_item = evt.item_id + if len(self.items) < 256 or evt.item_id in self.items: + self.items.setdefault(evt.item_id, {})[field] = getattr(evt, field) + return await original(evt) + + observer.hooks.set(service, name, speech) + + original_accept = observer.hooks.original(aggregator, "_handle_transcription") + original_commit = observer.hooks.original(aggregator, "_push_aggregation") + original_reset = observer.hooks.original(aggregator, "reset") + + async def accept(frame): + result = await original_accept(frame) + if frame.text.strip() and len(self.consumed) < 256: + self.consumed.append(native_value(frame)) + return result + + async def reset(): + self.consumed.clear() + return await original_reset() + + async def commit(*args, **kwargs): + consumed = self.consumed[:] + turn = observer.turns.user or observer.turns.messages.get(("user", aggregator._user_turn_start_timestamp)) + if turn: + observer.context_reply_to = turn["span"].span_id + result = await original_commit(*args, **kwargs) + if result and turn: + ids = [f.get("result", {}).get("item_id") for f in consumed if isinstance(f.get("result"), dict)] + turn["span"].log( + metadata={ + "pipecat.transcriptions": consumed, + "openai.item_ids": ids, + "braintrust.user_capture.association": "aggregator_consumed_frames", + } + ) + if len(self.associations) < 256: + self.associations.append((turn["span"], ids)) + else: + self.invalid = True + observer.context_reply_to = turn["span"].span_id + return result + + observer.hooks.set(aggregator, "_handle_transcription", accept) + observer.hooks.set(aggregator, "_push_aggregation", commit) + observer.hooks.set(aggregator, "reset", reset) + # Opt-out never installs the per-audio-frame send hook or stores sample ranges. + if observer.capture_user_audio: + original_send = observer.hooks.original(service, "_send_user_audio") + original_event = observer.hooks.original(service, "send_client_event") + original_connect = observer.hooks.original(service, "_connect") + audio_frame = ContextVar("realtime_input_frame", default=None) + self.bound_socket = None + + async def connect(): + result = await original_connect() + socket = service._websocket + if socket is not None and socket is not self.bound_socket: + if self.bound_socket is not None: + self.invalid = True # Provider buffer clock may restart. + self.bound_socket = socket + original_socket_send = observer.hooks.original(socket, "send") + + async def socket_send(message, *args, **kwargs): + frame = audio_frame.get() + try: + result = await original_socket_send(message, *args, **kwargs) + except BaseException: + if frame is not None: + self.invalid = True + raise + if frame is not None: + record_sent(frame) + return result + + observer.hooks.set(socket, "send", socket_send) + return result + + def record_sent(frame): + if frame.sample_rate != 24000 or frame.num_channels != 1: + self.invalid = True + try: + interval = observer.call_recording.capture(0, frame.audio, frame.sample_rate, frame.num_channels) + except Exception: + observer.call_recording.omit("capture_error") + interval = None + size = len(frame.audio) // 2 + if interval and len(self.sent_ranges) < 16000: + self.sent_ranges.append((self.sent_samples, self.sent_samples + size, interval)) + else: + self.invalid = True + self.sent_samples += size + + async def send_audio(frame): + token = audio_frame.set(frame) + try: + return await original_send(frame) + finally: + audio_frame.reset(token) + + observer.hooks.set(service, "_connect", connect) + + async def send_event(evt): + if evt.type == "input_audio_buffer.clear": + self.invalid = True + return await original_event(evt) + + observer.hooks.set(service, "_send_user_audio", send_audio) + observer.hooks.set(service, "send_client_event", send_event) + + def finish(self): + for span, ids in self.associations: + events = [{"item_id": item, **self.items.get(item, {})} for item in ids] + span.log(metadata={"openai.input_audio_segments": events}) + if not self.observer.capture_user_audio or self.invalid: + continue + for event in events: + if "audio_start_ms" not in event or "audio_end_ms" not in event: + continue + start, end = round(event["audio_start_ms"] * 24), round(event["audio_end_ms"] * 24) + ranges = [] + for a, b, interval in self.sent_ranges: + left, right = max(start, a), min(end, b) + if right > left: + ranges.append([interval["start"] + left - a, interval["start"] + right - a]) + if sum(b - a for a, b in ranges) == end - start: + self.observer.alignment.add(span, ranges, 0) + self.observer.root.log(metadata={"braintrust.realtime.alignment_invalidated": self.invalid}) + self.sent_ranges.clear() diff --git a/py/src/braintrust/integrations/pipecat/voice/recording.py b/py/src/braintrust/integrations/pipecat/voice/recording.py new file mode 100644 index 000000000..380d3a51e --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/voice/recording.py @@ -0,0 +1,22 @@ +"""Pipecat transport capture hooks.""" + +import time + + +def capture_transport_output(output, recording): + """Observe only successful writes and preserve the transport's pacing/result.""" + original = output.write_audio_frame + + async def write(frame): + if recording.reason: + return await original(frame) + observed_ns = time.monotonic_ns() + result = await original(frame) + if result: + try: + recording.capture(1, frame.audio, frame.sample_rate, frame.num_channels, observed_ns=observed_ns) + except Exception: # noqa: BLE001 - capture/export failures must not break the call + recording.omit("capture_error") + return result + + output.write_audio_frame = write diff --git a/py/src/braintrust/integrations/pipecat/voice/test_alignment.py b/py/src/braintrust/integrations/pipecat/voice/test_alignment.py new file mode 100644 index 000000000..d40f39438 --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/voice/test_alignment.py @@ -0,0 +1,148 @@ +import asyncio +import io +import unittest +from types import SimpleNamespace +from unittest.mock import patch + +import numpy as np +import soundfile as sf # pylint: disable=import-error +from braintrust.audio.alignment import Alignment, InputRanges +from braintrust.audio.recording import CallRecording +from pipecat.frames.frames import TTSAudioRawFrame, TTSStoppedFrame # pylint: disable=import-error +from pipecat.transports.base_output import BaseOutputTransport # pylint: disable=import-error +from pipecat.transports.base_transport import TransportParams # pylint: disable=import-error + +from .alignment import instrument_output +from .test_instrumentation import Span + + +class AlignmentTests(unittest.IsolatedAsyncioTestCase): + def test_input_trim_uses_sample_positions_not_span_time(self): + recording = CallRecording() + ledger = InputRanges() + for index in range(3): + frame = SimpleNamespace( + audio=np.full(320, index + 1, dtype=" 512: + self.messages.pop(next(iter(self.messages))) + self.states = [pending for pending in self.states if pending is not state] + if getattr(self, role) is state: + setattr(self, role, None) + + def finish(self): + for state in self.states: + if not state["ended"]: + state["span"].log(metadata={"braintrust.turn.incomplete": True}) + state["span"].end() + state["ended"] = True diff --git a/py/src/braintrust/integrations/pipecat/voice/user_capture.py b/py/src/braintrust/integrations/pipecat/voice/user_capture.py new file mode 100644 index 000000000..ac135fef5 --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/voice/user_capture.py @@ -0,0 +1,322 @@ +"""Pinned Pipecat 1.12 cascade hooks: STT segment -> accepted frames -> turn. + +The native aggregator owns membership. No transcript or timestamp matching. +""" + +import asyncio +import io +import time +import wave +from collections import deque + +from braintrust.audio.alignment import InputRanges +from braintrust.audio.attachments import prepare_recording +from braintrust.audio.budget import source_budget +from braintrust.audio.recording import encode_audio +from braintrust.audio.worker import RecordingBusy, encode_in_worker +from pipecat.frames.frames import TranscriptionFrame # pylint: disable=import-error + + +def encode_segments(segments, audio_format): + chunks = [] + rate = channels = None + for segment in segments: + with wave.open(io.BytesIO(segment), "rb") as source: + if source.getsampwidth() != 2: + raise ValueError("Expected PCM16 STT input") + current = source.getframerate(), source.getnchannels() + if rate is not None and current != (rate, channels): + raise ValueError("Mixed STT input formats") + rate, channels = current + chunks.append(source.readframes(source.getnframes())) + return encode_audio(chunks, rate, channels, audio_format) + + +class UserCapture: + def __init__(self, observer, stt, aggregator, native_value): + self.observer = observer + self.accepted = [] + self.by_frame = {} + self.batches = {} + self.bytes = 0 + self.boundaries = [] + self.segment_boundaries = deque(maxlen=256) + self.omitted = 0 + self.input_ranges = InputRanges() if observer.capture_user_audio else None + self.segments = [] + + original_start = observer.hooks.original(stt, "_handle_user_started_speaking") + original_stop = observer.hooks.original(stt, "_handle_user_stopped_speaking") + original_run = observer.hooks.original(stt, "run_stt") + original_accept = observer.hooks.original(aggregator, "_handle_transcription") + original_commit = observer.hooks.original(aggregator, "_push_aggregation") + original_reset = observer.hooks.original(aggregator, "reset") + original_audio = observer.hooks.original(stt, "process_audio_frame") + + async def process_audio(frame, direction): + try: + interval = observer.call_recording.capture(0, frame.audio, frame.sample_rate, frame.num_channels) + except Exception as error: # noqa: BLE001 - recording must not stop input processing + observer.call_recording.omit("capture_error") + interval = None + result = await original_audio(frame, direction) + self.input_ranges.append(len(frame.audio), interval) + # Mirror the actual retained native buffer length, not a guessed VAD time. + self.input_ranges.trim(len(stt._audio_buffer)) + return result + + async def speech_start(frame): + self.boundaries = [ + { + "pipecat.frame.type": type(frame).__name__, + "pipecat.frame": native_value(frame), + } + ] + return await original_start(frame) + + async def speech_stop(frame): + if stt.is_usable: + self.segment_boundaries.append( + { + "ranges": self.input_ranges.drain() if self.input_ranges else [], + "events": [ + *self.boundaries, + { + "pipecat.frame.type": type(frame).__name__, + "pipecat.frame": native_value(frame), + }, + ], + } + ) + elif self.input_ranges: + self.input_ranges.drain() + self.boundaries = [] + return await original_stop(frame) + + async def run(audio): + queued = self.segment_boundaries.popleft() if self.segment_boundaries else {"events": [], "ranges": []} + segment = { + "boundaries": queued["events"], + "ranges": queued["ranges"], + "audio": None, + "reason": "disabled", + "start": time.time(), + "end": None, + "frames": [], + "span": None, + } + tracked = len(self.segments) < 256 + if tracked: + self.segments.append(segment) + else: + self.omitted += 1 + if observer.capture_user_audio and tracked: + if self.bytes + len(audio) <= observer.max_audio_bytes and source_budget.reserve(len(audio)): + segment.update(audio=audio, reason=None) + self.bytes += len(audio) + else: + segment["reason"] = ( + "capture_byte_limit" + if self.bytes + len(audio) > observer.max_audio_bytes + else "process_capture_byte_limit" + ) + try: + async for frame in original_run(audio): + try: + segment["end"] = time.time() + if isinstance(frame, TranscriptionFrame): + segment["frames"].append(native_value(frame)) + if len(self.by_frame) < 256: + self.by_frame[frame.id] = segment + else: + self.omitted += 1 + elif getattr(frame, "error", None): + segment["error"] = str(frame.error) + except Exception: # noqa: BLE001 - always deliver the native STT frame + self.omitted += 1 + yield frame + except BaseException as error: + segment["error"] = type(error).__name__ + raise + finally: + if segment["end"] is None: + segment["end"] = time.time() + + async def accept(frame): + result = await original_accept(frame) + if frame.text.strip(): + if len(self.accepted) < 256: + self.accepted.append((native_value(frame), self.by_frame.pop(frame.id, None))) + else: + self.omitted += 1 + return result + + async def reset(): + self.accepted = [] + return await original_reset() + + async def commit(*args, **kwargs): + # Snapshot before native reset/context push. The original method + # can schedule turn-stop callbacks while it awaits downstream work. + accepted = self.accepted[:] + turn = observer.turns.user + result = await original_commit(*args, **kwargs) + if result and accepted: + owner = turn["span"] if turn else observer.root + segments = [] + for _, segment in accepted: + if segment is not None and not any(s is segment for s in segments): + segments.append(segment) + self.create_stt(segment, owner, turn) + owner.log( + metadata={ + "pipecat.transcriptions": [frame for frame, _ in accepted], + "pipecat.speech_events": [event for s in segments for event in s["boundaries"]], + "braintrust.user_capture.association": "aggregator_consumed_frames", + } + ) + if turn and (turn["span"].span_id in self.batches or len(self.batches) < 256): + batch = self.batches.setdefault( + turn["span"].span_id, + {"turn": turn, "segments": [], "frames": []}, + ) + batch["frames"].extend(frame for frame, _ in accepted) + for segment in segments: + if not any(s is segment for s in batch["segments"]): + batch["segments"].append(segment) + owner.log( + metadata={ + "pipecat.transcriptions": batch["frames"], + "pipecat.speech_events": [event for s in batch["segments"] for event in s["boundaries"]], + } + ) + if turn: + self.queue_completed(turn) + return result + + observer.hooks.set(stt, "_handle_user_started_speaking", speech_start) + observer.hooks.set(stt, "_handle_user_stopped_speaking", speech_stop) + observer.hooks.set(stt, "run_stt", run) + if observer.capture_user_audio: + observer.hooks.set(stt, "process_audio_frame", process_audio) + observer.hooks.set(aggregator, "_handle_transcription", accept) + observer.hooks.set(aggregator, "_push_aggregation", commit) + observer.hooks.set(aggregator, "reset", reset) + + def __del__(self): + retained = getattr(self, "bytes", 0) + if retained: + source_budget.release(retained) + self.bytes = 0 + + def create_stt(self, segment, owner, turn=None): + if segment["span"] is not None: + return + metadata = { + "pipecat.transcriptions": segment["frames"], + "pipecat.function": "run_stt", + } + if turn: + metadata.update(self.observer.turns.metadata(turn)) + span = owner.start_span( + name="pipecat.stt", + type="task", + start_time=segment["start"], + set_current=False, + internal={"instrumentation": "pipecat-auto"}, + metadata=metadata, + ) + text = " ".join(f["text"] for f in segment["frames"]) + span.log(output=[{"role": "user", "content": text}]) + if segment.get("error"): + span.log(error=segment["error"]) + span.end(end_time=segment["end"] or time.time()) + segment["span"] = span + self.observer.alignment.add(span, segment["ranges"], 0) + if turn: + self.observer.alignment.add(owner, segment["ranges"], 0) + span.log(input={"recording_span_id": owner.span_id, "recording_id": "user-clip"}) + + async def finish(self): + for segment in self.segments: + self.create_stt(segment, self.observer.root) + for batch in list(self.batches.values()): + self.queue_completed(batch["turn"], force=True) + if getattr(self, "tasks", None): + await asyncio.gather(*tuple(self.tasks)) + self.observer.root.log(metadata={"braintrust.user_capture.events_omitted": self.omitted}) + self.release() + + def queue_completed(self, turn, force=False): + batch = self.batches.get(turn["span"].span_id) + if not batch or batch.get("queued") or (not force and not turn.get("ended")): + return + batch["queued"] = True + if not hasattr(self, "tasks"): + self.tasks = set() + task = asyncio.create_task(self._publish_batch(batch)) + self.tasks.add(task) + task.add_done_callback(self.tasks.discard) + + async def _publish_batch(self, batch): + turn, segments = batch["turn"], batch["segments"] + descriptor = {"id": "user-clip", "sources": [{"boundary": "stt_input"}]} + reason = next((s["reason"] for s in segments if s["reason"]), None) + audio = [s["audio"] for s in segments if s["audio"] is not None] + if reason or not audio: + descriptor.update(state="omitted", reason=reason or "no_audio_observed") + else: + try: + encoded = await encode_in_worker( + prepare_recording, "user-clip", encode_segments, audio, self.observer.audio_format + ) + descriptor.update( + state="ready", + attachment={ + "span_id": turn["span"].span_id, + "ref": "/input/0/content/1/file/file_data", + }, + mime_type=encoded["mime_type"], + duration_ms=encoded["duration_ms"], + channel_count=encoded["channel_count"], + ) + attachment = encoded["attachment"] + turn["span"].log( + input=[ + { + "role": "user", + "content": [ + {"type": "text", "text": turn.get("content") or ""}, + { + "type": "file", + "file": { + "file_data": attachment, + "filename": f"user-clip.{encoded['extension']}", + }, + }, + ], + } + ] + ) + except Exception as error: # noqa: BLE001 - attachment failure must not break flush + descriptor.update( + state="omitted", + reason=str(error) if isinstance(error, RecordingBusy) else "encoding_failed", + ) + turn["span"].log(metadata={"audio.recordings": [descriptor]}) + + for segment in segments: + if segment["audio"] is not None: + size = len(segment["audio"]) + segment["audio"] = None + self.bytes -= size + source_budget.release(size) + await asyncio.to_thread(self.observer.logger.flush) + + def release(self): + self.batches.clear() + self.by_frame.clear() + self.accepted.clear() + self.segments.clear() + source_budget.release(self.bytes) + self.bytes = 0 diff --git a/py/uv.lock b/py/uv.lock index 01a0df6f1..127d5cbe8 100644 --- a/py/uv.lock +++ b/py/uv.lock @@ -1009,6 +1009,7 @@ test-pipecat = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-vcr" }, + { name = "soundfile" }, { name = "websockets" }, ] test-pydantic-ai-logfire = [ @@ -1248,6 +1249,7 @@ test-pipecat = [ { name = "pytest", specifier = "==9.1.1" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, { name = "pytest-vcr", specifier = "==1.0.2" }, + { name = "soundfile", specifier = ">=0.13.1" }, { name = "websockets", specifier = "==15.0.1" }, ] test-pydantic-ai-logfire = [ From 27ec415ce39f378dc34a85c1346f0c34aec168a6 Mon Sep 17 00:00:00 2001 From: David Elner Date: Thu, 1 Oct 2026 10:37:16 -0400 Subject: [PATCH 03/18] Added: turn_metrics to Pipecat --- .../integrations/pipecat/test_pipecat.py | 37 ++++++++++++++++++ .../integrations/pipecat/tracing.py | 16 +++++++- .../integrations/pipecat/turn_metrics.py | 23 +++++++++++ .../integrations/pipecat/voice/README.md | 1 + .../pipecat/voice/instrumentation.py | 15 ++++++++ .../integrations/pipecat/voice/test_turns.py | 38 +++++++++++++++++++ 6 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 py/src/braintrust/integrations/pipecat/turn_metrics.py diff --git a/py/src/braintrust/integrations/pipecat/test_pipecat.py b/py/src/braintrust/integrations/pipecat/test_pipecat.py index ab8511857..8f115a068 100644 --- a/py/src/braintrust/integrations/pipecat/test_pipecat.py +++ b/py/src/braintrust/integrations/pipecat/test_pipecat.py @@ -370,3 +370,40 @@ def test_setup_and_wrap_pipeline_worker_are_idempotent(): def test_auto_instrument_pipecat_subprocess(): pytest.importorskip("pipecat") verify_autoinstrument_script("test_auto_pipecat.py") + + +@pytest.mark.parametrize("metric_name", ["TurnMetricsData", "SmartTurnMetricsData"]) +def test_turn_detection_metrics_on_legacy_speech_span(metric_name): + metric_class = _import(f"pipecat.metrics.metrics.{metric_name}") + metric = metric_class(processor="BaseSmartTurn", is_complete=True, probability=0.97, e2e_processing_time_ms=82.4) + rows = [] + observer = BraintrustPipecatObserver(trace_turns=False) + observer._user_audio_span = SimpleNamespace(log=lambda **row: rows.append(row)) + observer._capture_metrics(SimpleNamespace(data=[metric]), SimpleNamespace(name="aggregator")) + assert rows[-1]["metadata"]["pipecat.turn_metrics"] == [ + dict( + type=metric_name, + processor="BaseSmartTurn", + is_complete=True, + probability=0.97, + e2e_processing_time_ms=82.4, + ) + ] + + +def test_turn_detection_without_audio_keeps_unassociated_measurement(): + metric_class = _import("pipecat.metrics.metrics.TurnMetricsData") + rows = [] + observer = BraintrustPipecatObserver(trace_turns=False, capture_audio_attachments=False) + observer._pipeline_span = SimpleNamespace(log=lambda **row: rows.append(row)) + observer._capture_metrics( + SimpleNamespace( + data=[ + metric_class(processor="KrispVivaTurn", is_complete=False, probability=0.3, e2e_processing_time_ms=120) + ] + ), + SimpleNamespace(name="aggregator"), + ) + assert observer._user_audio_span is None + assert not observer._user_audio + assert rows[-1]["metadata"]["pipecat.turn_metrics"][0]["is_complete"] is False diff --git a/py/src/braintrust/integrations/pipecat/tracing.py b/py/src/braintrust/integrations/pipecat/tracing.py index 7f98018ab..900430353 100644 --- a/py/src/braintrust/integrations/pipecat/tracing.py +++ b/py/src/braintrust/integrations/pipecat/tracing.py @@ -15,6 +15,8 @@ from braintrust.logger import NOOP_SPAN, Attachment, SpanTypeAttribute, current_span from braintrust.logger import start_span as _bt_start_span +from .turn_metrics import TURN_METRIC_TYPES, log_turn_metric + _INSTRUMENTATION = "pipecat-auto" @@ -122,6 +124,8 @@ def __init__( self._tts_default_span: Any | None = None self._tts_audio: dict[str, bytearray] = {} self._tts_audio_metadata: dict[str, dict[str, Any]] = {} + self._turn_metric_state: dict[str, Any] = {} + self._unassociated_turn_metric_state: dict[str, Any] = {} self._user_audio_span: Any | None = None self._user_audio: bytearray | None = None self._user_audio_metadata: dict[str, Any] = {} @@ -464,6 +468,7 @@ def _start_user_audio_span(self, frame: Any | None = None) -> None: self._ensure_pipeline_span() if self._user_audio_span is not None: return + self._turn_metric_state = {} self._user_audio = bytearray() self._user_audio_metadata = _audio_frame_metadata(frame) if frame is not None else {} self._user_audio_span = start_span( @@ -528,7 +533,16 @@ def _capture_metrics(self, frame: Any, processor: Any) -> None: processor_name = _processor_name(processor) for metric in getattr(frame, "data", []) or []: metric_type = type(metric).__name__ - if metric_type == "LLMUsageMetricsData": + if metric_type in TURN_METRIC_TYPES: + self._ensure_pipeline_span() + owner = self._user_audio_span or self._pipeline_span + state = ( + self._turn_metric_state + if self._user_audio_span is not None + else self._unassociated_turn_metric_state + ) + log_turn_metric(owner, state, metric) + elif metric_type == "LLMUsageMetricsData": metric_processor = getattr(metric, "processor", None) if processor_name is not None and metric_processor is not None and metric_processor != processor_name: continue diff --git a/py/src/braintrust/integrations/pipecat/turn_metrics.py b/py/src/braintrust/integrations/pipecat/turn_metrics.py new file mode 100644 index 000000000..5534da2a4 --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/turn_metrics.py @@ -0,0 +1,23 @@ +"""Native turn analyzer measurements, without inferred EOU timing.""" + +TURN_METRIC_TYPES = {"TurnMetricsData", "SmartTurnMetricsData"} +MAX_TURN_METRICS = 32 + + +def log_turn_metric(span, state, metric): + """Keep bounded predictions on their owner, preserving native units and identity.""" + predictions = state.setdefault("turn_metrics", []) + if len(predictions) >= MAX_TURN_METRICS: + state["turn_metrics_omitted"] = state.get("turn_metrics_omitted", 0) + 1 + span.log(metadata={"braintrust.turn_metrics.omitted": state["turn_metrics_omitted"]}) + return + predictions.append( + { + "type": type(metric).__name__, + **{ + name: getattr(metric, name) + for name in ("processor", "is_complete", "probability", "e2e_processing_time_ms") + }, + } + ) + span.log(metadata={"pipecat.turn_metrics": list(predictions)}) diff --git a/py/src/braintrust/integrations/pipecat/voice/README.md b/py/src/braintrust/integrations/pipecat/voice/README.md index b06107d97..add05d410 100644 --- a/py/src/braintrust/integrations/pipecat/voice/README.md +++ b/py/src/braintrust/integrations/pipecat/voice/README.md @@ -20,6 +20,7 @@ Install `braintrust[audio]` for Ogg/Opus or WAV encoding. Audio defaults off. Ex | Public interfaces | `setup_pipecat`, automatic integration setup, `wrap_pipeline_worker`, and an explicitly supplied `BraintrustPipecatObserver` share the same injection path. `trace_turns=False` retains the existing observer behavior. | | Trace | Native aggregator turns, actual STT operations, model responses, tool executions, and generated-audio lifecycles. Tool-only realtime responses can be pipeline children. | | Audio | Combined stereo call; segmented-STT caller clips on user turns; generated clips on TTS spans. Selections reference successfully captured samples. | +| Turn detection | Native analyzer predictions in `pipecat.turn_metrics` on the user turn; preserve processor, metric class, confidence, completion and reported milliseconds. Retain 32 predictions and count omissions. Unassociated observations remain on the pipeline. | | Input alignment | Segmented STT input provenance or successful OpenAI input writes plus server-VAD item offsets. Clears/reconnects invalidate unsupported mappings. | | Output alignment | Equal-rate mono PCM without a mixer. Other paths retain recordings without asserting unsupported selections. | | Memory | Configurable 32 MiB retained call PCM by default; separate 8 MiB caller/generated source limits; 64 MiB shared source budget; 32 MiB encoded-attachment budget. These are retained-data bounds, not total RSS bounds. | diff --git a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py index 002079a62..90bf30663 100644 --- a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py +++ b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py @@ -16,6 +16,7 @@ from braintrust.audio.worker import RecordingBusy, encode_in_worker from pipecat.observers.base_observer import BaseObserver # pylint: disable=import-error +from ..turn_metrics import TURN_METRIC_TYPES, log_turn_metric from .turns import Turns @@ -295,6 +296,20 @@ async def on_push_frame(self, data): return fields = native_value(frame) if kind != "LLMContextFrame" else {} event_owner = self.root + if kind == "MetricsFrame" and data.source is self.user_aggregator and self.turns.user: + # The single discovered user aggregator emits its analyzer predictions + # before stopping this turn. Other sources retain unassociated events. + remaining = [] + for metric in frame.data: + if type(metric).__name__ in TURN_METRIC_TYPES: + state = self.turns.user + # Turns assigns role dictionaries through setattr. + log_turn_metric(state["span"], state, metric) # pylint: disable=unsubscriptable-object + else: + remaining.append(native_value(metric)) + if not remaining: + return + fields["data"] = remaining if kind == "StartFrame": self.root.log( metadata={ diff --git a/py/src/braintrust/integrations/pipecat/voice/test_turns.py b/py/src/braintrust/integrations/pipecat/voice/test_turns.py index 303aac2c3..626216a41 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_turns.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_turns.py @@ -169,3 +169,41 @@ async def test_interruption_ends_tts_and_transcription_does_not_guess_a_turn(sel self.assertIn(state, self.observer.recordings) self.assertEqual(state["span"].rows[-1]["metadata"]["pipecat.end_frame"], "InterruptionFrame") await self.observer.finish() + + async def test_native_turn_metrics_ownership_and_limits(self): + from pipecat.frames.frames import MetricsFrame # pylint: disable=import-error + from pipecat.metrics.metrics import SmartTurnMetricsData, TurnMetricsData # pylint: disable=import-error + + observer = self.observer + await self.push(UserStartedSpeakingFrame()) + turn = observer.turns.user + for metric_class in (TurnMetricsData, SmartTurnMetricsData): + for index in range(20): + frame = MetricsFrame( + data=[ + metric_class( + processor="BaseSmartTurn", + is_complete=index == 19, + probability=0.97, + e2e_processing_time_ms=82.4, + ) + ] + ) + await self.push(frame, self.user) + await self.push(frame, self.user) # broadcast/repeated observations do not duplicate predictions + metadata = {key: value for row in turn["span"].rows for key, value in row.get("metadata", {}).items()} + assert len(metadata["pipecat.turn_metrics"]) == 32 + assert metadata["braintrust.turn_metrics.omitted"] == 8 + assert metadata["pipecat.turn_metrics"][0]["processor"] == "BaseSmartTurn" + assert metadata["pipecat.turn_metrics"][20]["type"] == "SmartTurnMetricsData" + before = len(turn["span"].rows) + await self.push( + MetricsFrame( + data=[TurnMetricsData(processor="other", is_complete=True, probability=1, e2e_processing_time_ms=3)] + ) + ) + assert len(turn["span"].rows) == before + observer.turns.stop("user", SimpleNamespace(content="hello", timestamp="t", user_id="u")) + await self.push(UserStartedSpeakingFrame()) + assert not any("pipecat.turn_metrics" in row.get("metadata", {}) for row in observer.turns.user["span"].rows) + await observer.finish() From 56c94aa55138703762e7f585864787df74f7a456 Mon Sep 17 00:00:00 2001 From: David Elner Date: Thu, 1 Oct 2026 10:55:06 -0400 Subject: [PATCH 04/18] Added: TTFB routing to Pipecat --- .../integrations/pipecat/test_pipecat.py | 40 +++++++++++++++++ .../integrations/pipecat/tracing.py | 27 ++++++++--- .../braintrust/integrations/pipecat/ttfb.py | 45 +++++++++++++++++++ .../integrations/pipecat/voice/README.md | 1 + .../pipecat/voice/instrumentation.py | 21 ++++++++- .../integrations/pipecat/voice/test_turns.py | 19 ++++++++ .../pipecat/voice/test_user_capture.py | 24 ++++++++++ .../pipecat/voice/user_capture.py | 10 +++++ 8 files changed, 180 insertions(+), 7 deletions(-) create mode 100644 py/src/braintrust/integrations/pipecat/ttfb.py diff --git a/py/src/braintrust/integrations/pipecat/test_pipecat.py b/py/src/braintrust/integrations/pipecat/test_pipecat.py index 8f115a068..fc900c164 100644 --- a/py/src/braintrust/integrations/pipecat/test_pipecat.py +++ b/py/src/braintrust/integrations/pipecat/test_pipecat.py @@ -407,3 +407,43 @@ def test_turn_detection_without_audio_keeps_unassociated_measurement(): assert observer._user_audio_span is None assert not observer._user_audio assert rows[-1]["metadata"]["pipecat.turn_metrics"][0]["is_complete"] is False + + +@pytest.mark.asyncio +async def test_ttfb_routes_by_processor_and_retains_unmatched(memory_logger): + observer = BraintrustPipecatObserver(trace_turns=False, capture_audio_attachments=False) + frames = importlib.import_module("pipecat.frames.frames") + metric_class = _import("pipecat.metrics.metrics.TTFBMetricsData") + llm, tts, stt = [SimpleNamespace(name=name) for name in ("llm", "tts", "stt")] + await observer._handle_frame(frames.LLMFullResponseStartFrame(), processor=llm) + await observer._handle_frame(frames.TTSStartedFrame(), processor=tts) + for processor, value in ((tts, 0.09), (stt, 0.12), (llm, 0.24)): + await observer._handle_frame( + frames.MetricsFrame(data=[metric_class(processor=processor.name, value=value)]), processor=processor + ) + await observer._handle_frame(frames.TTSStoppedFrame(), processor=tts) + observer._close_all_open_spans() + logs = memory_logger.pop() + assert _single_span(logs, "tts_response")["metadata"]["pipecat.ttfb"][0]["value"] == 0.09 + assert _single_span(logs, "pipecat_llm_response")["metrics"]["time_to_first_token"] == 0.24 + assert _single_span(logs, "pipecat_pipeline")["metadata"]["pipecat.ttfb"][0]["processor"] == "stt" + + +def test_ttfb_router_bounds_and_rejects_mismatched_sources(): + from braintrust.integrations.pipecat.ttfb import TTFBRouter + + metric_class = _import("pipecat.metrics.metrics.TTFBMetricsData") + root, operation = [], [] + router = TTFBRouter(lambda **row: root.append(row)) + source = SimpleNamespace(name="tts") + router.start("a", source, lambda **row: operation.append(row)) + metric = metric_class(processor="tts", value=0.1) + router.capture(metric, SimpleNamespace(name="tts")) + assert not operation # Same name is not the same processor instance. + for _ in range(35): + router.capture(metric, source) + assert operation[-1]["metadata"]["braintrust.ttfb.omitted"] == 3 + router.start("a", source, lambda **row: operation.append(row)) + assert router.capture(metric, source) is None + router.clear() + assert not router.active diff --git a/py/src/braintrust/integrations/pipecat/tracing.py b/py/src/braintrust/integrations/pipecat/tracing.py index 900430353..98e8a76a6 100644 --- a/py/src/braintrust/integrations/pipecat/tracing.py +++ b/py/src/braintrust/integrations/pipecat/tracing.py @@ -15,6 +15,7 @@ from braintrust.logger import NOOP_SPAN, Attachment, SpanTypeAttribute, current_span from braintrust.logger import start_span as _bt_start_span +from .ttfb import TTFBRouter from .turn_metrics import TURN_METRIC_TYPES, log_turn_metric @@ -115,6 +116,7 @@ def __init__( self._llm_span: Any | None = None self._llm_parent: str | None = None self._llm_text_parts: list[str] = [] + self._ttfb = TTFBRouter(self._log_unmatched_ttfb) self._llm_metrics: dict[str, Any] = {} self._llm_metadata: dict[str, Any] = {} self._llm_tool_calls: list[dict[str, Any]] = [] @@ -233,7 +235,7 @@ async def _handle_frame(self, frame: Any, *, processor: Any = None) -> None: elif frame_type == "TranscriptionFrame": self._log_transcription(frame) elif frame_type == "TTSStartedFrame": - self._start_tts_span(frame) + self._start_tts_span(frame, processor) elif frame_type == "TTSTextFrame": self._append_tts_text(frame) elif frame_type == "TTSAudioRawFrame": @@ -289,6 +291,7 @@ def _capture_llm_context(self, frame: Any, processor: Any) -> None: def _start_llm_span(self, processor: Any) -> None: self._ensure_pipeline_span(processor=processor) if self._llm_span is not None: + self._ttfb.start("llm", None, self._llm_span.log) return metadata = {**self._latest_llm_metadata, **_metadata_from_processor(processor)} self._llm_metadata = _filter_llm_metadata(metadata) @@ -304,6 +307,7 @@ def _start_llm_span(self, processor: Any) -> None: set_current=False, ) self._llm_parent = self._llm_span.export() + self._ttfb.start("llm", processor, self._llm_span.log) def _capture_llm_tool_calls(self, frame: Any) -> None: calls = getattr(frame, "function_calls", None) or [] @@ -338,6 +342,7 @@ def _end_llm_span(self) -> None: if metadata: event["metadata"] = metadata self._llm_span.log(**event) + self._ttfb.end("llm") self._llm_span.end() self._llm_span = None self._llm_parent = None @@ -400,7 +405,7 @@ def _log_transcription(self, frame: Any) -> None: ) span.end() - def _start_tts_span(self, frame: Any) -> None: + def _start_tts_span(self, frame: Any, processor: Any = None) -> None: self._ensure_pipeline_span() context_id = getattr(frame, "context_id", None) or "__default__" span = start_span( @@ -415,6 +420,7 @@ def _start_tts_span(self, frame: Any) -> None: } span.log(metadata={k: v for k, v in metadata.items() if v is not None}) self._tts_spans[context_id] = span + self._ttfb.start(("tts", context_id), processor, span.log) if context_id == "__default__": self._tts_default_span = span @@ -447,6 +453,7 @@ def _log_tts_audio(self, frame: Any) -> None: def _end_tts_span(self, frame: Any) -> None: context_id = getattr(frame, "context_id", None) or "__default__" + self._ttfb.end(("tts", context_id)) span = self._tts_spans.pop(context_id, None) if span is not None: output = self._pop_tts_audio_output(context_id) @@ -529,6 +536,10 @@ def _discard_tts_audio(self, context_id: str) -> None: self._tts_audio.pop(context_id, None) self._tts_audio_metadata.pop(context_id, None) + def _log_unmatched_ttfb(self, **event): + self._ensure_pipeline_span() + self._pipeline_span.log(**event) + def _capture_metrics(self, frame: Any, processor: Any) -> None: processor_name = _processor_name(processor) for metric in getattr(frame, "data", []) or []: @@ -548,12 +559,14 @@ def _capture_metrics(self, frame: Any, processor: Any) -> None: continue self._llm_metrics.update(_llm_usage_metrics(getattr(metric, "value", None))) self._llm_metadata.update(_metadata_from_metric(metric)) - elif metric_type == "TTFBMetricsData" and self._llm_span is not None: + self._llm_metadata.update(_metadata_from_processor(processor)) + elif metric_type == "TTFBMetricsData": + owner = self._ttfb.capture(metric, processor) value = getattr(metric, "value", None) - if isinstance(value, (int, float)) and not isinstance(value, bool): + if owner == "llm" and isinstance(value, (int, float)) and not isinstance(value, bool): self._llm_metrics["time_to_first_token"] = value - self._llm_metadata.update(_metadata_from_metric(metric)) - self._llm_metadata.update(_metadata_from_processor(processor)) + self._llm_metadata.update(_metadata_from_metric(metric)) + self._llm_metadata.update(_metadata_from_processor(processor)) self._llm_metadata = _filter_llm_metadata(self._llm_metadata) def _log_error_frame(self, frame: Any) -> None: @@ -561,6 +574,7 @@ def _log_error_frame(self, frame: Any) -> None: error = getattr(frame, "exception", None) or getattr(frame, "error", None) if self._llm_span is not None: self._llm_span.log(error=error) + self._ttfb.end("llm") self._llm_span.end() self._llm_span = None if self._pipeline_span is not None: @@ -597,6 +611,7 @@ def _close_child_spans(self) -> None: self._tts_default_span = None def _close_all_open_spans(self) -> None: + self._ttfb.clear() self._close_child_spans() if self._pipeline_span is not None: self._pipeline_span.end() diff --git a/py/src/braintrust/integrations/pipecat/ttfb.py b/py/src/braintrust/integrations/pipecat/ttfb.py new file mode 100644 index 000000000..444af4ade --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/ttfb.py @@ -0,0 +1,45 @@ +"""Associate reported TTFB only with an unambiguous live processor operation.""" + + +class TTFBRouter: + def __init__(self, fallback): + self.fallback = fallback + self.active = {} + self.unmatched = {} + + def start(self, key, processor, log): + # Repeated starts without a matching end cannot identify one operation. + if key in self.active: + processor = None + self.active[key] = (processor, log, {}) + + def end(self, key): + self.active.pop(key, None) + + def clear(self): + self.active.clear() + + def capture(self, metric, source): + name = getattr(metric, "processor", None) + matches = [ + (key, log, state) + for key, (processor, log, state) in self.active.items() + if processor is source and name is not None and getattr(processor, "name", None) == name + ] + if len(matches) == 1: + key, log, state = matches[0] + else: + key, log, state = None, self.fallback, self.unmatched + values = state.setdefault("values", []) + if len(values) >= 32: + state["omitted"] = state.get("omitted", 0) + 1 + log(metadata={"braintrust.ttfb.omitted": state["omitted"]}) + else: + payload = ( + metric.model_dump(mode="json") + if hasattr(metric, "model_dump") + else {"processor": name, "value": metric.value} + ) + values.append(payload) + log(metadata={"pipecat.ttfb": list(values)}) + return key diff --git a/py/src/braintrust/integrations/pipecat/voice/README.md b/py/src/braintrust/integrations/pipecat/voice/README.md index add05d410..39073b81e 100644 --- a/py/src/braintrust/integrations/pipecat/voice/README.md +++ b/py/src/braintrust/integrations/pipecat/voice/README.md @@ -20,6 +20,7 @@ Install `braintrust[audio]` for Ogg/Opus or WAV encoding. Audio defaults off. Ex | Public interfaces | `setup_pipecat`, automatic integration setup, `wrap_pipeline_worker`, and an explicitly supplied `BraintrustPipecatObserver` share the same injection path. `trace_turns=False` retains the existing observer behavior. | | Trace | Native aggregator turns, actual STT operations, model responses, tool executions, and generated-audio lifecycles. Tool-only realtime responses can be pipeline children. | | Audio | Combined stereo call; segmented-STT caller clips on user turns; generated clips on TTS spans. Selections reference successfully captured samples. | +| TTFB | Native `pipecat.ttfb` measurements follow the emitting processor and a unique active operation. STT measurements follow `run_stt` into its eventual span. Ambiguous/unmatched metrics remain on the pipeline; arrays are bounded to 32 with omission counts. | | Turn detection | Native analyzer predictions in `pipecat.turn_metrics` on the user turn; preserve processor, metric class, confidence, completion and reported milliseconds. Retain 32 predictions and count omissions. Unassociated observations remain on the pipeline. | | Input alignment | Segmented STT input provenance or successful OpenAI input writes plus server-VAD item offsets. Clears/reconnects invalidate unsupported mappings. | | Output alignment | Equal-rate mono PCM without a mixer. Other paths retain recordings without asserting unsupported selections. | diff --git a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py index 90bf30663..0d57d4157 100644 --- a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py +++ b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py @@ -16,6 +16,7 @@ from braintrust.audio.worker import RecordingBusy, encode_in_worker from pipecat.observers.base_observer import BaseObserver # pylint: disable=import-error +from ..ttfb import TTFBRouter from ..turn_metrics import TURN_METRIC_TYPES, log_turn_metric from .turns import Turns @@ -137,6 +138,7 @@ def __init__( self.root = ( root if root is not None else logger.start_span(name="pipecat.pipeline", type="task", set_current=False) ) + self.ttfb = TTFBRouter(self.root.log) self.turns = Turns(self.root, self.hooks) self.turns.on_completed = self._turn_completed self.alignment = Alignment(self.root, self.call_recording) @@ -296,6 +298,16 @@ async def on_push_frame(self, data): return fields = native_value(frame) if kind != "LLMContextFrame" else {} event_owner = self.root + if kind == "MetricsFrame": + remaining = [] + for metric in frame.data: + if type(metric).__name__ == "TTFBMetricsData": + self.ttfb.capture(metric, data.source) + else: + remaining.append(native_value(metric)) + if not remaining: + return + fields["data"] = remaining if kind == "MetricsFrame" and data.source is self.user_aggregator and self.turns.user: # The single discovered user aggregator emits its analyzer predictions # before stopping this turn. Other sources retain unassociated events. @@ -305,7 +317,7 @@ async def on_push_frame(self, data): state = self.turns.user # Turns assigns role dictionaries through setattr. log_turn_metric(state["span"], state, metric) # pylint: disable=unsubscriptable-object - else: + elif type(metric).__name__ != "TTFBMetricsData": remaining.append(native_value(metric)) if not remaining: return @@ -396,6 +408,7 @@ async def on_push_frame(self, data): set_current=False, internal={"instrumentation": "pipecat-auto"}, ) + self.ttfb.start("llm", data.source, self.llm.log) elif kind == "LLMTextFrame" and self.llm: self.llm_text.append(frame.text) elif kind == "FunctionCallsStartedFrame": @@ -466,6 +479,7 @@ async def on_push_frame(self, data): output=[{"role": "assistant", "content": "".join(self.llm_text)}], metadata={"pipecat.text": "".join(self.llm_text)}, ) + self.ttfb.end("llm") self.llm.end() self.llm = None elif kind == "TTSStartedFrame": @@ -485,6 +499,7 @@ async def on_push_frame(self, data): set_current=False, internal={"instrumentation": "pipecat-auto"}, ) + self.ttfb.start(("tts", context), data.source, span.log) self.tts[context] = { "span": span, "chunks": [], @@ -501,6 +516,7 @@ async def on_push_frame(self, data): state["text"].append(frame.text) state["span"].log(metadata={"pipecat.text": "".join(state["text"])}) elif kind == "TTSStoppedFrame": + self.ttfb.end(("tts", self.frame_context(frame))) state = self.tts.pop(self.frame_context(frame), None) if state: state["span"].end() @@ -510,6 +526,8 @@ async def on_push_frame(self, data): state["span"].log(metadata={"pipecat.end_frame": kind}) state["span"].end() self.complete_recording(state) + for context in self.tts: + self.ttfb.end(("tts", context)) self.tts.clear() elif kind in {"ErrorFrame", "FatalErrorFrame"}: self.root.log(error=str(frame.error)) @@ -612,6 +630,7 @@ async def _finish(self): self.user_capture.release() async def _finalize(self): + self.ttfb.clear() self.turns.finish() for span in [self.llm, self.user, *self.tools.values()]: if span: diff --git a/py/src/braintrust/integrations/pipecat/voice/test_turns.py b/py/src/braintrust/integrations/pipecat/voice/test_turns.py index 626216a41..3dec84a2d 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_turns.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_turns.py @@ -207,3 +207,22 @@ async def test_native_turn_metrics_ownership_and_limits(self): await self.push(UserStartedSpeakingFrame()) assert not any("pipecat.turn_metrics" in row.get("metadata", {}) for row in observer.turns.user["span"].rows) await observer.finish() + + async def test_ttfb_routes_only_to_unambiguous_processor_operation(self): + from pipecat.frames.frames import MetricsFrame, TTSStoppedFrame # pylint: disable=import-error + from pipecat.metrics.metrics import TTFBMetricsData # pylint: disable=import-error + + llm, tts = SimpleNamespace(name="llm"), SimpleNamespace(name="tts") + await self.push(LLMFullResponseStartFrame(), llm) + await self.push(TTSStartedFrame(context_id="a"), tts) + span = self.observer.tts["a"]["span"] + await self.push(MetricsFrame(data=[TTFBMetricsData(processor="tts", value=0.09)]), tts) + assert span.rows[-1]["metadata"]["pipecat.ttfb"][0]["value"] == 0.09 + await self.push(TTSStartedFrame(context_id="b"), tts) + await self.push(MetricsFrame(data=[TTFBMetricsData(processor="tts", value=0.11)]), tts) + assert self.observer.root.rows[-1]["metadata"]["pipecat.ttfb"][0]["value"] == 0.11 + await self.push(TTSStoppedFrame(context_id="a"), tts) + await self.push(TTSStoppedFrame(context_id="b"), tts) + await self.push(MetricsFrame(data=[TTFBMetricsData(processor="tts", value=0.13)]), tts) + assert self.observer.root.rows[-1]["metadata"]["pipecat.ttfb"][-1]["value"] == 0.13 + await self.observer.finish() diff --git a/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py b/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py index f8e03eb81..19006931a 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py @@ -6,14 +6,17 @@ import soundfile as sf # pylint: disable=import-error from pipecat.frames.frames import ( # pylint: disable=import-error + MetricsFrame, TranscriptionFrame, VADUserStartedSpeakingFrame, VADUserStoppedSpeakingFrame, ) +from pipecat.metrics.metrics import TTFBMetricsData # pylint: disable=import-error from pipecat.processors.aggregators.llm_context import LLMContext # pylint: disable=import-error from pipecat.processors.aggregators.llm_response_universal import ( # pylint: disable=import-error LLMContextAggregatorPair, ) +from pipecat.processors.frame_processor import FrameDirection # pylint: disable=import-error from .instrumentation import NativeObserver, encode_wav, native_value from .test_instrumentation import Span @@ -48,6 +51,24 @@ async def push_context(): aggregator.push_context_frame = push_context stt = STT() + stt.name = "test-stt" + original_run = stt.run_stt + + async def measured_run(audio): + await observer.on_push_frame( + SimpleNamespace( + frame=MetricsFrame(data=[TTFBMetricsData(processor=stt.name, value=0.12)]), + first_push=True, + source=stt, + destination=aggregator, + direction=FrameDirection.DOWNSTREAM, + timestamp=1234, + ) + ) + async for result in original_run(audio): + yield result + + stt.run_stt = measured_run capture = UserCapture(observer, stt, aggregator, native_value) observer.user_capture = capture await stt._handle_user_started_speaking(VADUserStartedSpeakingFrame()) @@ -67,6 +88,9 @@ async def push_context(): self.assertEqual(context.get_messages()[-1]["content"], "Where is my order?") self.assertEqual(capture.batches[turn["span"].span_id]["frames"][0]["id"], frame.id) self.assertEqual(len(capture.batches[turn["span"].span_id]["segments"][0]["boundaries"]), 2) + stt_span = capture.batches[turn["span"].span_id]["segments"][0]["span"] + metadata = {key: value for row in stt_span.rows for key, value in row.get("metadata", {}).items()} + self.assertEqual(metadata["pipecat.ttfb"][0]["value"], 0.12) return observer, capture, turn async def test_native_consumption_attributes_earlier_frame_and_clip_to_later_turn( diff --git a/py/src/braintrust/integrations/pipecat/voice/user_capture.py b/py/src/braintrust/integrations/pipecat/voice/user_capture.py index ac135fef5..001f45f5e 100644 --- a/py/src/braintrust/integrations/pipecat/voice/user_capture.py +++ b/py/src/braintrust/integrations/pipecat/voice/user_capture.py @@ -120,6 +120,14 @@ async def run(audio): if self.bytes + len(audio) > observer.max_audio_bytes else "process_capture_byte_limit" ) + segment["ttfb_metadata"] = {} + + def log_ttfb(**event): + segment["ttfb_metadata"].update(event["metadata"]) + if segment["span"] is not None: + segment["span"].log(**event) + + observer.ttfb.start(("stt", id(segment)), stt, log_ttfb) try: async for frame in original_run(audio): try: @@ -139,6 +147,7 @@ async def run(audio): segment["error"] = type(error).__name__ raise finally: + observer.ttfb.end(("stt", id(segment))) if segment["end"] is None: segment["end"] = time.time() @@ -215,6 +224,7 @@ def create_stt(self, segment, owner, turn=None): metadata = { "pipecat.transcriptions": segment["frames"], "pipecat.function": "run_stt", + **segment.get("ttfb_metadata", {}), } if turn: metadata.update(self.observer.turns.metadata(turn)) From 4cb5a2b6f194c152b2f5cf48f4624ad9ce938e12 Mon Sep 17 00:00:00 2001 From: David Elner Date: Thu, 1 Oct 2026 11:07:41 -0400 Subject: [PATCH 05/18] Fixed: Preserve LLM usage and timing metrics in native Pipecat traces --- .../integrations/pipecat/llm_metrics.py | 73 +++++++++++++++++++ .../integrations/pipecat/tracing.py | 71 +----------------- .../braintrust/integrations/pipecat/ttfb.py | 16 ++-- .../pipecat/voice/instrumentation.py | 18 ++++- .../pipecat/voice/test_recorded_realtime.py | 4 + .../integrations/pipecat/voice/test_turns.py | 32 ++++++++ 6 files changed, 138 insertions(+), 76 deletions(-) create mode 100644 py/src/braintrust/integrations/pipecat/llm_metrics.py diff --git a/py/src/braintrust/integrations/pipecat/llm_metrics.py b/py/src/braintrust/integrations/pipecat/llm_metrics.py new file mode 100644 index 000000000..9e75d1108 --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/llm_metrics.py @@ -0,0 +1,73 @@ +"""Shared conversion of native Pipecat usage into Braintrust LLM metrics.""" + +from typing import Any + + +def _metadata_from_processor(processor: Any) -> dict[str, Any]: + metadata: dict[str, Any] = {} + settings = getattr(processor, "_settings", None) + model = getattr(settings, "model", None) or getattr(processor, "model", None) + if isinstance(model, str): + metadata["model"] = model + provider = _provider_from_processor(processor) + if provider: + metadata["provider"] = provider + return metadata + + +def _metadata_from_metric(metric: Any) -> dict[str, Any]: + metadata: dict[str, Any] = {} + model = getattr(metric, "model", None) + if isinstance(model, str): + metadata["model"] = model + processor = getattr(metric, "processor", None) + if isinstance(processor, str): + provider = _provider_from_name(processor) + if provider: + metadata["provider"] = provider + return metadata + + +def _provider_from_processor(processor: Any) -> str | None: + module = getattr(type(processor), "__module__", "") + return _provider_from_name(module) + + +def _provider_from_name(name: str) -> str | None: + lowered = name.lower() + providers = { + "openai": "openai", + "anthropic": "anthropic", + "google": "google", + "gemini": "google", + "mistral": "mistral", + "cohere": "cohere", + "bedrock": "bedrock", + "aws": "bedrock", + "openrouter": "openrouter", + } + for needle, provider in providers.items(): + if needle in lowered: + return provider + return None + + +def _llm_usage_metrics(usage: Any) -> dict[str, Any]: + metrics: dict[str, Any] = {} + prompt_tokens = getattr(usage, "prompt_tokens", None) + completion_tokens = getattr(usage, "completion_tokens", None) + total_tokens = getattr(usage, "total_tokens", None) + cache_read = getattr(usage, "cache_read_input_tokens", None) + cache_creation = getattr(usage, "cache_creation_input_tokens", None) + reasoning_tokens = getattr(usage, "reasoning_tokens", None) + for key, value in ( + ("prompt_tokens", prompt_tokens), + ("completion_tokens", completion_tokens), + ("tokens", total_tokens), + ("cache_read_input_tokens", cache_read), + ("cache_creation_input_tokens", cache_creation), + ("reasoning_tokens", reasoning_tokens), + ): + if isinstance(value, (int, float)) and not isinstance(value, bool): + metrics[key] = value + return metrics diff --git a/py/src/braintrust/integrations/pipecat/tracing.py b/py/src/braintrust/integrations/pipecat/tracing.py index 98e8a76a6..c1c682ae1 100644 --- a/py/src/braintrust/integrations/pipecat/tracing.py +++ b/py/src/braintrust/integrations/pipecat/tracing.py @@ -15,6 +15,7 @@ from braintrust.logger import NOOP_SPAN, Attachment, SpanTypeAttribute, current_span from braintrust.logger import start_span as _bt_start_span +from .llm_metrics import _llm_usage_metrics, _metadata_from_metric, _metadata_from_processor from .ttfb import TTFBRouter from .turn_metrics import TURN_METRIC_TYPES, log_turn_metric @@ -744,55 +745,6 @@ def _tools_schema_to_openai_tools(tools: Any) -> list[dict[str, Any]]: return ret -def _metadata_from_processor(processor: Any) -> dict[str, Any]: - metadata: dict[str, Any] = {} - settings = getattr(processor, "_settings", None) - model = getattr(settings, "model", None) or getattr(processor, "model", None) - if isinstance(model, str): - metadata["model"] = model - provider = _provider_from_processor(processor) - if provider: - metadata["provider"] = provider - return metadata - - -def _metadata_from_metric(metric: Any) -> dict[str, Any]: - metadata: dict[str, Any] = {} - model = getattr(metric, "model", None) - if isinstance(model, str): - metadata["model"] = model - processor = getattr(metric, "processor", None) - if isinstance(processor, str): - provider = _provider_from_name(processor) - if provider: - metadata["provider"] = provider - return metadata - - -def _provider_from_processor(processor: Any) -> str | None: - module = getattr(type(processor), "__module__", "") - return _provider_from_name(module) - - -def _provider_from_name(name: str) -> str | None: - lowered = name.lower() - providers = { - "openai": "openai", - "anthropic": "anthropic", - "google": "google", - "gemini": "google", - "mistral": "mistral", - "cohere": "cohere", - "bedrock": "bedrock", - "aws": "bedrock", - "openrouter": "openrouter", - } - for needle, provider in providers.items(): - if needle in lowered: - return provider - return None - - def _processor_name(processor: Any) -> str | None: name = getattr(processor, "name", None) if isinstance(name, str): @@ -811,27 +763,6 @@ def _filter_llm_metadata(metadata: dict[str, Any]) -> dict[str, Any]: return {key: value for key, value in metadata.items() if key in _ALLOWED_LLM_METADATA_FIELDS and value is not None} -def _llm_usage_metrics(usage: Any) -> dict[str, Any]: - metrics: dict[str, Any] = {} - prompt_tokens = getattr(usage, "prompt_tokens", None) - completion_tokens = getattr(usage, "completion_tokens", None) - total_tokens = getattr(usage, "total_tokens", None) - cache_read = getattr(usage, "cache_read_input_tokens", None) - cache_creation = getattr(usage, "cache_creation_input_tokens", None) - reasoning_tokens = getattr(usage, "reasoning_tokens", None) - for key, value in ( - ("prompt_tokens", prompt_tokens), - ("completion_tokens", completion_tokens), - ("tokens", total_tokens), - ("cache_read_input_tokens", cache_read), - ("cache_creation_input_tokens", cache_creation), - ("reasoning_tokens", reasoning_tokens), - ): - if isinstance(value, (int, float)) and not isinstance(value, bool): - metrics[key] = value - return metrics - - def _tool_call_from_pipecat_call(call: Any) -> dict[str, Any] | None: name = getattr(call, "function_name", None) tool_call_id = getattr(call, "tool_call_id", None) diff --git a/py/src/braintrust/integrations/pipecat/ttfb.py b/py/src/braintrust/integrations/pipecat/ttfb.py index 444af4ade..eb28b7faa 100644 --- a/py/src/braintrust/integrations/pipecat/ttfb.py +++ b/py/src/braintrust/integrations/pipecat/ttfb.py @@ -19,17 +19,23 @@ def end(self, key): def clear(self): self.active.clear() - def capture(self, metric, source): + def owner(self, metric, source, *, operation=None): name = getattr(metric, "processor", None) matches = [ (key, log, state) for key, (processor, log, state) in self.active.items() - if processor is source and name is not None and getattr(processor, "name", None) == name + if processor is source + and name is not None + and getattr(processor, "name", None) == name + and (operation is None or key == operation) ] if len(matches) == 1: - key, log, state = matches[0] - else: - key, log, state = None, self.fallback, self.unmatched + return matches[0] + return None, self.fallback, self.unmatched + + def capture(self, metric, source): + key, log, state = self.owner(metric, source) + name = getattr(metric, "processor", None) values = state.setdefault("values", []) if len(values) >= 32: state["omitted"] = state.get("omitted", 0) + 1 diff --git a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py index 0d57d4157..f163505a1 100644 --- a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py +++ b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py @@ -16,6 +16,7 @@ from braintrust.audio.worker import RecordingBusy, encode_in_worker from pipecat.observers.base_observer import BaseObserver # pylint: disable=import-error +from ..llm_metrics import _llm_usage_metrics, _metadata_from_metric, _metadata_from_processor from ..ttfb import TTFBRouter from ..turn_metrics import TURN_METRIC_TYPES, log_turn_metric from .turns import Turns @@ -302,7 +303,21 @@ async def on_push_frame(self, data): remaining = [] for metric in frame.data: if type(metric).__name__ == "TTFBMetricsData": - self.ttfb.capture(metric, data.source) + owner = self.ttfb.capture(metric, data.source) + if owner == "llm": + self.llm.log(metrics={"time_to_first_token": metric.value}) + elif ( + type(metric).__name__ == "LLMUsageMetricsData" + and self.ttfb.owner(metric, data.source, operation="llm")[0] == "llm" + ): + self.llm.log( + metrics=_llm_usage_metrics(metric.value), + metadata={ + "pipecat.usage": native_value(metric), + **_metadata_from_processor(data.source), + **_metadata_from_metric(metric), + }, + ) else: remaining.append(native_value(metric)) if not remaining: @@ -404,6 +419,7 @@ async def on_push_frame(self, data): metadata={ **self.turns.metadata(self.llm_turn), "braintrust.continuation.tool_call_ids": self.context_tool_results, + **_metadata_from_processor(data.source), }, set_current=False, internal={"instrumentation": "pipecat-auto"}, diff --git a/py/src/braintrust/integrations/pipecat/voice/test_recorded_realtime.py b/py/src/braintrust/integrations/pipecat/voice/test_recorded_realtime.py index 5c2b2c3b7..280175645 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_recorded_realtime.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_recorded_realtime.py @@ -39,17 +39,21 @@ async def output(frame, direction=FrameDirection.DOWNSTREAM): ) ) + service._setup = SimpleNamespace(enable_usage_metrics=True, enable_metrics=False) service.push_frame = output RealtimeCapture(observer, service, pair.user()) calls = [] for response in responses: span = observer.root.start_span(name="pipecat.llm_response") observer.llm = span + observer.ttfb.start("llm", service, span.log) event = events.ResponseDone( type="response.done", event_id="fixture-envelope", response=events.Response.model_validate(response) ) await service._handle_evt_response_done(event) metadata = {key: value for row in span.rows for key, value in row.get("metadata", {}).items()} + standard_metrics = {key: value for row in span.rows for key, value in row.get("metrics", {}).items()} + assert standard_metrics["tokens"] == response["usage"]["total_tokens"] assert metadata["openai.response.id"] == response["id"] assert metadata["openai.response"]["usage"]["total_tokens"] == response["usage"]["total_tokens"] for row in span.rows: diff --git a/py/src/braintrust/integrations/pipecat/voice/test_turns.py b/py/src/braintrust/integrations/pipecat/voice/test_turns.py index 3dec84a2d..49c8a724f 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_turns.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_turns.py @@ -226,3 +226,35 @@ async def test_ttfb_routes_only_to_unambiguous_processor_operation(self): await self.push(MetricsFrame(data=[TTFBMetricsData(processor="tts", value=0.13)]), tts) assert self.observer.root.rows[-1]["metadata"]["pipecat.ttfb"][-1]["value"] == 0.13 await self.observer.finish() + + async def test_native_llm_standard_metrics_preserve_native_usage(self): + from pipecat.frames.frames import MetricsFrame # pylint: disable=import-error + from pipecat.metrics.metrics import ( # pylint: disable=import-error + LLMTokenUsage, + LLMUsageMetricsData, + TTFBMetricsData, + ) + + processor = SimpleNamespace(name="OpenAILLMService#0", model="gpt-4o-mini") + await self.push(LLMFullResponseStartFrame(), processor) + span = self.observer.llm + usage = LLMUsageMetricsData( + processor=processor.name, + model=processor.model, + value=LLMTokenUsage(prompt_tokens=10, completion_tokens=3, total_tokens=13), + ) + await self.push(MetricsFrame(data=[TTFBMetricsData(processor=processor.name, value=0.2)]), processor) + await self.push(TTSStartedFrame(context_id="speech"), processor) + await self.push(MetricsFrame(data=[usage]), processor) + await self.push(MetricsFrame(data=[TTFBMetricsData(processor="tts", value=0.9)]), SimpleNamespace(name="tts")) + await self.push(LLMFullResponseEndFrame(), processor) + metrics = {key: value for row in span.rows for key, value in row.get("metrics", {}).items()} + metadata = {key: value for row in span.rows for key, value in row.get("metadata", {}).items()} + assert metrics["prompt_tokens"] == 10 + assert metrics["completion_tokens"] == 3 + assert metrics["tokens"] == 13 + assert metrics["time_to_first_token"] == 0.2 + assert metadata["model"] == "gpt-4o-mini" + assert metadata["provider"] == "openai" + assert metadata["pipecat.usage"]["value"]["total_tokens"] == 13 + await self.observer.finish() From 7d0dfd0d7dc8c8ca41c9c643cf51c065cd8fb5e5 Mon Sep 17 00:00:00 2001 From: David Elner Date: Thu, 1 Oct 2026 11:20:48 -0400 Subject: [PATCH 06/18] Fixed: timing-dependent Pipecat attachment test --- .../integrations/pipecat/voice/test_user_capture.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py b/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py index 19006931a..577a2de79 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py @@ -97,11 +97,12 @@ async def test_native_consumption_attributes_earlier_frame_and_clip_to_later_tur self, ): observer, capture, turn = await self.exercise(True) - with patch("braintrust.audio.attachments.RecordingAttachment", side_effect=lambda **kwargs: kwargs): - await observer.finish() + # Force progressive export to complete before final cleanup. + await asyncio.gather(*tuple(capture.tasks)) + await observer.finish() payload = next(row["input"] for row in turn["span"].rows if isinstance(row.get("input"), list)) self.assertEqual(payload[0]["content"][0]["text"], "Where is my order?") - decoded, rate = sf.read(io.BytesIO(payload[0]["content"][1]["file"]["file_data"]["data"])) + decoded, rate = sf.read(io.BytesIO(payload[0]["content"][1]["file"]["file_data"].data)) self.assertAlmostEqual(len(decoded) / rate, 0.1) self.assertGreater(abs(decoded).max(), 0.01) self.assertEqual(capture.bytes, 0) From 4308a3ebce4f3eb1d8ee4ee1f59684fa3f0ef003 Mon Sep 17 00:00:00 2001 From: David Elner Date: Thu, 1 Oct 2026 18:50:53 -0400 Subject: [PATCH 07/18] Fixed: Missing llm_tool_calls in Pipecat --- .../pipecat/voice/instrumentation.py | 31 ++++++++++--------- .../integrations/pipecat/voice/realtime.py | 12 ++++++- .../pipecat/voice/test_recorded_realtime.py | 9 ++++-- .../integrations/pipecat/voice/test_turns.py | 4 +++ 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py index f163505a1..ee70bff52 100644 --- a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py +++ b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py @@ -155,6 +155,7 @@ def __init__( self.duplicate_events = 0 self.clock_anchored = False self.llm = None + self.llm_tool_calls = [] self.llm_text = [] self.user = None self.tools = {} @@ -407,6 +408,7 @@ async def on_push_frame(self, data): self.context_tool_results = self.realtime.pending_tool_results[:] self.realtime.pending_tool_results.clear() self.result_origins.clear() + self.llm_tool_calls = [] self.llm_text = [] # A new model operation may arrive before the previous aggregator # callback task finishes. Keep each native response lifecycle distinct. @@ -428,18 +430,16 @@ async def on_push_frame(self, data): elif kind == "LLMTextFrame" and self.llm: self.llm_text.append(frame.text) elif kind == "FunctionCallsStartedFrame": + self.llm_tool_calls = [ + { + "id": call.tool_call_id, + "type": "function", + "function": {"name": call.function_name, "arguments": json.dumps(native_value(call.arguments))}, + } + for call in frame.function_calls + ] if self.llm_turn: - self.llm_turn["tool_calls"] = [ - { - "id": call.tool_call_id, - "type": "function", - "function": { - "name": call.function_name, - "arguments": json.dumps(native_value(call.arguments)), - }, - } - for call in frame.function_calls - ] + self.llm_turn["tool_calls"] = list(self.llm_tool_calls) self.turns.log_message(self.llm_turn, "") for call in frame.function_calls: self.requests.setdefault( @@ -491,10 +491,11 @@ async def on_push_frame(self, data): tool.log(metadata={"pipecat.cancelled": True}) tool.end() elif kind == "LLMFullResponseEndFrame" and self.llm: - self.llm.log( - output=[{"role": "assistant", "content": "".join(self.llm_text)}], - metadata={"pipecat.text": "".join(self.llm_text)}, - ) + text = "".join(self.llm_text) + message = {"role": "assistant", "content": text or None} + if self.llm_tool_calls: + message["tool_calls"] = list(self.llm_tool_calls) + self.llm.log(output=[message], metadata={"pipecat.text": text} if text else {}) self.ttfb.end("llm") self.llm.end() self.llm = None diff --git a/py/src/braintrust/integrations/pipecat/voice/realtime.py b/py/src/braintrust/integrations/pipecat/voice/realtime.py index 3ed87350a..a3c10f471 100644 --- a/py/src/braintrust/integrations/pipecat/voice/realtime.py +++ b/py/src/braintrust/integrations/pipecat/voice/realtime.py @@ -48,6 +48,7 @@ async def item_added(evt): observer.context_reply_to = turn["span"].span_id if turn else None observer.llm_turn = None observer.llm_text = [] + observer.llm_tool_calls = [] observer.llm = observer.root.start_span( name="pipecat.llm_response", type="llm", @@ -90,6 +91,15 @@ async def response(evt, original=original): response_span.log(metadata={"openai.response.id": response_id}) if getattr(evt, "response", None) is not None: response_span.log(metadata={"openai.response": native_value(evt.response)}) + observer.llm_tool_calls = [ + { + "id": item.call_id, + "type": "function", + "function": {"name": item.name, "arguments": item.arguments}, + } + for item in evt.response.output + if item.type == "function_call" + ] try: result = await original(evt) if response_span and getattr(evt, "response", None) is not None: @@ -99,7 +109,7 @@ async def response(evt, original=original): output=[ { "role": "assistant", - "content": "", + "content": "".join(observer.llm_text) or None, "tool_calls": [ { "id": item.call_id, diff --git a/py/src/braintrust/integrations/pipecat/voice/test_recorded_realtime.py b/py/src/braintrust/integrations/pipecat/voice/test_recorded_realtime.py index 280175645..0b796630d 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_recorded_realtime.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_recorded_realtime.py @@ -56,9 +56,12 @@ async def output(frame, direction=FrameDirection.DOWNSTREAM): assert standard_metrics["tokens"] == response["usage"]["total_tokens"] assert metadata["openai.response.id"] == response["id"] assert metadata["openai.response"]["usage"]["total_tokens"] == response["usage"]["total_tokens"] - for row in span.rows: - for message in row.get("output", []): - calls.extend(message.get("tool_calls", [])) + final_output = [row["output"] for row in span.rows if "output" in row][-1] + for message in final_output: + calls.extend(message.get("tool_calls", [])) + if any(item["type"] == "function_call" for item in response["output"]): + assert final_output[0]["content"] is None + assert metadata.get("pipecat.text") != "" assert len(calls) == 1 assert calls[0]["function"]["name"] == "lookup_order" assert "1042" in calls[0]["function"]["arguments"] diff --git a/py/src/braintrust/integrations/pipecat/voice/test_turns.py b/py/src/braintrust/integrations/pipecat/voice/test_turns.py index 49c8a724f..f7c3e0a0d 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_turns.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_turns.py @@ -81,6 +81,10 @@ async def test_tool_continuation_preserves_origin_after_next_user_starts(self): ) tool = observer.tools["call-1"] await self.push(LLMFullResponseEndFrame()) + final_output = [row["output"] for row in first_model.rows if "output" in row][-1] + self.assertEqual(final_output[0]["tool_calls"][0]["id"], "call-1") + self.assertIsNone(final_output[0]["content"]) + self.assertFalse(any(row.get("metadata", {}).get("pipecat.text") == "" for row in first_model.rows)) observer.turns.stop("assistant", SimpleNamespace(content="", timestamp="t2", interrupted=False)) await self.push(UserStartedSpeakingFrame()) await self.push( From 30bfb41579b0c3fa6e46f32546b35ea1b26f21db Mon Sep 17 00:00:00 2001 From: David Elner Date: Thu, 1 Oct 2026 21:46:23 -0400 Subject: [PATCH 08/18] Changed: Pipecat instrumentation fields to generic names --- .../integrations/pipecat/voice/README.md | 2 +- .../pipecat/voice/instrumentation.py | 11 +++-- .../integrations/pipecat/voice/realtime.py | 7 ++- .../pipecat/voice/test_instrumentation.py | 1 + .../pipecat/voice/test_realtime.py | 47 ++++++++++++++++++- .../pipecat/voice/test_recorded_realtime.py | 2 +- .../integrations/pipecat/voice/test_turns.py | 15 ++++-- .../integrations/pipecat/voice/turns.py | 10 ++-- .../pipecat/voice/user_capture.py | 6 ++- 9 files changed, 79 insertions(+), 22 deletions(-) diff --git a/py/src/braintrust/integrations/pipecat/voice/README.md b/py/src/braintrust/integrations/pipecat/voice/README.md index 39073b81e..7aa9b9f7a 100644 --- a/py/src/braintrust/integrations/pipecat/voice/README.md +++ b/py/src/braintrust/integrations/pipecat/voice/README.md @@ -18,7 +18,7 @@ Install `braintrust[audio]` for Ogg/Opus or WAV encoding. Audio defaults off. Ex |---|---| | Native voice path | Pipecat 1.12.0, one input/output transport, universal user/assistant aggregators, segmented STT or OpenAI Realtime. Other pipeline shapes/versions retain existing frame tracing. | | Public interfaces | `setup_pipecat`, automatic integration setup, `wrap_pipeline_worker`, and an explicitly supplied `BraintrustPipecatObserver` share the same injection path. `trace_turns=False` retains the existing observer behavior. | -| Trace | Native aggregator turns, actual STT operations, model responses, tool executions, and generated-audio lifecycles. Tool-only realtime responses can be pipeline children. | +| Trace | `user_turn`, `assistant_turn`, `stt`, `llm_response`, `tts`, and actual tool names retain native boundaries and metadata. Realtime audio uses `pipecat.audio_output`; tool-only model responses can be pipeline children. `turn.id` / `turn.reply_to` and `continuation.tool_call_ids` preserve observed associations. | | Audio | Combined stereo call; segmented-STT caller clips on user turns; generated clips on TTS spans. Selections reference successfully captured samples. | | TTFB | Native `pipecat.ttfb` measurements follow the emitting processor and a unique active operation. STT measurements follow `run_stt` into its eventual span. Ambiguous/unmatched metrics remain on the pipeline; arrays are bounded to 32 with omission counts. | | Turn detection | Native analyzer predictions in `pipecat.turn_metrics` on the user turn; preserve processor, metric class, confidence, completion and reported milliseconds. Retain 32 predictions and count omissions. Unassociated observations remain on the pipeline. | diff --git a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py index ee70bff52..803222a56 100644 --- a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py +++ b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py @@ -415,12 +415,12 @@ async def on_push_frame(self, data): self.turns.assistant = None self.llm_turn = self.turns.start("assistant", kind, self.context_reply_to) self.llm = self.llm_turn["span"].start_span( - name="pipecat.llm_response", + name="llm_response", type="llm", input=self.last_context, metadata={ **self.turns.metadata(self.llm_turn), - "braintrust.continuation.tool_call_ids": self.context_tool_results, + "continuation.tool_call_ids": self.context_tool_results, **_metadata_from_processor(data.source), }, set_current=False, @@ -447,7 +447,7 @@ async def on_push_frame(self, data): ( self.llm or self.root, self.turns.metadata(self.llm_turn) - or ({"braintrust.turn.reply_to": self.context_reply_to} if self.context_reply_to else {}), + or ({"turn.reply_to": self.context_reply_to} if self.context_reply_to else {}), ), ) if self.llm: @@ -480,7 +480,7 @@ async def on_push_frame(self, data): if kind == "FunctionCallResultFrame": request = self.requests.pop(frame.tool_call_id, None) if request: - self.result_origins[frame.tool_call_id] = request[1].get("braintrust.turn.reply_to") + self.result_origins[frame.tool_call_id] = request[1].get("turn.reply_to") tool.log( output=native_value(frame.result), metadata={"pipecat.result": native_value(frame.result)}, @@ -505,13 +505,14 @@ async def on_push_frame(self, data): return turn = self.turns.assistant or self.turns.start("assistant", kind) span = turn["span"].start_span( - name="pipecat.tts_response", + name="pipecat.audio_output" if self.realtime else "tts", type="task", metadata={ **self.turns.metadata(turn), "pipecat.context_id": getattr(frame, "context_id", None), **({"openai.response.id": context} if self.realtime and context else {}), "pipecat.append_to_context": frame.append_to_context, + **_metadata_from_processor(data.source), }, set_current=False, internal={"instrumentation": "pipecat-auto"}, diff --git a/py/src/braintrust/integrations/pipecat/voice/realtime.py b/py/src/braintrust/integrations/pipecat/voice/realtime.py index a3c10f471..a857ba277 100644 --- a/py/src/braintrust/integrations/pipecat/voice/realtime.py +++ b/py/src/braintrust/integrations/pipecat/voice/realtime.py @@ -6,6 +6,7 @@ from contextvars import ContextVar +from ..llm_metrics import _metadata_from_processor from .instrumentation import native_value @@ -50,16 +51,18 @@ async def item_added(evt): observer.llm_text = [] observer.llm_tool_calls = [] observer.llm = observer.root.start_span( - name="pipecat.llm_response", + name="llm_response", type="llm", set_current=False, internal={"instrumentation": "pipecat-auto"}, metadata={ + **_metadata_from_processor(service), "openai.item_id": evt.item.id, "openai.call_id": evt.item.call_id, - "braintrust.turn.reply_to": observer.context_reply_to, + "turn.reply_to": observer.context_reply_to, }, ) + observer.ttfb.start("llm", service, observer.llm.log) return await original_item(evt) observer.hooks.set(service, "_handle_evt_conversation_item_added", item_added) diff --git a/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py b/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py index 02c38fc6e..b5a989ff8 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py @@ -63,6 +63,7 @@ async def push(frame, first_push=True): await push(frame) await push(TTSStoppedFrame(context_id="c1")) await observer.finish() + self.assertEqual(observer.recordings[0]["span"].rows[0]["name"], "tts") recording = observer.recordings[0]["span"].rows[-1]["metadata"]["audio.recordings"][0] self.assertEqual(recording["state"], "omitted") self.assertEqual(observer.audio_bytes, 0) diff --git a/py/src/braintrust/integrations/pipecat/voice/test_realtime.py b/py/src/braintrust/integrations/pipecat/voice/test_realtime.py index 3c78e5204..ddb323917 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_realtime.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_realtime.py @@ -3,7 +3,8 @@ import unittest from types import SimpleNamespace -from pipecat.frames.frames import InputAudioRawFrame # pylint: disable=import-error +from pipecat.frames.frames import InputAudioRawFrame, TTSStartedFrame, TTSStoppedFrame # pylint: disable=import-error +from pipecat.processors.frame_processor import FrameDirection # pylint: disable=import-error from .instrumentation import NativeObserver from .realtime import RealtimeCapture @@ -39,7 +40,11 @@ async def send_audio(frame): service.send_client_event, service._send_user_audio = send_event, send_audio aggregator = SimpleNamespace( - _handle_transcription=noop, _push_aggregation=noop, reset=noop, add_event_handler=lambda *args: None + _handle_transcription=noop, + _push_aggregation=noop, + reset=noop, + add_event_handler=lambda *args: None, + remove_event_handler=lambda *args: None, ) return service, aggregator @@ -72,3 +77,41 @@ async def test_disabled_does_not_wrap_connect_or_audio_send(self): await service._send_user_audio(InputAudioRawFrame(b"\0\0" * 480, 24000, 1)) self.assertEqual(capture.sent_ranges, []) self.assertEqual(observer.call_recording.bytes, 0) + + async def test_realtime_audio_is_not_a_synthesis_request(self): + observer = NativeObserver(Span(), retain_audio=False) + service, aggregator = self.service() + service.name = "realtime" + observer.realtime = RealtimeCapture(observer, service, aggregator) + for frame in (TTSStartedFrame(context_id="response-1"), TTSStoppedFrame(context_id="response-1")): + await observer.on_push_frame( + SimpleNamespace( + frame=frame, + first_push=True, + source=service, + destination=SimpleNamespace(name="sink"), + direction=FrameDirection.DOWNSTREAM, + timestamp=0, + ) + ) + span = observer.recordings[0]["span"] + self.assertEqual(span.rows[0]["name"], "pipecat.audio_output") + self.assertEqual(span.rows[0]["metadata"]["pipecat.context_id"], "response-1") + self.assertEqual(span.rows[0]["metadata"]["openai.response.id"], "response-1") + await observer.finish() + + async def test_tool_only_response_registers_service_metrics(self): + observer = NativeObserver(Span(), retain_audio=False) + service, aggregator = self.service() + service.name = "OpenAIRealtimeLLMService#0" + service.model = "gpt-realtime" + RealtimeCapture(observer, service, aggregator) + await service._handle_evt_conversation_item_added( + SimpleNamespace(item=SimpleNamespace(type="function_call", id="item-1", call_id="call-1")) + ) + self.assertEqual(observer.llm.rows[0]["name"], "llm_response") + self.assertEqual(observer.llm.rows[0]["metadata"]["model"], "gpt-realtime") + self.assertEqual(observer.llm.rows[0]["metadata"]["openai.call_id"], "call-1") + metric = SimpleNamespace(processor=service.name) + self.assertEqual(observer.ttfb.owner(metric, service, operation="llm")[0], "llm") + await observer.finish() diff --git a/py/src/braintrust/integrations/pipecat/voice/test_recorded_realtime.py b/py/src/braintrust/integrations/pipecat/voice/test_recorded_realtime.py index 0b796630d..4d6aab4e9 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_recorded_realtime.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_recorded_realtime.py @@ -44,7 +44,7 @@ async def output(frame, direction=FrameDirection.DOWNSTREAM): RealtimeCapture(observer, service, pair.user()) calls = [] for response in responses: - span = observer.root.start_span(name="pipecat.llm_response") + span = observer.root.start_span(name="llm_response") observer.llm = span observer.ttfb.start("llm", service, span.log) event = events.ResponseDone( diff --git a/py/src/braintrust/integrations/pipecat/voice/test_turns.py b/py/src/braintrust/integrations/pipecat/voice/test_turns.py index f7c3e0a0d..c535aee77 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_turns.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_turns.py @@ -82,6 +82,11 @@ async def test_tool_continuation_preserves_origin_after_next_user_starts(self): tool = observer.tools["call-1"] await self.push(LLMFullResponseEndFrame()) final_output = [row["output"] for row in first_model.rows if "output" in row][-1] + self.assertEqual(first_user["span"].rows[0]["name"], "user_turn") + self.assertEqual(first_assistant["span"].rows[0]["name"], "assistant_turn") + self.assertEqual(first_model.rows[0]["name"], "llm_response") + self.assertIn(tool, first_model.children) + self.assertEqual(tool.rows[0]["metadata"]["pipecat.tool_call_id"], "call-1") self.assertEqual(final_output[0]["tool_calls"][0]["id"], "call-1") self.assertIsNone(final_output[0]["content"]) self.assertFalse(any(row.get("metadata", {}).get("pipecat.text") == "" for row in first_model.rows)) @@ -102,16 +107,16 @@ async def test_tool_continuation_preserves_origin_after_next_user_starts(self): assert continuation is not None self.assertIsNot(continuation, first_assistant) self.assertEqual( - continuation["metadata"]["braintrust.turn.reply_to"], + continuation["metadata"]["turn.reply_to"], first_user["span"].span_id, ) self.assertIn(tool, first_model.children) self.assertEqual( - tool.rows[0]["metadata"]["braintrust.turn.id"], + tool.rows[0]["metadata"]["turn.id"], first_assistant["span"].span_id, ) self.assertEqual( - observer.llm.rows[0]["metadata"]["braintrust.continuation.tool_call_ids"], + observer.llm.rows[0]["metadata"]["continuation.tool_call_ids"], ["call-1"], ) await self.push(TTSStartedFrame(context_id="tts-1")) @@ -157,7 +162,7 @@ async def test_delayed_callback_ends_its_turn_not_new_response(self): self.observer.turns.confirm("assistant") await self.observer.finish() self.assertTrue(second["ended"]) - self.assertTrue(second["span"].rows[-1]["metadata"]["braintrust.turn.incomplete"]) + self.assertTrue(second["span"].rows[-1]["metadata"]["turn.incomplete"]) async def test_interruption_ends_tts_and_transcription_does_not_guess_a_turn(self): await self.push(UserStartedSpeakingFrame()) @@ -165,7 +170,7 @@ async def test_interruption_ends_tts_and_transcription_does_not_guess_a_turn(sel await self.push(TranscriptionFrame(text="new question", user_id="", timestamp="t2")) transcription = self.observer.root.children[-1] self.assertEqual(transcription.rows[0]["name"], "pipecat.stt_transcription") - self.assertNotIn("braintrust.turn.id", transcription.rows[0]["metadata"]) + self.assertNotIn("turn.id", transcription.rows[0]["metadata"]) await self.push(TTSStartedFrame(context_id="interrupted")) state = self.observer.tts["interrupted"] await self.push(InterruptionFrame()) diff --git a/py/src/braintrust/integrations/pipecat/voice/turns.py b/py/src/braintrust/integrations/pipecat/voice/turns.py index 7c9516e99..f3e8f18c8 100644 --- a/py/src/braintrust/integrations/pipecat/voice/turns.py +++ b/py/src/braintrust/integrations/pipecat/voice/turns.py @@ -24,19 +24,19 @@ def start(self, role, trigger, reply_to=None): return current state = { "span": self.root.start_span( - name=f"pipecat.{role}_turn", + name=f"{role}_turn", type="task", set_current=False, internal={"instrumentation": "pipecat-auto"}, - metadata={"braintrust.turn.start_observation": trigger}, + metadata={"pipecat.turn.start_observation": trigger}, ), "role": role, "confirmed": False, "ended": False, } - state["metadata"] = {"braintrust.turn.id": state["span"].span_id} + state["metadata"] = {"turn.id": state["span"].span_id} if role == "assistant" and reply_to: - state["metadata"]["braintrust.turn.reply_to"] = reply_to + state["metadata"]["turn.reply_to"] = reply_to state["span"].log(metadata=state["metadata"]) setattr(self, role, state) self.pending[role].append(state) @@ -129,6 +129,6 @@ def stop(self, role, message, strategy=None): def finish(self): for state in self.states: if not state["ended"]: - state["span"].log(metadata={"braintrust.turn.incomplete": True}) + state["span"].log(metadata={"turn.incomplete": True}) state["span"].end() state["ended"] = True diff --git a/py/src/braintrust/integrations/pipecat/voice/user_capture.py b/py/src/braintrust/integrations/pipecat/voice/user_capture.py index 001f45f5e..05da5d590 100644 --- a/py/src/braintrust/integrations/pipecat/voice/user_capture.py +++ b/py/src/braintrust/integrations/pipecat/voice/user_capture.py @@ -16,6 +16,8 @@ from braintrust.audio.worker import RecordingBusy, encode_in_worker from pipecat.frames.frames import TranscriptionFrame # pylint: disable=import-error +from ..llm_metrics import _metadata_from_processor + def encode_segments(segments, audio_format): chunks = [] @@ -96,6 +98,7 @@ async def speech_stop(frame): async def run(audio): queued = self.segment_boundaries.popleft() if self.segment_boundaries else {"events": [], "ranges": []} segment = { + "service_metadata": _metadata_from_processor(stt), "boundaries": queued["events"], "ranges": queued["ranges"], "audio": None, @@ -224,12 +227,13 @@ def create_stt(self, segment, owner, turn=None): metadata = { "pipecat.transcriptions": segment["frames"], "pipecat.function": "run_stt", + **segment.get("service_metadata", {}), **segment.get("ttfb_metadata", {}), } if turn: metadata.update(self.observer.turns.metadata(turn)) span = owner.start_span( - name="pipecat.stt", + name="stt", type="task", start_time=segment["start"], set_current=False, From 20f1011fac8248e7df4b3995a68a81e15c8b57db Mon Sep 17 00:00:00 2001 From: David Elner Date: Mon, 5 Oct 2026 22:12:20 -0400 Subject: [PATCH 09/18] Changed: Pipecat tests to use VCR and recording behavior during shutdown --- py/src/braintrust/audio/recording.py | 6 +- py/src/braintrust/audio/test_segments.py | 32 + .../pipecat/_test_audio_cassettes.py | 198 + .../integrations/pipecat/_test_websocket.py | 127 + .../request-0-2.wav | Bin 0 -> 95614 bytes .../response-3.wav | Bin 0 -> 103244 bytes .../test_cascade_voice_conversation.yaml | 431 ++ .../agent-000.wav | Bin 0 -> 98444 bytes .../agent-001.wav | Bin 0 -> 105644 bytes .../caller.wav | Bin 0 -> 166844 bytes .../test_realtime_voice_conversation.json | 4108 +++++++++++++++++ .../pipecat/test_audio_cassettes.py | 120 + .../integrations/pipecat/test_pipecat.py | 294 +- .../pipecat/test_voice_pipeline.py | 393 ++ .../pipecat/test_websocket_cassette.py | 51 + .../pipecat/voice/fixtures/README.md | 21 +- .../pipecat/voice/fixtures/order.wav | Bin 0 -> 83666 bytes .../voice/fixtures/realtime-responses.json | 165 - .../pipecat/voice/test_instrumentation.py | 17 +- .../pipecat/voice/test_realtime.py | 41 +- .../pipecat/voice/test_recorded_realtime.py | 68 - .../integrations/pipecat/voice/test_turns.py | 44 +- .../pipecat/voice/test_user_capture.py | 16 - 23 files changed, 5608 insertions(+), 524 deletions(-) create mode 100644 py/src/braintrust/integrations/pipecat/_test_audio_cassettes.py create mode 100644 py/src/braintrust/integrations/pipecat/_test_websocket.py create mode 100644 py/src/braintrust/integrations/pipecat/cassettes/latest/test_cascade_voice_conversation.audio/request-0-2.wav create mode 100644 py/src/braintrust/integrations/pipecat/cassettes/latest/test_cascade_voice_conversation.audio/response-3.wav create mode 100644 py/src/braintrust/integrations/pipecat/cassettes/latest/test_cascade_voice_conversation.yaml create mode 100644 py/src/braintrust/integrations/pipecat/cassettes/latest/test_realtime_voice_conversation.audio/agent-000.wav create mode 100644 py/src/braintrust/integrations/pipecat/cassettes/latest/test_realtime_voice_conversation.audio/agent-001.wav create mode 100644 py/src/braintrust/integrations/pipecat/cassettes/latest/test_realtime_voice_conversation.audio/caller.wav create mode 100644 py/src/braintrust/integrations/pipecat/cassettes/latest/test_realtime_voice_conversation.json create mode 100644 py/src/braintrust/integrations/pipecat/test_audio_cassettes.py create mode 100644 py/src/braintrust/integrations/pipecat/test_voice_pipeline.py create mode 100644 py/src/braintrust/integrations/pipecat/test_websocket_cassette.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/fixtures/order.wav delete mode 100644 py/src/braintrust/integrations/pipecat/voice/fixtures/realtime-responses.json delete mode 100644 py/src/braintrust/integrations/pipecat/voice/test_recorded_realtime.py diff --git a/py/src/braintrust/audio/recording.py b/py/src/braintrust/audio/recording.py index 3facf4908..361cffb28 100644 --- a/py/src/braintrust/audio/recording.py +++ b/py/src/braintrust/audio/recording.py @@ -118,9 +118,9 @@ def capture(self, channel, pcm, sample_rate, channels, *, observed_ns=None, obse else max((observed_ns - self.origin_ns) / 1e6, self.ends[channel]) ) duration = len(pcm) / 2 / channels / sample_rate * 1000 - if duration > 1000: - self.omit("frame_size_limit") - elif start + duration > self.max_duration_ms: + # Transport frames can contain seconds of audio (e.g. shutdown silence). + # Bound retained bytes and timeline duration, not the transport's packetization. + if start + duration > self.max_duration_ms: self.omit("duration_limit") elif self.bytes + len(pcm) > self.max_bytes: self.omit("capture_byte_limit") diff --git a/py/src/braintrust/audio/test_segments.py b/py/src/braintrust/audio/test_segments.py index a1d29276a..148921839 100644 --- a/py/src/braintrust/audio/test_segments.py +++ b/py/src/braintrust/audio/test_segments.py @@ -38,6 +38,38 @@ async def publish(segment, encoded): position += len(decoded) / rate * 1000 self.assertAlmostEqual(position, 1800) + async def test_large_frame_crosses_rotation_without_losing_samples(self): + files = [] + + async def publish(segment, encoded): + files.append((segment.start_ms, encoded)) + + recorder = SegmentedRecording( + options=RecordingOptions(segment_duration_seconds=1), on_segment=publish, audio_format="wav" + ) + # One native write contains three seconds. Subsequent writes advance + # the export watermark across it; rotation must preserve every sample. + audio = np.arange(24000 * 3, dtype=np.int16) + recorder.capture(1, audio.tobytes(), 24000, 1, observed_ns=0) + for index in range(4): + recorder.capture(1, b"\0\0" * 24000, 24000, 1, observed_ns=(3 + index) * 1_000_000_000) + if recorder.pending: + await asyncio.gather(*tuple(recorder.pending)) + self.assertTrue(files, "rotation must export before shutdown") + await recorder.finish() + pieces = [] + position = 0 + for start, encoded in files: + self.assertAlmostEqual(start, position) + samples, rate = sf.read(io.BytesIO(encoded["data"]), dtype="int16", always_2d=True) + pieces.append(samples[:, 1]) + position += len(samples) / rate * 1000 + np.testing.assert_array_equal( + np.concatenate(pieces), np.concatenate([audio, np.zeros(24000 * 4, dtype=np.int16)]) + ) + self.assertIsNone(recorder.reason) + self.assertEqual(recorder.retained_bytes, 0) + async def test_duration_limit_preserves_partial_audio(self): files = [] diff --git a/py/src/braintrust/integrations/pipecat/_test_audio_cassettes.py b/py/src/braintrust/integrations/pipecat/_test_audio_cassettes.py new file mode 100644 index 000000000..e379fd012 --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/_test_audio_cassettes.py @@ -0,0 +1,198 @@ +"""Lossless audio sidecars for test cassettes; never used by SDK instrumentation.""" + +import base64 +import copy +import shutil +import wave +from contextlib import contextmanager +from email.parser import BytesParser +from email.policy import default +from pathlib import Path +from tempfile import TemporaryDirectory + +from vcr.persisters.filesystem import CassetteNotFoundError +from vcr.serialize import deserialize, serialize + + +def write_audio(path, data, *, pcm=False): + path.parent.mkdir(parents=True, exist_ok=True) + if pcm: + # These OpenAI speech/realtime fixtures use mono 24 kHz PCM16. + with wave.Wave_write(str(path)) as audio: + audio.setparams((1, 2, 24000, 0, "NONE", "not compressed")) + audio.writeframes(data) + else: + path.write_bytes(data) + + +def read_audio(directory, reference): + path = directory / reference["audio_file"] + with wave.open(str(path)) as audio: + if reference.get("pcm"): + assert (audio.getnchannels(), audio.getsampwidth(), audio.getframerate()) == (1, 2, 24000) + expected_bytes = audio.getnframes() * audio.getnchannels() * audio.getsampwidth() + data = audio.readframes(audio.getnframes()) + assert len(data) == expected_bytes, "Truncated WAV fixture" + if not reference.get("pcm"): + data = path.read_bytes() + start = reference.get("offset_bytes", 0) + length = reference.get("length_bytes", len(data)) + assert 0 <= start <= start + length <= len(data), "Audio reference exceeds fixture file" + return data[start : start + length] + + +def publish_cassette(staged, target): + """Replace a complete fixture set; restore old audio if manifest replacement fails. + + Re-recording is sequential. This is rollback for ordinary I/O failures, + not a filesystem transaction resilient to process termination/power loss. + """ + audio = target.with_suffix(".audio") + replacement = staged.with_suffix(".audio") + replacement.mkdir(exist_ok=True) + backup = staged.parent / "previous.audio" + had_audio = audio.exists() + if had_audio: + audio.replace(backup) + installed = False + try: + replacement.replace(audio) + installed = True + staged.replace(target) + except BaseException: + if installed: + shutil.rmtree(audio) + if had_audio: + backup.replace(audio) + raise + + +@contextmanager +def staged_cassette(target): + target.parent.mkdir(parents=True, exist_ok=True) + with TemporaryDirectory(prefix=".recording-", dir=target.parent) as directory: + staged = Path(directory) / target.name + yield staged + publish_cassette(staged, target) + + +class AudioPersister: + """Use ordinary VCR serialization after restoring exact HTTP body bytes.""" + + @staticmethod + def load_cassette(cassette_path, serializer): + path = Path(cassette_path) + if not path.is_file(): + raise CassetteNotFoundError() + data = serializer.deserialize(path.read_text()) + for interaction in data["interactions"]: + body = interaction["request"].get("body") + if isinstance(body, dict) and "parts" in body: + interaction["request"]["body"] = b"".join( + part.encode() if isinstance(part, str) else read_audio(path.parent, part) for part in body["parts"] + ) + body = interaction["response"]["body"].get("string") + if isinstance(body, dict) and "audio_file" in body: + interaction["response"]["body"]["string"] = read_audio(path.parent, body) + return deserialize(serializer.serialize(data), serializer) + + @staticmethod + def save_cassette(cassette_path, cassette_dict, serializer): + with staged_cassette(Path(cassette_path)) as staged: + AudioPersister._save(staged, cassette_dict, serializer) + + @staticmethod + def _save(path, cassette_dict, serializer): + data = serializer.deserialize(serialize(copy.deepcopy(cassette_dict), serializer)) + directory = path.with_suffix(".audio") + for index, interaction in enumerate(data["interactions"]): + request = interaction["request"] + headers = {key.lower(): value for key, value in request["headers"].items()} + content_type = headers.get("content-type", [""])[0] + if "multipart/" in content_type: + body = request["body"] + message = BytesParser(policy=default).parsebytes( + f"Content-Type: {content_type}\r\n\r\n".encode() + body + ) + parts, remaining = [], body + for number, part in enumerate(message.iter_parts()): + if not part.get_filename(): + continue + payload = part.get_payload(decode=True) + # Preserve multipart headers, boundaries and text verbatim. + prefix, found, remaining = remaining.partition(payload) + assert found, "Multipart audio payload missing" + name = directory / f"request-{index}-{number}.wav" + write_audio(name, payload) + parts.extend([prefix.decode(), {"audio_file": f"{directory.name}/{name.name}"}]) + if parts: + request["body"] = {"parts": [*parts, remaining.decode()]} + response = interaction["response"] + headers = {key.lower(): value for key, value in response["headers"].items()} + content_type = headers.get("content-type", [""])[0] + if "audio/" in content_type or "application/octet-stream" in content_type: + # The test requests PCM explicitly; do not reinterpret arbitrary codecs. + import json + + assert json.loads(request["body"])["response_format"] == "pcm" + name = directory / f"response-{index}.wav" + write_audio(name, response["body"]["string"], pcm=True) + response["body"]["string"] = {"audio_file": f"{directory.name}/{name.name}", "pcm": True} + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(serializer.serialize(data)) + + +def save_websocket(path, endpoint, events): + with staged_cassette(path) as staged: + _save_websocket(staged, endpoint, events) + + +def _save_websocket(path, endpoint, events): + """Combine chunks into playable files, retaining each chunk's exact range.""" + import json + + events = copy.deepcopy(events) + directory = path.with_suffix(".audio") + streams = {} + for event in events: + message = event["message"] + kind = message.get("type") + field = ( + "audio" + if kind == "input_audio_buffer.append" + else "delta" + if kind == "response.output_audio.delta" + else None + ) + if field is None: + continue + key = "caller" if field == "audio" else message["response_id"] + if key not in streams: + name = "caller.wav" if key == "caller" else f"agent-{sum(k != 'caller' for k in streams):03d}.wav" + streams[key] = (name, bytearray()) + name, pcm = streams[key] + chunk = base64.b64decode(message[field], validate=True) + message[field] = { + "audio_file": f"{directory.name}/{name}", + "pcm": True, + "offset_bytes": len(pcm), + "length_bytes": len(chunk), + } + pcm.extend(chunk) + for name, pcm in streams.values(): + write_audio(directory / name, pcm, pcm=True) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps({"endpoint": endpoint, "events": events}, indent=2) + "\n") + + +def load_websocket(path): + import json + + data = json.loads(path.read_text()) + for event in data["events"]: + message = event["message"] + for field in ("audio", "delta"): + reference = message.get(field) + if isinstance(reference, dict) and "audio_file" in reference: + message[field] = base64.b64encode(read_audio(path.parent, reference)).decode() + return data diff --git a/py/src/braintrust/integrations/pipecat/_test_websocket.py b/py/src/braintrust/integrations/pipecat/_test_websocket.py new file mode 100644 index 000000000..33d4f3848 --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/_test_websocket.py @@ -0,0 +1,127 @@ +"""Test-only, ordered recording/replay at Pipecat's real WebSocket boundary. + +Only transport I/O is replaced. Requests must match; native service handlers, +frames, aggregators and instrumentation are unchanged. No headers are stored. +""" + +import asyncio +import json +from pathlib import Path + +from ._test_audio_cassettes import load_websocket, save_websocket + + +class WebSocketCassette: + def __init__(self, path: Path, record=False): + self.path, self.record = path, record + saved = {} if record else load_websocket(path) + self.events = saved.get("events", []) + self.index = 0 + self.condition = asyncio.Condition() + self.closed = False + self.socket = None + self.client_ids = {} + self.failure = None + self.endpoint = saved.get("endpoint") + + async def connect(self, real_connect, **kwargs): + if self.record: + self.endpoint = kwargs["uri"] + self.socket = await real_connect(**kwargs) + else: + assert kwargs["uri"] == self.endpoint, "WebSocket endpoint changed; re-record cassette" + return self + + def _normalize(self, message): + value = json.loads(json.dumps(message)) + value.pop("event_id", None) + item = value.get("item", {}) + if item.get("id") in self.client_ids: + item["id"] = self.client_ids[item["id"]] + return value + + async def send(self, data): + message = json.loads(data) + if self.record: + self.events.append({"direction": "send", "message": message}) + await self.socket.send(data) + return + try: + async with self.condition: + await asyncio.wait_for( + self.condition.wait_for( + lambda: ( + self.closed + or self.index == len(self.events) + or self.events[self.index]["direction"] == "send" + ) + ), + 5, + ) + assert not self.closed and self.index < len(self.events), "Unexpected WebSocket send" + expected = self.events[self.index]["message"] + # Initial conversation item IDs are client-generated. Preserve + # their correspondence in incoming echoes, not just in matching. + if message.get("type") == "conversation.item.create": + actual_id = message["item"].get("id") + expected_id = expected.get("item", {}).get("id") + if actual_id and expected_id: + self.client_ids[actual_id] = expected_id + assert self._normalize(message) == self._normalize(expected), ( + f"WebSocket request mismatch at event {self.index}: {message.get('type')}" + ) + self.index += 1 + self.condition.notify_all() + except Exception as error: + self.failure = error + raise + + def __aiter__(self): + return self + + async def __anext__(self): + if self.record: + data = await self.socket.recv() + self.events.append({"direction": "receive", "message": json.loads(data)}) + return data + async with self.condition: + await self.condition.wait_for( + lambda: ( + self.closed + or (self.index < len(self.events) and self.events[self.index]["direction"] == "receive") + ) + ) + if self.closed: + raise StopAsyncIteration + message = self.events[self.index]["message"] + self.index += 1 + self.condition.notify_all() + # Rewrite only values identical to client IDs learned from outgoing + # requests. Provider-generated IDs and all payloads are preserved. + reverse = {recorded: actual for actual, recorded in self.client_ids.items()} + + def remap(value): + if isinstance(value, dict): + return {key: remap(item) for key, item in value.items()} + if isinstance(value, list): + return [remap(item) for item in value] + return reverse.get(value, value) if isinstance(value, str) else value + + await asyncio.sleep(0) + return json.dumps(remap(message)) + + async def close(self): + self.closed = True + if self.socket: + await self.socket.close() + async with self.condition: + self.condition.notify_all() + + def verify(self): + assert self.failure is None, f"WebSocket replay failed: {self.failure}" + if not self.record: + assert self.index == len(self.events), "WebSocket cassette was not fully consumed" + + def save(self): + assert self.record and self.events + save_websocket(self.path, self.endpoint, self.events) diff --git a/py/src/braintrust/integrations/pipecat/cassettes/latest/test_cascade_voice_conversation.audio/request-0-2.wav b/py/src/braintrust/integrations/pipecat/cassettes/latest/test_cascade_voice_conversation.audio/request-0-2.wav new file mode 100644 index 0000000000000000000000000000000000000000..6d5c2aca4216c3bfafcf4f0459c74c0e4534656a GIT binary patch literal 95614 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 vex_Xy90_f) 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 Qz=?np0Ve|g-y`t<04HQAy#N3J literal 0 HcmV?d00001 diff --git a/py/src/braintrust/integrations/pipecat/cassettes/latest/test_cascade_voice_conversation.audio/response-3.wav b/py/src/braintrust/integrations/pipecat/cassettes/latest/test_cascade_voice_conversation.audio/response-3.wav new file mode 100644 index 0000000000000000000000000000000000000000..ccdc4923dc621585873d7f54ff9982e04eee3548 GIT binary patch literal 103244 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

G}aljo9 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 literal 0 HcmV?d00001 diff --git a/py/src/braintrust/integrations/pipecat/cassettes/latest/test_cascade_voice_conversation.yaml b/py/src/braintrust/integrations/pipecat/cassettes/latest/test_cascade_voice_conversation.yaml new file mode 100644 index 000000000..5ea45c89e --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/cassettes/latest/test_cascade_voice_conversation.yaml @@ -0,0 +1,431 @@ +interactions: +- request: + body: + parts: + - "--61f6aee3212ab98ae8a4bcfba196d0cc\r\nContent-Disposition: form-data; name=\"model\"\r\n\r\ngpt-transcribe\r\n--61f6aee3212ab98ae8a4bcfba196d0cc\r\nContent-Disposition: + form-data; name=\"language\"\r\n\r\nen\r\n--61f6aee3212ab98ae8a4bcfba196d0cc\r\nContent-Disposition: + form-data; name=\"file\"; filename=\"audio.wav\"\r\nContent-Type: audio/wav\r\n\r\n" + - audio_file: test_cascade_voice_conversation.audio/request-0-2.wav + - "\r\n--61f6aee3212ab98ae8a4bcfba196d0cc--\r\n" + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '95975' + content-type: + - multipart/form-data; boundary=61f6aee3212ab98ae8a4bcfba196d0cc + host: + - api.openai.com + user-agent: + - AsyncOpenAI/Python 3.22.1 + x-stainless-arch: + - arm64 + x-stainless-async: + - async:asyncio + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 3.22.1 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.14 + method: POST + uri: https://api.openai.com/v1/audio/transcriptions + response: + body: + string: '{"text":"Where is my order number 1042?","languages":[{"code":"en"}],"usage":{"type":"duration","seconds":3}}' + headers: + access-control-expose-headers: + - X-Request-ID + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + cf-ray: + - a45e173f48dc9991-ORD + connection: + - keep-alive + content-length: + - '109' + content-type: + - application/json + date: + - Mon, 05 Oct 2026 17:19:22 GMT + openai-processing-ms: + - '415' + openai-version: + - '2020-10-01' + server: + - cloudflare + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-reset-requests: + - 2ms + x-request-id: + - req_60ff4c35d79a4ef4ba5524b883e47c7a + status: + code: 200 + message: OK +- request: + body: '{"messages":[{"role":"system","content":"You are an English order assistant. + Always call lookup_order for the supplied order number. After its result, answer + only: Your order arrives Friday."},{"role":"user","content":"Where is my order + number 1042?"}],"model":"gpt-4.1-mini","stream":true,"stream_options":{"include_usage":true},"temperature":0,"tools":[{"type":"function","function":{"name":"lookup_order","description":"Look + up an order","parameters":{"type":"object","properties":{"order_id":{"type":"string"}},"required":["order_id"]}}}]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '543' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - AsyncOpenAI/Python 3.22.1 + x-stainless-arch: + - arm64 + x-stainless-async: + - async:asyncio + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 3.22.1 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.14 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: 'data: {"id":"chatcmpl-EVgdOuXIzx8k2PyBeZ13DlFvyTiHs","object":"chat.completion.chunk","created":1791220762,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_HNcMI5HXdcAexG9Q2LyrywSk","type":"function","function":{"name":"lookup_order","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"0G"} + + + data: {"id":"chatcmpl-EVgdOuXIzx8k2PyBeZ13DlFvyTiHs","object":"chat.completion.chunk","created":1791220762,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"t0EAEPjwOE3V4"} + + + data: {"id":"chatcmpl-EVgdOuXIzx8k2PyBeZ13DlFvyTiHs","object":"chat.completion.chunk","created":1791220762,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"order"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"hTsuGEQluaQ"} + + + data: {"id":"chatcmpl-EVgdOuXIzx8k2PyBeZ13DlFvyTiHs","object":"chat.completion.chunk","created":1791220762,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"_id"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"RbDW9yy4p5d0B"} + + + data: {"id":"chatcmpl-EVgdOuXIzx8k2PyBeZ13DlFvyTiHs","object":"chat.completion.chunk","created":1791220762,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"NQsK0o1U7Yp"} + + + data: {"id":"chatcmpl-EVgdOuXIzx8k2PyBeZ13DlFvyTiHs","object":"chat.completion.chunk","created":1791220762,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"104"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"m129b49pxd4cL"} + + + data: {"id":"chatcmpl-EVgdOuXIzx8k2PyBeZ13DlFvyTiHs","object":"chat.completion.chunk","created":1791220762,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"2"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"dcNLW3HIbOM5kP8"} + + + data: {"id":"chatcmpl-EVgdOuXIzx8k2PyBeZ13DlFvyTiHs","object":"chat.completion.chunk","created":1791220762,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"EWvEcBb97SY20"} + + + data: {"id":"chatcmpl-EVgdOuXIzx8k2PyBeZ13DlFvyTiHs","object":"chat.completion.chunk","created":1791220762,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null,"obfuscation":"mM84yutyoCf36X"} + + + data: {"id":"chatcmpl-EVgdOuXIzx8k2PyBeZ13DlFvyTiHs","object":"chat.completion.chunk","created":1791220762,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[],"usage":{"prompt_tokens":81,"completion_tokens":16,"total_tokens":97,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"h4w8R04Gi"} + + + data: [DONE] + + + ' + headers: + access-control-expose-headers: + - X-Request-ID + - CF-Ray + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + cf-ray: + - a45e17434956eb08-ORD + connection: + - keep-alive + content-type: + - text/event-stream; charset=utf-8 + date: + - Mon, 05 Oct 2026 17:19:22 GMT + openai-processing-ms: + - '401' + openai-version: + - '2020-10-01' + server: + - cloudflare + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999952' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_abcbe4d2155743049ef5fc4d96018f97 + status: + code: 200 + message: OK +- request: + body: '{"messages":[{"role":"system","content":"You are an English order assistant. + Always call lookup_order for the supplied order number. After its result, answer + only: Your order arrives Friday."},{"role":"user","content":"Where is my order + number 1042?"},{"role":"assistant","tool_calls":[{"id":"call_HNcMI5HXdcAexG9Q2LyrywSk","function":{"name":"lookup_order","arguments":"{\"order_id\": + \"1042\"}"},"type":"function"}]},{"role":"tool","content":"{\"order_id\": \"1042\", + \"delivery\": \"Friday\"}","tool_call_id":"call_HNcMI5HXdcAexG9Q2LyrywSk"}],"model":"gpt-4.1-mini","stream":true,"stream_options":{"include_usage":true},"temperature":0,"tools":[{"type":"function","function":{"name":"lookup_order","description":"Look + up an order","parameters":{"type":"object","properties":{"order_id":{"type":"string"}},"required":["order_id"]}}}]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '836' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - AsyncOpenAI/Python 3.22.1 + x-stainless-arch: + - arm64 + x-stainless-async: + - async:asyncio + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 3.22.1 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.14 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: 'data: {"id":"chatcmpl-EVgdPGIU8tt1JYoX26BJKEwutVjgO","object":"chat.completion.chunk","created":1791220763,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"iy4PmEzM"} + + + data: {"id":"chatcmpl-EVgdPGIU8tt1JYoX26BJKEwutVjgO","object":"chat.completion.chunk","created":1791220763,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[{"index":0,"delta":{"content":"Your"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"SJmzGt"} + + + data: {"id":"chatcmpl-EVgdPGIU8tt1JYoX26BJKEwutVjgO","object":"chat.completion.chunk","created":1791220763,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[{"index":0,"delta":{"content":" + order"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"7VgR"} + + + data: {"id":"chatcmpl-EVgdPGIU8tt1JYoX26BJKEwutVjgO","object":"chat.completion.chunk","created":1791220763,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[{"index":0,"delta":{"content":" + arrives"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"6t"} + + + data: {"id":"chatcmpl-EVgdPGIU8tt1JYoX26BJKEwutVjgO","object":"chat.completion.chunk","created":1791220763,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[{"index":0,"delta":{"content":" + Friday"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"AQQ"} + + + data: {"id":"chatcmpl-EVgdPGIU8tt1JYoX26BJKEwutVjgO","object":"chat.completion.chunk","created":1791220763,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"HSolHVyjx"} + + + data: {"id":"chatcmpl-EVgdPGIU8tt1JYoX26BJKEwutVjgO","object":"chat.completion.chunk","created":1791220763,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"QUpY"} + + + data: {"id":"chatcmpl-EVgdPGIU8tt1JYoX26BJKEwutVjgO","object":"chat.completion.chunk","created":1791220763,"model":"gpt-4.1-mini-2025-04-14","service_tier":"default","system_fingerprint":"fp_717f55dfc9","choices":[],"usage":{"prompt_tokens":119,"completion_tokens":6,"total_tokens":125,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"lqUhtSpH"} + + + data: [DONE] + + + ' + headers: + access-control-expose-headers: + - X-Request-ID + - CF-Ray + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + cf-ray: + - a45e1747d93beb08-ORD + connection: + - keep-alive + content-type: + - text/event-stream; charset=utf-8 + date: + - Mon, 05 Oct 2026 17:19:23 GMT + openai-processing-ms: + - '342' + openai-version: + - '2020-10-01' + server: + - cloudflare + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29998' + x-ratelimit-remaining-tokens: + - '149999842' + x-ratelimit-reset-requests: + - 4ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_06cb46e2f249447ea68ce13d2c440229 + status: + code: 200 + message: OK +- request: + body: '{"input":"Your order arrives Friday.","model":"gpt-4o-mini-tts","voice":"alloy","response_format":"pcm"}' + headers: + accept: + - application/octet-stream + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '104' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - AsyncOpenAI/Python 3.22.1 + x-stainless-arch: + - arm64 + x-stainless-async: + - async:asyncio + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 3.22.1 + x-stainless-raw-response: + - stream + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.14 + method: POST + uri: https://api.openai.com/v1/audio/speech + response: + body: + string: + audio_file: test_cascade_voice_conversation.audio/response-3.wav + pcm: true + headers: + access-control-expose-headers: + - X-Request-ID + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + cf-ray: + - a45e174b9f200f96-ORD + connection: + - keep-alive + content-type: + - audio/pcm + date: + - Mon, 05 Oct 2026 17:19:23 GMT + openai-processing-ms: + - '350' + openai-version: + - '2020-10-01' + server: + - cloudflare + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999995' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_747786e6e11a4ea1ad86e5112285942c + status: + code: 200 + message: OK +version: 1 diff --git a/py/src/braintrust/integrations/pipecat/cassettes/latest/test_realtime_voice_conversation.audio/agent-000.wav b/py/src/braintrust/integrations/pipecat/cassettes/latest/test_realtime_voice_conversation.audio/agent-000.wav new file mode 100644 index 0000000000000000000000000000000000000000..b5f0b07c5d3445dd33f5381ab9944deb774c7f82 GIT binary patch literal 98444 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 MU807k4BpKBA7zwY#{d8T literal 0 HcmV?d00001 diff --git a/py/src/braintrust/integrations/pipecat/cassettes/latest/test_realtime_voice_conversation.audio/agent-001.wav b/py/src/braintrust/integrations/pipecat/cassettes/latest/test_realtime_voice_conversation.audio/agent-001.wav new file mode 100644 index 0000000000000000000000000000000000000000..702ae2668544c2e8bba0554f6096388038cdfab0 GIT binary patch literal 105644 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 literal 0 HcmV?d00001 diff --git a/py/src/braintrust/integrations/pipecat/cassettes/latest/test_realtime_voice_conversation.audio/caller.wav b/py/src/braintrust/integrations/pipecat/cassettes/latest/test_realtime_voice_conversation.audio/caller.wav new file mode 100644 index 0000000000000000000000000000000000000000..b77ccbc6dc142e43efdbb2733b4e66c16ab7c176 GIT binary patch literal 166844 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

edemg2RT9s!&$$J5s(w(BevrdUHgRG<#2dnk{} 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 +~v*3qd0W%oA!0E^B zf-WNeHKM?%ImsQc=z;o%Rp)wnCsWOmW5G!8&UGg>GX+Q}i z?bwY8F3FRVMkmfq@=FO!otGYwQr?YmH>D;et&Yuz=@YXg>S;I` zx;um$)Zf>~BiZe}`&suX&UbhOhr#w|dZ8KA0w9o_vRX_PhE#m8{-d^8U4wm>UzBTr zxiS$5M1RE$@ul{;?In zaqzFVFtWX;*eco~Jkea(GD`SC_(vEbBE{zRx>i|pL(4FsplwWRhvt2)Kz)Up^S$zM zQmNcYGgxQS9M>%-a)9pr5e=m0GNt^l@R`55FYvYoio)hbI7N1iz7>Bqr8a#)+P2i^ znV$-V6+09p=04AxUpTsOW`1Md!GbwOv4x{@7Nk#4O--vyH>UPa>>tC6YKvGK`Z(Z$ z?+c$g??djLoz6N8=loz*qF3zI1P5*qM;YH~cdI^OrC7PVo48I?CW)6G6isN=0ju~R zKlrz~=|;=f)`-@F&6*~+mbTXEpz5g+K5YvTZ4$LY|FYXphz-&cP^07o?7~;l zSx}=Ul;+D5l~)xM-0}C8gv)QiJ?mmnf-VwIm(z;%vPq&%ZO28S(t(nbVp{%7`y4-@ z3sSqP`fHb%#@U2q1FR%$-fYJN{vN>>mr$=-e_`O<;GFO^G20W4BvHw)Q>SE%&W_2w zncF>|TfDn0ul!U=a^a19s$f&mjKZuuO|~sNFXv35qsnWb_LzsA0Pi$fwH_OY zT~Iw$`>F?E0hl-3F#p2#K=tP;EJMx4Xz3xzEcphki}J4o5iJ(Ah*jdlVzD$sz0??E zsWMzqKT;8ThV2ddk9`5MnA=^3xjk^}Knw<$Lw zFRguXlt(jPgO@s;0ib@E?`G&HqeIPyhJ*ded;Q@xk98f95oH z{`;vVr@deML*X^yOVMjthT<-`1w|>|s*Cgty(^Hyb($H50MmB-x%QUsp0PXReWvMO zYrp6#jlM=Fd=K2C4$yh%9s|MR8z@4)fi~_l#LF_pOQ4Mu%Xi4rq+QydwB8hkO1>*y zwby`ia{#C{z;M&^%r=Va5aZn7a?z#Ut)rK}-_!u_;5iXR3Bl>PSwpjWWYc-v;?Kpi z3sVXn7pGO#*WIprUOlr+T$rApp1VC;l~tFWo;@hzdE&!}_<)sOsU9KjM_ig6C$kNR z+(;7hIj)@+&t!!P^vY{oed3$5)_lmC$-z`56HJtjnpdt8Ac2n1X$9~8ElK?9FLizQ2>QZGCOAwyU;1_CDlI@&UPn+(c^0MN~KHkX->J zNU_<;I>5TiyunZoe3CQZgtSG2VugwdWq|4=7AGGl9w7>ojFJ8pZ*A+=Dr|Y&GO(qu zaI;LUA86fRI;={Tg{pjsE1c<0A+B{E-+Z?C>-@U=?GG9nBTFmE^~nE}-&|N+^0VZ3 z@#SK9>FSD8)sJiK9WAw;DhB0eq?IR6O}m?UA{|SZ9F-I%3Rva-hwBgb(3PAbmZSZs zUV`mWn5B;Gcbme1-`=@-PxFYs=?#;<_4?NIL)7rSVO@jnx3cM<@THK~650Gr$dQ?` zuj(!uLR)QEVs^1+TDMqd*v?V+(QKxQVMNcMgBgX)L?(rv0z&Xn_F6uexAK zwTMiJu^L?DK4}+e;?*kbq%ucgkqwkp$UaN^OQwptgWe}m#1d_6>)D#w?ABD@e4$;5 zjWnGnx|&h#MC`k60dGn$SzKb1{3 z??W7@GQ&u9Anr#mcl_q|)MspnIr>rD*670Ul!&DX%W_tf&8rerPpZu95L5QA4WmK6>lqjDmRs(1;5f)B)o~<5IHTvA$(9syZ;Q&4T9dRK=QP?!0=c&&zXPQny$4xX>XS91ub5XOd)qrTcK)V zmf?r-t>vC=i@na4Lgd(RvH)2Rr`+At4{9i_r@E8tY)=WqR%tsxXe|u$PUCyX9<=EP z>hiVEfCD^1Z32!!F4T0+SA@!yz)GJlJ15%IznM47edzoOuhsxo1D zw+<)Df{F%ZTa%SBC1KM76h0+BeSP+Nc6ZvuloCv%N;^aX$A7u9o=8N)!5a1&T<}h%B9pH z($BuuHqAbfj3rC$9qd))V)C%VB^%qKT&UP6KOkSK*a41XE}}7Q!=w&)Ao8A@%H565CUfY6 zoQGpO83r3yR~*5(Wz@iY!NdrC(om>f3aWjxdLO( z!gTle4k0;SQ(P`N=eZ`iE)x7;`%>3U;o2RFrBLtj1$>P2q++pLc>Le^->nUXUkm?C zXv%Ay*0!{LXuAY-D*mFw;>%KnqEr(Fc_J$=)Q`qT84JuI7LFAo0!R+knRK#O*uKME zy$8`m1j48W6Cj%*Cpyk;Li>3^{B>QRjpQP(lj@)*~<6WW%YSAPi0e1 z#sXBofXDO}`=a`)nupC%CSXfcUaBy~bjf#7wDha$vv~&N6t9otHFiAe%^1xydu$9I z6ni$!Iqz&iRqm6tSxKW)ZsquP_*IwRy{ao-bD->8!G@gQndXf1S&n&s^4oLvrgV!e z@;mA}g@1>)+HoD9@7T;rCrgau)f$;j6x+@c{U=G04Uj%;KhqrlC+v51 zA`;l(b1W{9&&@Hlg4>iHXz-7MLI(pL|)<^;qTz3aOSeT92y0;J)Q-Pj1SHJ zUC1w53##aq$+wbkWtNpZtkrZ0?98eDuXJbr_RP1*tqHcIIT@33&SyPI>Jz@%`+;C5 z=Os(ZcIP^9_Ap+Nh~=;Lxh$?t-!irBf`lQrNSBMILM>p<-t^;Rp8`Z=E2;&C7pT%*L#{#o48k3JNmM zCm&5B)132Xm1ovub(GXRt@u>*Br`m5Ow7R8K}mk;1Jmy$x<*|0mAkm{o`KhOC>;%V zik?WSZ3q5X87=t^H9h;q$0ge&_rMwbc>9F58qjmvh4V!Bq-PbS7$4iASR$XS=%wlb zdm^#!h<>Y~3y_+(8Rp^x4LePvA^)kcthO9CSC~qT<&fp71X^CRUImVFZu+_4zjPJb zq4EWO`BrQju&{TkOjtR#3cI0tqQ;@l+fk#`oY3CYuF_u8mI2}Ik**JTvT8M-)ZlB3 z^;c9%8R8J>CXLNz;fxd96x8!SI|}&6_;JovUTedM6j4EYS>G~Q!THQFDT?HZ3_7oO zsb^(&jaTi<%Alg^wC*uOz=?KL-1dZy@yzITAya&=IzQ*!LVnrPY!7X#$wBspJc2ux`jY$!lb%0odX!ZIXvUecv}VjvZ4@#r~=}y6Zqjnya%wMZ{s_ zbNrZAryinxhEv8E<9z)mZMiOAzgJhS2~l0cKB!olcnzldtRx}c-B3uBpOiNh{S;4? z4XV@XFRB)-1I&m-twh^HcTM+KCj&NMA6$ml6Em(8x8SGBfuLf*>E2k8YFC$iS%uPl3AJ-T*P z#jkv8iXz$^>Ki;IXnBw!FexD4SMG7rX)Aj^NtlC83rufJEYmLFdPt$dWg1khyp(?h zeu`;GC&Q--aE*We|PYVg3{!k(Mfd;p|zr7Tr^QS7daGz3sDINP1xxtDp* z2O_~5uR`x<{>vl$QkwHaD~f8fI~LWPsVFF2S8y`NoK>FlIe%I4`x4KhzuEOE`(k|~ z-UT)JSlwPjj<_2?jJJu!r{5E&Eyv8EruFzoZ8h+)+_lkKAI*7Hv1+z@ArN~G=;T_S zZj!#pP->)rJ~$3PhmSYxHO#_iL5<%q?S7pK_)4^D9kv7LY(ub13SaqASvb@STjV$8 z;XrliD*FPic(IE6a$k8L`CG*bY`m%$P~2Xq3N;_KZ*>v+o}g8^ihnWOHuAx{Zw8R_ z?idqH38tCG-iD6&TD=~4Q!ZMS>Ybv4^e8y9U6dxOpPB-YzPxs~U%u=hR&Z$0`#>@{ z1h|*CG71X075{2))-A4+)!wK)TKcA7b?(-jz}&3d{@D-HFDE~VFNm%S?e2HNeW24q zUI&f?jE2^R63FJFB95@z31RSsOEErm>P$af_qdBP5}{0xP^Rml2LlY-h$p>KJ5}rx*+I zKKhP&FMXOWQJbXQsT&0ZlIOTi|6RWYUjuvP#YWV`gzWTh>nB1&u!yCWR?~2!9M9Je z*M3!Nf%<<1h=%)t7qkoNU6w1J$TNZca#1!x)*Tq$eED+u2RRq2p(2%UU?(se(_m|W zSg~3i1-r1d+E|FUFX5eB4V^%p!!Zsvh8f2j8iBy_9`A`)0XMW%drP%RKvd-ReKw_sc*CzpaUymPF~Wj z1Lo>F;H8g&-N{-!8{YuF@GsWX% zNU!+0=__+~=5@;3oii}2J|jAPaC-O5_MHBOYf7hdI9-ue5nq0x7!8IpF}>=ZdZl`eY7+K9 zIY4Ky$eWT@Xd_l^7- z_7=$3%(b;!&sjn&{Vi%s0w~XBnWIc);5Prxs5f=6T(*v}O(RFrR}lwvALI-PdIge? z-a*^YE2tOBgFLv!w!o5Yz%^&E_Xh0=!AmNNse^4!kOTLA` z1oD>-mn2IDNsdd*l1I`!IYZeU>e60gZmMspP3oVpr<|+Vti^P_;Pm2UiZ(wq+s(Z# zN=vTwsHM)#GIaz+(ki^4Znyf4vR$@R{6W~-9MD|Unk#;U?J^By@LXa7Rz~ej>X0!! zTbDg6D=A}2nmP4O+SrWWSwC{M1>1`wO7lwJ7HbOL=PXYjka#9)Z*UJEAJ?wD5zO5b z!?xQ}3l2w9%xRVeP{OG!vn&n3hvxh*r(%gB-rK6k&D2$TH-ez=kvhZ+GIS%*I>r~q zHO6Pi9zGyn6CKRSc#Aq(`9l^U8wOfWXZbbxK;V8!6 zO1vG|@)mGcuuA^PRw)iEi?QoicU6>Hr0xXla35{D?z+CK;i*w27r+HWM2O zlnA#LS_Yasn2s3w>9=VnU=1>*C`)*^d0MlyHD2PT?r#~-3UTueN{soFOlO?VMsg0p zYN1SsnM#z69`zSO5nmH@*3#?xWR4{9|7i5 z7fGx1uY9NSHr5|_gcH>n8ms1iBv^^|o~{g!GJZ0BFy~tCT3%Y+Y~O5KYR~NZ6-UaCNWV!$k_FNz&<_8V z_fsNJ#aO6$0%Y}VTD7)VcUQMeKM!AGIAj!=GA;eAS;Qxz!gd7iJlXGnOso>B#HLrgTBfBw6maWX4 zSAZ73E9q5wvcy`HSkREINHxS=4PO;d=lMhsz|N)GEgg*BxVJt6G|4v&$4z4``>mAq znzfU4y0tg4%r@JePj;g$^d(ShCol@pD@YSE96g4PLDk4yq>VmC)!5rDN;qKtr#h$D zD@&2ilWdpFk=kWI;ln~z=U`7gNZkWY)h94;K2;2Zo!2d?o0O1jlJ1dxk-I=e!EIC z<(I^6ZCzW7g@NKH$_9ghJ}JoZ9~-kLy;&COC;`K$d3V!BJ%r4H7W`*W{&VN+c zzvy{kdcl?4$gCl${_%^$NBBQ<7xD+QPf>i!7d;RBefnq@>i+4k7_6ok>rz5QJSA2W z!->7bT$|p$4(M_pkrU`V@Fw`g5TbvO&PXUIVNO##GQ$>cJ!u+_U)F%zt~^6}RD2$4 zZIvR6xIj7*h)*SoWnCUtb(B&7F?5s2 zQPM{-QrAFq;*@#Z3av;wkkeHBu>5R?gXODB8;kzrN9A6~=H)!f9hWc456yp=E6aAw zoRN|p7Zn~6FwAqM(*>Q~RfO zS5Z%>HtHqzl_({RQj=`BA^>YsjfZ>xd%9nG4froXF3s2(RPjv9C(A(VEvw!dOAIEa z5*fsCtKG8BGTAcIl51H5+|>Etfb_vM#&pOi1K+oYhPQ?=!xsHktpN6+1={Vn(Xts? z?|91lLqxaKH~E{(H&j2Zt*zZqHM*QCD#>4!^C^p&CCk)gj>`O$?vmOy@j#3wY-+$R z&jzQSTrTQpyJ+mGt5h$7-Qr?wxVnRGtKqSE8qo`!0w=)UN@qW5UuT!u*^~^bU%Xja z?5*s2_Cipg9zq_HCy85TKjUltN$p~FGWHXEl5!;NqV?@rZ5hH(tqkFEA-8RA8w0A8 z5y&F;)j$m&&fT4H3dmrWj7Lo!%{$Gn%;}cTmNILB^^0YKrMsmo_yG1d zH-MYpEz?|+*|^cTz_<0G>20cVJBR&pas4v%@1PXns>YCaIcOpo}1E;?JA+gz6 zi%WEs@|u3NpQ{H|3@Z`jzsgF^K+@Nz6{p=xJ(jXPsegQG^q{bYfU#aZokw$fpf2_Z z^HlwQ^?a-zyR9;4F6e(6`&ob6@6dbEO$-%-!6Xe86Fw5h8UyT5NYU(_rWU+ zg`lo4Hw?k|;%5z2#$$$RysMtpjn=KyJjH~HS4ux^iFpRHlYi0oarEbm=;HWFSK9A8->;k=DZMMjD

+W@bCmi(=Uuz`^UJ@f5yO*GnVCMk<8T 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.12.0" + ): + pytest.skip("Native voice hooks require Pipecat 1.12") assert setup_pipecat(project_name="test-project-pipecat-py-tracing") init_test_logger("test-project-pipecat-py-tracing") llm = OpenAILLMService( @@ -282,8 +180,20 @@ async def test_setup_pipecat_traces_real_pipeline_frames(memory_logger): max_completion_tokens=20, ), ) + processors = [llm] + if native: + pair = _import("pipecat.processors.aggregators.llm_response_universal.LLMContextAggregatorPair")(LLMContext()) + params = _import("pipecat.transports.base_transport.TransportParams")() + processors = [ + _import("pipecat.transports.base_input.BaseInputTransport")(params), + _import("pipecat.services.openai.stt.OpenAISTTService")(api_key="unused"), + pair.user(), + llm, + _import("pipecat.transports.base_output.BaseOutputTransport")(params), + pair.assistant(), + ] worker = _make_worker( - Pipeline([llm]), + Pipeline(processors), name="bt-pipecat-test-worker", params=PipelineParams(enable_metrics=True, enable_usage_metrics=True), ) @@ -303,13 +213,26 @@ async def on_pipeline_started(_worker, _frame): await asyncio.wait_for(runner.run(), timeout=20) observer = next(o for o in getattr(worker, "_observer")._observers if isinstance(o, BraintrustPipecatObserver)) - if version_satisfies(detect_module_version(importlib.import_module("pipecat"), ("pipecat",)), ">=1.12.0"): - assert observer.observe_every_push is False - assert not hasattr(observer, "_seen_frame_ids") - else: - assert observer._seen_frame_ids + assert (observer._voice is not None) is native logs = memory_logger.pop() + if native: + pipeline_span = _single_span(logs, "pipecat.pipeline") + turn = _single_span(logs, "assistant_turn") + llm_span = _single_span(logs, "llm_response") + assert turn["span_parents"] == [pipeline_span["span_id"]] + assert llm_span["span_parents"] == [turn["span_id"]] + assert llm_span["metadata"]["turn.id"] == turn["span_id"] + assert llm_span["span_attributes"]["type"] == "llm" + assert "braintrust pipecat integration" in llm_span["output"][0]["content"].lower() + assert llm_span["metadata"]["pipecat.usage"]["value"]["total_tokens"] == llm_span["metrics"]["tokens"] + assert llm_span["metadata"]["model"] == "gpt-4o-mini" + assert llm_span["metadata"]["provider"] == "openai" + assert llm_span["metrics"]["prompt_tokens"] > 0 + assert llm_span["metrics"]["completion_tokens"] > 0 + assert llm_span["metrics"]["time_to_first_token"] >= 0 + assert llm_span["metadata"]["pipecat.ttfb"] + return pipeline_span = _single_span(logs, "pipecat_pipeline") assert _span_type(pipeline_span) == "task" assert pipeline_span.get("metrics", {}).get("end") is not None @@ -372,15 +295,20 @@ def test_auto_instrument_pipecat_subprocess(): verify_autoinstrument_script("test_auto_pipecat.py") -@pytest.mark.parametrize("metric_name", ["TurnMetricsData", "SmartTurnMetricsData"]) -def test_turn_detection_metrics_on_legacy_speech_span(metric_name): - metric_class = _import(f"pipecat.metrics.metrics.{metric_name}") - metric = metric_class(processor="BaseSmartTurn", is_complete=True, probability=0.97, e2e_processing_time_ms=82.4) - rows = [] - observer = BraintrustPipecatObserver(trace_turns=False) - observer._user_audio_span = SimpleNamespace(log=lambda **row: rows.append(row)) - observer._capture_metrics(SimpleNamespace(data=[metric]), SimpleNamespace(name="aggregator")) - assert rows[-1]["metadata"]["pipecat.turn_metrics"] == [ +@pytest.mark.parametrize("metric_name,capture_audio", [("TurnMetricsData", False), ("SmartTurnMetricsData", True)]) +@pytest.mark.asyncio +async def test_legacy_turn_metrics_follow_speech_or_pipeline(memory_logger, metric_name, capture_audio): + frames = importlib.import_module("pipecat.frames.frames") + metric = _import(f"pipecat.metrics.metrics.{metric_name}")( + processor="BaseSmartTurn", is_complete=True, probability=0.97, e2e_processing_time_ms=82.4 + ) + observer = BraintrustPipecatObserver(trace_turns=False, capture_audio_attachments=capture_audio) + await observer.on_pipeline_started() + await observer._handle_frame(frames.UserStartedSpeakingFrame()) + await observer._handle_frame(frames.MetricsFrame(data=[metric])) + await observer.cleanup() + span = _single_span(memory_logger.pop(), "user_speaking" if capture_audio else "pipecat_pipeline") + assert span["metadata"]["pipecat.turn_metrics"] == [ dict( type=metric_name, processor="BaseSmartTurn", @@ -391,41 +319,41 @@ def test_turn_detection_metrics_on_legacy_speech_span(metric_name): ] -def test_turn_detection_without_audio_keeps_unassociated_measurement(): - metric_class = _import("pipecat.metrics.metrics.TurnMetricsData") - rows = [] - observer = BraintrustPipecatObserver(trace_turns=False, capture_audio_attachments=False) - observer._pipeline_span = SimpleNamespace(log=lambda **row: rows.append(row)) - observer._capture_metrics( - SimpleNamespace( - data=[ - metric_class(processor="KrispVivaTurn", is_complete=False, probability=0.3, e2e_processing_time_ms=120) - ] - ), - SimpleNamespace(name="aggregator"), - ) - assert observer._user_audio_span is None - assert not observer._user_audio - assert rows[-1]["metadata"]["pipecat.turn_metrics"][0]["is_complete"] is False - - @pytest.mark.asyncio async def test_ttfb_routes_by_processor_and_retains_unmatched(memory_logger): observer = BraintrustPipecatObserver(trace_turns=False, capture_audio_attachments=False) frames = importlib.import_module("pipecat.frames.frames") metric_class = _import("pipecat.metrics.metrics.TTFBMetricsData") - llm, tts, stt = [SimpleNamespace(name=name) for name in ("llm", "tts", "stt")] + processor_class = _import("pipecat.processors.frame_processor.FrameProcessor") + llm, tts, stt = [processor_class(name=name) for name in ("llm", "tts", "stt")] await observer._handle_frame(frames.LLMFullResponseStartFrame(), processor=llm) await observer._handle_frame(frames.TTSStartedFrame(), processor=tts) for processor, value in ((tts, 0.09), (stt, 0.12), (llm, 0.24)): await observer._handle_frame( frames.MetricsFrame(data=[metric_class(processor=processor.name, value=value)]), processor=processor ) + usage_class = _import("pipecat.metrics.metrics.LLMUsageMetricsData") + tokens_class = _import("pipecat.metrics.metrics.LLMTokenUsage") + await observer._handle_frame( + frames.MetricsFrame( + data=[ + usage_class( + processor="llm", value=tokens_class(prompt_tokens=10, completion_tokens=5, total_tokens=15) + ), + usage_class( + processor="unrelated-classifier", + value=tokens_class(prompt_tokens=100, completion_tokens=20, total_tokens=120), + ), + ] + ), + processor=llm, + ) await observer._handle_frame(frames.TTSStoppedFrame(), processor=tts) observer._close_all_open_spans() logs = memory_logger.pop() assert _single_span(logs, "tts_response")["metadata"]["pipecat.ttfb"][0]["value"] == 0.09 assert _single_span(logs, "pipecat_llm_response")["metrics"]["time_to_first_token"] == 0.24 + assert _single_span(logs, "pipecat_llm_response")["metrics"]["tokens"] == 15 assert _single_span(logs, "pipecat_pipeline")["metadata"]["pipecat.ttfb"][0]["processor"] == "stt" diff --git a/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py b/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py new file mode 100644 index 000000000..ee42e60d4 --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py @@ -0,0 +1,393 @@ +"""Provider-backed voice contract: HTTP cassettes, real Pipecat pipeline and export.""" + +# pylint: disable=import-error +import asyncio +import io +import json +import os +import wave +from email.parser import BytesParser +from email.policy import default +from pathlib import Path +from tempfile import TemporaryDirectory + +import numpy as np +import pytest +from braintrust.audio import RecordingOptions +from braintrust.integrations.pipecat import setup_pipecat +from braintrust.integrations.pipecat.test_pipecat import ( + _make_worker, + _single_span, + _worker_runner_kwargs, +) +from braintrust.integrations.pipecat.test_pipecat import memory_logger as memory_logger +from braintrust.test_helpers import init_test_logger +from pipecat.adapters.schemas.function_schema import FunctionSchema +from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.frames.frames import InputAudioRawFrame, VADUserStartedSpeakingFrame, VADUserStoppedSpeakingFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.worker import PipelineParams +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair +from pipecat.services.openai.llm import OpenAILLMService +from pipecat.services.openai.stt import OpenAISTTService +from pipecat.services.openai.tts import OpenAITTSService +from pipecat.transports.base_input import BaseInputTransport +from pipecat.transports.base_output import BaseOutputTransport +from pipecat.transports.base_transport import TransportParams +from pipecat.workers.runner import WorkerRunner + + +def _request_payload(request): + content_type = request.headers.get("Content-Type", "") + body = request.body + if "multipart/form-data" in content_type: + message = BytesParser(policy=default).parsebytes(f"Content-Type: {content_type}\r\n\r\n".encode() + body) + return [(part.get("Content-Disposition"), part.get_payload(decode=True)) for part in message.iter_parts()] + return json.loads(body) + + +@pytest.fixture(scope="module") +def vcr(vcr): + # Compare audio bytes, prompts, model/settings and tool results. Multipart + # boundaries are random HTTP framing, not part of the request's meaning. + def payload_matches(actual, expected): + assert _request_payload(actual) == _request_payload(expected) + + from braintrust.integrations.pipecat._test_audio_cassettes import AudioPersister + + vcr.register_persister(AudioPersister) + vcr.register_matcher("voice_payload", payload_matches) + return vcr + + +@pytest.fixture +def vcr_cassette_name(request, vcr_cassette_dir, vcr): + name = request.node.originalname or request.node.name + if vcr.record_mode != "all": + yield name + return + from braintrust.integrations.pipecat._test_audio_cassettes import publish_cassette + + # A fresh temporary path avoids VCR appending old interactions. The + # dependent VCR fixture saves here before this fixture's teardown runs. + target = Path(vcr_cassette_dir) / f"{name}.yaml" + target.parent.mkdir(parents=True, exist_ok=True) + with TemporaryDirectory(prefix=".recording-", dir=target.parent) as directory: + staged = Path(directory) / target.name + yield str(staged) + if getattr(request.node, "voice_recording_validated", False): + publish_cassette(staged, target) + + +class MemoryInput(BaseInputTransport): + async def start(self, frame): + await super().start(frame) + await self.set_transport_ready(frame) + + +class MemoryOutput(BaseOutputTransport): + """Local application transport: accept output without a telephone or playback delay.""" + + async def start(self, frame): + await super().start(frame) + await self.set_transport_ready(frame) + + async def write_audio_frame(self, frame): + self.last_audio_frame = frame + return True + + +async def _run_conversation(worker, finished): + runner = WorkerRunner(**_worker_runner_kwargs()) + task = asyncio.create_task(runner.run(worker)) + try: + await asyncio.wait_for(finished.wait(), 45) + await worker.stop_when_done() + await asyncio.wait_for(task, 10) + finally: + if not task.done(): + await worker.cancel() + await task + + +def _decode_recordings(rows): + spans = {row["span_id"]: row for row in rows} + recordings = {} + for owner in rows: + for descriptor in owner.get("metadata", {}).get("audio.recordings", []): + assert descriptor["state"] == "ready", descriptor + attachment = descriptor["attachment"] + value = spans[attachment["span_id"]] + for part in attachment["ref"].strip("/").split("/"): + part = part.replace("~1", "/").replace("~0", "~") + value = value[int(part)] if isinstance(value, list) else value[part] + with wave.open(io.BytesIO(value.data)) as audio: + rate = audio.getframerate() + channels = audio.getnchannels() + samples = np.frombuffer(audio.readframes(audio.getnframes()), dtype=" rate * 2 + assert np.any(samples[: -rate * 2, 1]), "agent speech must survive shutdown" + assert not np.any(samples[-rate * 2 :, 1]), "closing silence must be retained" + + +@pytest.mark.vcr(match_on=["method", "uri", "voice_payload"]) +@pytest.mark.asyncio +async def test_cascade_voice_conversation(memory_logger, request): + setup_pipecat(capture_audio_attachments=True, audio_format="wav", recording_options=RecordingOptions()) + init_test_logger("test-project-pipecat-py-tracing") + tool_calls = [] + + async def lookup_order(params): + tool_calls.append(params.arguments) + await params.result_callback({"order_id": params.arguments["order_id"], "delivery": "Friday"}) + + context = LLMContext( + messages=[ + { + "role": "system", + "content": "You are an English order assistant. Always call lookup_order for the supplied order number. " + "After its result, answer only: Your order arrives Friday.", + } + ], + tools=ToolsSchema( + standard_tools=[ + FunctionSchema( + name="lookup_order", + description="Look up an order", + properties={"order_id": {"type": "string"}}, + required=["order_id"], + handler=lookup_order, + ) + ] + ), + ) + pair = LLMContextAggregatorPair(context) + params = TransportParams(audio_in_enabled=True, audio_out_enabled=True) + source, output = MemoryInput(params), MemoryOutput(params) + key = os.environ["OPENAI_API_KEY"] + stt = OpenAISTTService(api_key=key) + llm = OpenAILLMService(api_key=key, settings=OpenAILLMService.Settings(model="gpt-4.1-mini", temperature=0)) + tts = OpenAITTSService( + api_key=key, + settings=OpenAITTSService.Settings(model="gpt-4o-mini-tts", voice="alloy"), + ) + worker = _make_worker( + Pipeline([source, stt, pair.user(), llm, tts, output, pair.assistant()]), + params=PipelineParams( + audio_in_sample_rate=16000, audio_out_sample_rate=24000, enable_metrics=True, enable_usage_metrics=True + ), + ) + finished = asyncio.Event() + + @pair.assistant().event_handler("on_assistant_turn_stopped") + async def turn_finished(aggregator, message): + if "friday" in message.content.lower(): + finished.set() + + @worker.event_handler("on_pipeline_started") + async def feed(worker, frame): + # A fixed recording and speech boundaries are the test input. Recognition, + # model/tool behavior, synthesis and turn callbacks all run through Pipecat. + with wave.open(str(Path(__file__).parent / "voice/fixtures/order.wav")) as audio: + pcm = audio.readframes(audio.getnframes()) + await source.push_frame(VADUserStartedSpeakingFrame()) + for offset in range(0, len(pcm), 640): + await source.push_audio_frame(InputAudioRawFrame(pcm[offset : offset + 640], 16000, 1)) + await asyncio.sleep(0.02) + await source.push_frame(VADUserStoppedSpeakingFrame()) + + await _run_conversation(worker, finished) + rows = memory_logger.pop() + assert not [r for r in rows if r.get("error")] + assert len(tool_calls) == 1 + assert "1042" in tool_calls[0]["order_id"].replace(" ", "") + root = _single_span(rows, "pipecat.pipeline") + recordings = _decode_recordings(rows) + _assert_shutdown_audio(output, root, recordings) + user = _single_span(rows, "user_turn") + recognition = _single_span(rows, "stt") + tool = _single_span(rows, "lookup_order") + synthesis = _single_span(rows, "tts") + models = [r for r in rows if r.get("span_attributes", {}).get("name") == "llm_response"] + assert models + assert recognition["span_parents"] == [user["span_id"]] + assert any(tool["span_parents"] == [r["span_id"]] for r in models) + assert any( + r["span_id"] not in tool["span_parents"] and "friday" in json.dumps(r["output"]).lower() for r in models + ), "a model response must deliver the tool result to the caller" + assert "friday" in synthesis["metadata"]["pipecat.text"].lower() + for model in models: + assert model["metadata"]["model"] == "gpt-4.1-mini" + assert model["metrics"]["tokens"] > 0 + _assert_selections(user, root, recordings, 0) + _assert_selections(synthesis, root, recordings, 1) + request.node.voice_recording_validated = True + + +@pytest.mark.asyncio +async def test_realtime_voice_conversation(memory_logger, monkeypatch, request, vcr_cassette_dir): + from braintrust.integrations.pipecat._test_websocket import WebSocketCassette + from pipecat.audio.utils import create_stream_resampler + from pipecat.frames.frames import LLMRunFrame + from pipecat.services.openai.realtime import events + from pipecat.services.openai.realtime import llm as realtime_module + + cassette = WebSocketCassette( + Path(vcr_cassette_dir) / "test_realtime_voice_conversation.json", + record=request.config.getoption("--vcr-record") == "all", + ) + original_connect = realtime_module.websocket_connect + + async def connect(**kwargs): + return await cassette.connect(original_connect, **kwargs) + + monkeypatch.setattr(realtime_module, "websocket_connect", connect) + setup_pipecat(capture_audio_attachments=True, audio_format="wav") + init_test_logger("test-project-pipecat-py-tracing") + tool_calls = [] + + async def lookup_order(params): + tool_calls.append(params.arguments) + await params.result_callback({"order_id": params.arguments["order_id"], "delivery": "Friday"}) + + context = LLMContext( + messages=[ + { + "role": "system", + "content": "Speak English. Start by saying exactly: Hello, what is your order number? " + "When the user gives an order number, always call lookup_order. " + "After its result, say only: Your order arrives Friday.", + } + ], + tools=ToolsSchema( + standard_tools=[ + FunctionSchema( + name="lookup_order", + description="Look up an order", + properties={"order_id": {"type": "string"}}, + required=["order_id"], + handler=lookup_order, + ) + ] + ), + ) + pair = LLMContextAggregatorPair(context) + params = TransportParams(audio_in_enabled=True, audio_out_enabled=True) + source, output = MemoryInput(params), MemoryOutput(params) + service = realtime_module.OpenAIRealtimeLLMService( + api_key=os.environ["OPENAI_API_KEY"], + settings=realtime_module.OpenAIRealtimeLLMService.Settings( + model="gpt-realtime", + session_properties=events.SessionProperties( + audio=events.AudioConfiguration( + input=events.AudioInput( + transcription=events.InputAudioTranscription(model="gpt-4o-mini-transcribe", language="en"), + turn_detection=events.TurnDetection(), + ), + output=events.AudioOutput(voice="alloy"), + ) + ), + ), + ) + worker = _make_worker( + Pipeline([source, pair.user(), service, output, pair.assistant()]), + params=PipelineParams( + audio_in_sample_rate=24000, audio_out_sample_rate=24000, enable_metrics=True, enable_usage_metrics=True + ), + ) + greeted, finished = asyncio.Event(), asyncio.Event() + + @pair.assistant().event_handler("on_assistant_turn_stopped") + async def turn_finished(aggregator, message): + if "friday" in message.content.lower(): + finished.set() + else: + greeted.set() + + @worker.event_handler("on_pipeline_started") + async def feed(worker, frame): + await worker.queue_frame(LLMRunFrame()) + await asyncio.wait_for(greeted.wait(), 20) + with wave.open(str(Path(__file__).parent / "voice/fixtures/order.wav")) as audio: + pcm = audio.readframes(audio.getnframes()) + pcm = await create_stream_resampler().resample(pcm, 16000, 24000) + pcm += b"\0" * 48000 # Server VAD detects the end of the spoken request. + for offset in range(0, len(pcm), 960): + await source.push_audio_frame(InputAudioRawFrame(pcm[offset : offset + 960], 24000, 1)) + await asyncio.sleep(0.02) + + await _run_conversation(worker, finished) + cassette.verify() + rows = memory_logger.pop() + assert not [r for r in rows if r.get("error")] + assert len(tool_calls) == 1 + assert "1042" in tool_calls[0]["order_id"].replace(" ", "") + root = _single_span(rows, "pipecat.pipeline") + recordings = _decode_recordings(rows) + _assert_shutdown_audio(output, root, recordings) + user = _single_span(rows, "user_turn") + tool = _single_span(rows, "lookup_order") + models = [r for r in rows if r.get("span_attributes", {}).get("name") == "llm_response"] + assert models + assert any(tool["span_parents"] == [r["span_id"]] for r in models) + assert any( + r["span_id"] not in tool["span_parents"] and "friday" in json.dumps(r["output"]).lower() for r in models + ), "a model response must deliver the tool result to the caller" + _assert_selections(user, root, recordings, 0) + for model in models: + assert model["metadata"]["provider"] == "openai" + assert model["metadata"]["model"].startswith("gpt-realtime") + assert model["metrics"]["tokens"] > 0 + assert model["metadata"].get("pipecat.text") != "" + assert model["output"] + assert not [r for r in rows if r.get("span_attributes", {}).get("name") in ("stt", "tts")] + audio_spans = [r for r in rows if r.get("span_attributes", {}).get("name") == "pipecat.audio_output"] + assert audio_spans + for audio_span in audio_spans: + assert audio_span["metadata"]["openai.response.id"] + _assert_selections(audio_span, root, recordings, 1) + if cassette.record: + cassette.save() diff --git a/py/src/braintrust/integrations/pipecat/test_websocket_cassette.py b/py/src/braintrust/integrations/pipecat/test_websocket_cassette.py new file mode 100644 index 000000000..166635e98 --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/test_websocket_cassette.py @@ -0,0 +1,51 @@ +"""Guard the recorder itself: changed requests must never silently replay.""" + +import json + +import pytest + +from ._test_websocket import WebSocketCassette + + +@pytest.mark.asyncio +async def test_websocket_cassette_preserves_ids_and_rejects_changed_payload(tmp_path): + path = tmp_path / "socket.json" + path.write_text( + json.dumps( + { + "endpoint": "wss://example.test", + "events": [ + { + "direction": "send", + "message": { + "type": "conversation.item.create", + "event_id": "old-event", + "item": {"id": "old-item", "text": "hello"}, + }, + }, + { + "direction": "receive", + "message": {"type": "conversation.item.created", "item": {"id": "old-item"}}, + }, + { + "direction": "send", + "message": {"type": "response.create", "response": {"modalities": ["audio"]}}, + }, + ], + } + ) + ) + cassette = WebSocketCassette(path) + await cassette.send( + json.dumps( + {"type": "conversation.item.create", "event_id": "new-event", "item": {"id": "new-item", "text": "hello"}} + ) + ) + assert json.loads(await anext(cassette))["item"]["id"] == "new-item" + with pytest.raises(AssertionError, match="not fully consumed"): + cassette.verify() + with pytest.raises(AssertionError, match="request mismatch"): + await cassette.send(json.dumps({"type": "response.create", "response": {"modalities": ["text"]}})) + with pytest.raises(AssertionError, match="replay failed"): + cassette.verify() + await cassette.close() diff --git a/py/src/braintrust/integrations/pipecat/voice/fixtures/README.md b/py/src/braintrust/integrations/pipecat/voice/fixtures/README.md index 759efe020..80c51cd54 100644 --- a/py/src/braintrust/integrations/pipecat/voice/fixtures/README.md +++ b/py/src/braintrust/integrations/pipecat/voice/fixtures/README.md @@ -1,5 +1,20 @@ -# Recorded realtime responses +# Voice cassette maintenance -`realtime-responses.json` contains the native OpenAI response objects observed during the synthetic DEMO-1042 call on September 30, 2026, using Pipecat 1.12.0 and `gpt-realtime`. It includes the greeting, tool request, and spoken continuation. These are exported provider response objects, not complete WebSocket traffic. No credentials or audio bytes are included. +Run from `py/`. Replay needs no credentials: -Trace row: `bbffcf29-8862-4ee1-bd78-8365c1a20382`, project `voice-sdk-instrumentation-pipecat`. +```sh +mise exec -- uv run nox -s 'test_pipecat(latest)' -- --vcr-record=none +mise exec -- uv run nox -s 'test_pipecat(1.3.0)' -- --vcr-record=none +``` + +To re-record the voice conversations, set `OPENAI_API_KEY` in your shell, then run: + +```sh +mise exec -- uv run nox -s 'test_pipecat(latest)' -- --vcr-record=all -k voice_conversation +``` + +- Re-recording makes short, billable OpenAI calls. No other API keys are needed. Existing fixtures are replaced only after the conversation passes its assertions. +- HTTP uses VCR; Realtime uses `_test_websocket.py` to record/replay WebSocket messages. `_test_audio_cassettes.py` stores audio separately and restores exact bytes for replay. +- Commit YAML/JSON manifests **and** companion `.audio/` directories under `pipecat/cassettes/latest/` together. Re-recording regenerates these WAVs; do not edit or compress them manually. WebSocket offsets refer to PCM bytes, excluding the WAV header. +- `order.wav` is the fixed caller input: 16 kHz mono PCM16, “Where is my order number one zero four two?”, generated with macOS's Samantha voice. Changing it requires re-recording both conversations. +- After re-recording, review the fixture diff and run replay with `--vcr-record=none` before committing. Missing or truncated audio files fail replay. diff --git a/py/src/braintrust/integrations/pipecat/voice/fixtures/order.wav b/py/src/braintrust/integrations/pipecat/voice/fixtures/order.wav new file mode 100644 index 0000000000000000000000000000000000000000..d7230f1ec723c54579bd841b37be74a7eebacde7 GIT binary patch literal 83666 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

(Q#;>;Yx{NA)^i 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 Date: Mon, 5 Oct 2026 22:44:27 -0400 Subject: [PATCH 10/18] Added: Pipecat runnable examples --- examples/README.md | 1 + examples/pipecat/README.md | 28 ++++++++ examples/pipecat/cascade.py | 62 +++++++++++++++++ examples/pipecat/common.py | 115 ++++++++++++++++++++++++++++++++ examples/pipecat/order.wav | Bin 0 -> 83666 bytes examples/pipecat/pyproject.toml | 12 ++++ examples/pipecat/realtime.py | 50 ++++++++++++++ 7 files changed, 268 insertions(+) create mode 100644 examples/pipecat/README.md create mode 100644 examples/pipecat/cascade.py create mode 100644 examples/pipecat/common.py create mode 100644 examples/pipecat/order.wav create mode 100644 examples/pipecat/pyproject.toml create mode 100644 examples/pipecat/realtime.py diff --git a/examples/README.md b/examples/README.md index c9bb52e28..76355619b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -79,6 +79,7 @@ Unless noted otherwise, every example below uses `braintrust.auto_instrument()`. | `openai_agents/` | OpenAI Agents SDK `Runner` running an agent | | `openrouter/` | OpenRouter chat completion routed to OpenAI | | `otel/` | OpenTelemetry interop — `BraintrustSpanProcessor`, filtering, distributed tracing | +| [pipecat/](pipecat/) | Cascade and realtime voice agents with tool calls and recordings — uses `setup_pipecat()` | | `pydantic_ai/` | Pydantic AI agent run inside a `start_span` for a permalink | | `strands/` | Strands `Agent` against `gpt-4o-mini` | | `temporal/` | Distributed Temporal workflow tracing via `BraintrustPlugin` | diff --git a/examples/pipecat/README.md b/examples/pipecat/README.md new file mode 100644 index 000000000..3693a0319 --- /dev/null +++ b/examples/pipecat/README.md @@ -0,0 +1,28 @@ +# Pipecat voice tracing + +Two small voice agents using real OpenAI services and the local Braintrust SDK: + +| Example | Pipeline | +| --- | --- | +| `cascade.py` | Speech → OpenAI transcription → LLM + order lookup → synthesized speech | +| `realtime.py` | Speech → OpenAI Realtime + order lookup → speech | + +Requires Python 3.11–3.13, `uv`, `OPENAI_API_KEY`, and `BRAINTRUST_API_KEY` in your environment. From the repository root, run either command: + +```sh +uv run --project examples/pipecat python examples/pipecat/cascade.py +uv run --project examples/pipecat python examples/pipecat/realtime.py +``` + +Each command installs its dependencies, runs one conversation, and prints a trace link in the `example-pipecat` project. OpenAI usage is billable. + +The bundled `order.wav` (about 82 KiB) asks “Where is my order number one zero four two?” `common.py` supplies file input and silent output so no microphone, speaker, browser, or phone setup is needed. The local `lookup_order` tool returns a fictional delivery date. Listen to the captured audio in the trace. + +Instrumentation is enabled with: + +```python +logger = braintrust.init_logger(project="example-pipecat") +setup_pipecat(capture_audio_attachments=True) +``` + +Pipecat metrics are enabled in `PipelineParams`. The trace includes turns, model calls, tool execution, metrics, and Ogg audio attachments. Set `capture_audio_attachments=False` to trace without recording audio. diff --git a/examples/pipecat/cascade.py b/examples/pipecat/cascade.py new file mode 100644 index 000000000..2c5aa3169 --- /dev/null +++ b/examples/pipecat/cascade.py @@ -0,0 +1,62 @@ +"""Audio → transcription → model/tool → synthesized speech, traced by Braintrust.""" + +import asyncio +import os + +import braintrust +from braintrust.integrations.pipecat import setup_pipecat +from common import INSTRUCTIONS, RecordedInput, SilentOutput, order_context, run_conversation + +# Pipecat requires Python 3.11+ and is installed by this example, not the shared lint environment. +# pylint: disable=import-error +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.pipeline.pipeline import Pipeline +from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair, LLMUserAggregatorParams +from pipecat.services.openai.llm import OpenAILLMService +from pipecat.services.openai.stt import OpenAISTTService +from pipecat.services.openai.tts import OpenAITTSService +from pipecat.transports.base_transport import TransportParams + + +async def main(): + logger = braintrust.init_logger(project="example-pipecat") + setup_pipecat(capture_audio_attachments=True) + + api_key = os.environ["OPENAI_API_KEY"] + transport = TransportParams(audio_in_enabled=True, audio_out_enabled=True) + source, output = RecordedInput(transport), SilentOutput(transport) + aggregators = LLMContextAggregatorPair( + order_context(), + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) + pipeline = Pipeline( + [ + source, + OpenAISTTService(api_key=api_key), + aggregators.user(), + OpenAILLMService( + api_key=api_key, + settings=OpenAILLMService.Settings( + model="gpt-4.1-mini", + system_instruction=INSTRUCTIONS, + ), + ), + OpenAITTSService( + api_key=api_key, + settings=OpenAITTSService.Settings( + model="gpt-4o-mini-tts", + voice="alloy", + ), + ), + output, + aggregators.assistant(), + ] + ) + with logger.start_span(name="cascade") as trace: + await run_conversation(pipeline, aggregators, source) + await asyncio.to_thread(logger.flush) + print(f"Trace: {trace.link()}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/pipecat/common.py b/examples/pipecat/common.py new file mode 100644 index 000000000..339cdec9a --- /dev/null +++ b/examples/pipecat/common.py @@ -0,0 +1,115 @@ +"""Example application plumbing: play a WAV instead of opening a microphone.""" + +import asyncio +import wave +from pathlib import Path + +# Pipecat requires Python 3.11+ and is installed by this example, not the shared lint environment. +# pylint: disable=import-error +from pipecat.adapters.schemas.function_schema import FunctionSchema +from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.audio.utils import create_stream_resampler +from pipecat.frames.frames import InputAudioRawFrame, LLMRunFrame +from pipecat.pipeline.worker import PipelineParams, PipelineWorker +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.transports.base_input import BaseInputTransport +from pipecat.transports.base_output import BaseOutputTransport +from pipecat.workers.runner import WorkerRunner + + +async def lookup_order(params): + """A fictional local order database; replace with your application's tool.""" + await params.result_callback({"order_id": params.arguments["order_id"], "delivery": "Friday"}) + + +def order_context(): + return LLMContext( + tools=ToolsSchema( + standard_tools=[ + FunctionSchema( + name="lookup_order", + description="Look up an order's delivery date", + properties={"order_id": {"type": "string"}}, + required=["order_id"], + handler=lookup_order, + ) + ] + ), + ) + + +INSTRUCTIONS = ( + "Speak English. Greet the caller briefly and ask for their order number. " + "When they supply it, always call lookup_order. " + "After its result, say only: Your order arrives Friday." +) + + +class RecordedInput(BaseInputTransport): + async def start(self, frame): + await super().start(frame) + await self.set_transport_ready(frame) + + async def play(self, sample_rate): + with wave.open(str(Path(__file__).with_name("order.wav"))) as audio: + pcm = audio.readframes(audio.getnframes()) + pcm = await create_stream_resampler().resample(pcm, 16000, sample_rate) + # Include silence around the request so native VAD determines its boundaries. + pcm = b"\0" * sample_rate + pcm + b"\0" * (sample_rate * 2) + frame_bytes = sample_rate // 50 * 2 + for offset in range(0, len(pcm), frame_bytes): + await self.push_audio_frame(InputAudioRawFrame(pcm[offset : offset + frame_bytes], sample_rate, 1)) + await asyncio.sleep(0.02) + + +class SilentOutput(BaseOutputTransport): + """Accept agent audio without requiring an audio device; listen in the trace.""" + + async def start(self, frame): + await super().start(frame) + await self.set_transport_ready(frame) + + async def write_audio_frame(self, frame): + return True + + +async def run_conversation(pipeline, aggregators, source, *, realtime=False): + sample_rate = 24000 if realtime else 16000 + worker = PipelineWorker( + pipeline, + params=PipelineParams( + audio_in_sample_rate=sample_rate, + audio_out_sample_rate=24000, + enable_metrics=True, + enable_usage_metrics=True, + ), + ) + greeted, finished = asyncio.Event(), asyncio.Event() + + @aggregators.assistant().event_handler("on_assistant_turn_stopped") + async def assistant_turn(aggregator, message): + print(f"Agent: {message.content}", flush=True) + if "friday" in message.content.lower(): + finished.set() + else: + greeted.set() + + @aggregators.user().event_handler("on_user_turn_message_added") + async def user_turn(aggregator, message): + print(f"Caller: {message.content}", flush=True) + + @worker.event_handler("on_pipeline_started") + async def started(worker, frame): + await worker.queue_frame(LLMRunFrame()) + await asyncio.wait_for(greeted.wait(), 30) + await source.play(sample_rate) + + task = asyncio.create_task(WorkerRunner(handle_sigint=False).run(worker)) + try: + await asyncio.wait_for(finished.wait(), 60) + await worker.stop_when_done() + await task + finally: + if not task.done(): + await worker.cancel() + await task diff --git a/examples/pipecat/order.wav b/examples/pipecat/order.wav new file mode 100644 index 0000000000000000000000000000000000000000..d7230f1ec723c54579bd841b37be74a7eebacde7 GIT binary patch literal 83666 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

(Q#;>;Yx{NA)^i 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 Date: Tue, 6 Oct 2026 09:18:42 -0400 Subject: [PATCH 11/18] Fixed: Realtime audio test cassette mismatch --- .../pipecat/test_voice_pipeline.py | 7 ++++--- .../pipecat/voice/fixtures/README.md | 2 +- .../pipecat/voice/fixtures/order-24khz.wav | Bin 0 -> 118844 bytes 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 py/src/braintrust/integrations/pipecat/voice/fixtures/order-24khz.wav diff --git a/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py b/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py index ee42e60d4..3f019d8cc 100644 --- a/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py +++ b/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py @@ -270,7 +270,6 @@ async def feed(worker, frame): @pytest.mark.asyncio async def test_realtime_voice_conversation(memory_logger, monkeypatch, request, vcr_cassette_dir): from braintrust.integrations.pipecat._test_websocket import WebSocketCassette - from pipecat.audio.utils import create_stream_resampler from pipecat.frames.frames import LLMRunFrame from pipecat.services.openai.realtime import events from pipecat.services.openai.realtime import llm as realtime_module @@ -351,9 +350,11 @@ async def turn_finished(aggregator, message): async def feed(worker, frame): await worker.queue_frame(LLMRunFrame()) await asyncio.wait_for(greeted.wait(), 20) - with wave.open(str(Path(__file__).parent / "voice/fixtures/order.wav")) as audio: + # Fixed wire-rate input keeps strict cassette matching independent of + # platform-specific resampler rounding. Recording uses this same input. + with wave.open(str(Path(__file__).parent / "voice/fixtures/order-24khz.wav")) as audio: + assert (audio.getframerate(), audio.getnchannels(), audio.getsampwidth()) == (24000, 1, 2) pcm = audio.readframes(audio.getnframes()) - pcm = await create_stream_resampler().resample(pcm, 16000, 24000) pcm += b"\0" * 48000 # Server VAD detects the end of the spoken request. for offset in range(0, len(pcm), 960): await source.push_audio_frame(InputAudioRawFrame(pcm[offset : offset + 960], 24000, 1)) diff --git a/py/src/braintrust/integrations/pipecat/voice/fixtures/README.md b/py/src/braintrust/integrations/pipecat/voice/fixtures/README.md index 80c51cd54..8e2b1f43b 100644 --- a/py/src/braintrust/integrations/pipecat/voice/fixtures/README.md +++ b/py/src/braintrust/integrations/pipecat/voice/fixtures/README.md @@ -16,5 +16,5 @@ mise exec -- uv run nox -s 'test_pipecat(latest)' -- --vcr-record=all -k voice_c - Re-recording makes short, billable OpenAI calls. No other API keys are needed. Existing fixtures are replaced only after the conversation passes its assertions. - HTTP uses VCR; Realtime uses `_test_websocket.py` to record/replay WebSocket messages. `_test_audio_cassettes.py` stores audio separately and restores exact bytes for replay. - Commit YAML/JSON manifests **and** companion `.audio/` directories under `pipecat/cassettes/latest/` together. Re-recording regenerates these WAVs; do not edit or compress them manually. WebSocket offsets refer to PCM bytes, excluding the WAV header. -- `order.wav` is the fixed caller input: 16 kHz mono PCM16, “Where is my order number one zero four two?”, generated with macOS's Samantha voice. Changing it requires re-recording both conversations. +- `order.wav` is the cascade caller input: 16 kHz mono PCM16, “Where is my order number one zero four two?”, generated with macOS's Samantha voice. Realtime uses `order-24khz.wav`, a pre-resampled copy of that request. Keeping its input bytes fixed avoids platform-dependent resampler rounding during strict WebSocket replay. Changing either input requires re-recording its conversation. - After re-recording, review the fixture diff and run replay with `--vcr-record=none` before committing. Missing or truncated audio files fail replay. diff --git a/py/src/braintrust/integrations/pipecat/voice/fixtures/order-24khz.wav b/py/src/braintrust/integrations/pipecat/voice/fixtures/order-24khz.wav new file mode 100644 index 0000000000000000000000000000000000000000..6cc6ff3936dceb6a3b45d41ed33079f1c8d00291 GIT binary patch literal 118844 zcmXVY1$5h3*R5oBn3=KRq+w{%HYqbRGt-ornVFfHJEbX4J7s1FnZ#(>7QFj^>#e(D zD~c>jNB17sd!KY})x3G$8yM=*qm%6sLb@(GNu;K=V_{7`-?f0SyaMyXe< zVAvE)p%qr)6}uuSvZ5$3q=qz*4$?!${|z_D6Y_@qpdcs|ih!b_SSS%ng)*TWC>JV# zilGvy42*K90%{CK6Q~lZf|`NR9BKizf?9*o_WzGIP#dtPIrw`O)D#@s2zUamAY~wU9RhwI2KJ6t#)BjN0!PkM7Awn@HOeMshq6~W z1kQF|xuVS|TN_93rR4WpY<}jJ#OhC*P94$}TxZX{L+=mVTg+N;K3C zngg8&*BA=7hnK;3VFpfB4OQ(`eNY+Ijny;N7u5CYP)&QyBFz;|wZ>ChpdFxHsy(57 zr2Vbsv_@T+Eiq@iiu05$;qV271prtfdHB&V?8k>5*x}DmtI;d)< zst1;fga3hgLkxJ*@yczv8@S)qQi}9M94@+x7hK(3kn5bYpVQa*+_B2h#^LAqC7cl! z2?K-*AyM!ZRD#{k*#*07HwZyOw2&ec3ax}b!e7Eh;ezl=zyu#ho};&8k>iBpnZxGr zbrw1MIOjSKIsbEV&O}#F*IL&zmxtI@JR~~AKGFjz9oQyB*{(!EC!r$v3EWFXsn)79 zG;cJMv_85kx?Xyxey5?t_{})Y&C~s;d$GsA9vwYDcy{x8NXCTrJd z4yv!Hp26QCRFPyaIYufJySOGhcRQX5E_;D}CjXEN;-<0HOn2rpJ&-n2<0ut%n(R&L z$@|1cViZwML=rxP8=)gSz*audlUPh#B&vxpvOjr{tR~~B5!6Y_N~O|c>ErYlTF;a* ze=(<+pNuctmR-w!U=z8e9L`$#;WIN zR%qAiw(3tB?il}b``}*dfqGiKOx|Xn@4gTGj`}YQXdmbs^ekv}un@c;gbQgGx*_y& zs5ul3{StaSbYQ3|bZbap$b#U{LD4~d1D6Ku@jvhP*!P9cZSUh=J3JS8^mi|E^EaCH zf9uw32Wc|Y68r%=uFR0zOR1vHWpTV09^3Em_qorkgGpir)8{CN>O=lZG$tP4J#jUD z5gUL-VkG(m-G?qi=c2RG`RHc!GWrAc!a8C*us>Ksd;^Z-9f+Gm3b~%-$syFgR1&>_ zeoKck1DHe1cgCG9Vu!LT*|Y2$mS-cm*4%RLD_726;PdTY>^p?9j?T`Ot`6cDX_x#( ziGT;F4ykcXscxSBoW9yia%Eg??xKKL>Du`rv?&kWlw9GVDqC!idnw z{gHuDW1|j4{SC&>sBTeYb2Fg zgNNSznz4^T)}7S$(YUEE!+jt(<(@QE%yv0}UH-P8;ZJZE*%yqH&Y;IrkI7VW9RU%` z@gV#VR*2Q0yU^}v6zW8(k(bCrzM7ag>hHnBI8HK&yOD-pB4Wtu4mkd*mp7Cqn}3Yj_ehoglz~74A~f@3LNAA z$+xx79j_eEEACZp?+nBB2HgcsSG7U)A2eUdk}>hDYlt(!;jlmDH*yo${!BZ%Ej5bV zPCN%TnGP&62vwmskSRz3;)YmluWgTP4{Xv`!?G#+kZCP7Kn5~ z4giZaMX#Xw*fp#Se~4FsS(HxhAYEj4u)Z*=0o|WoOCP3>(EI3(^cXsic2MW3u9Sv) zOI{_fkblTx>LIm>KFK)P`FtCpt#gX_NX~=LsZ(_43=Q4)dYZh0{4xV#gX)9lhdRRg zMC^;a8FeH2RLs`cNpa2N!xNB%Q;B_&^vU~^LmEtLaJj(^@ajzNlsq-5b>g#lV_ayA z6nQ*6C3H&=;os8tZ?85Ul<};7r}myYUUeHOR{yYRS&GV51E2C!6I=TQAZpl%cx_NgQ}wE(>v%*^kljz9YoXA z9cm2aN8Kd{kQSmp@dj^(zrvvaV(B z$Pm&#rBo(&OVGu(iYyNG48(jGk6hyg?J(77rB?jlEODp=cY6uHk1OXyuk{ibYd4!3IfG=NtI=O*8MYBSgN?>ebTaCL z?nhKeC)<7N2x~*D%KFn{wv<_aSWnodAhGCUbS~BzFC>D=FXTAtEATx7=tcBOdNw@; zc%k*+y@aj_Ll5p*$=Z{W*c%o=CsbO%@gvg3ZR0a z`EByD8eY!Sq&`X*8vQ7AlfTtti0%ON&h^CJmR&%dBHrVda2g*=1d{)eZK=^zJhg#5 zLG;8gqZV6$wbHz@KBz9b_HNC`ng+F3YyYn6YRb2Kv&JEP(fgP?ah*6qBoKS>ID8Ry z3_XiHu{o@z*3*_{mKw_(tHtVT>un1~B7tX)!dhcy48_k9e-i^i>|%*Jax3+WdQExK zOXy#;o#ttgcG4ff{OL^_=o!=*(oFOtKI7l9O7xUXwjQ!&q6R#XROn>;O|gx-#4Xb| zE2Lla=A_0M`x}-ObS~Ooe6M76X<=!<;`0TI@{Z>|&-Kkq&fA+8TQIliN=a?$uTp!- z$YQD>D)(_lQL-*(S4gVwZ)2dkt3(LDIe`gfvY90I0@utw2ILmq9bpbw_`|vA&qNK{ zz&68NTlcHRTrE}qTcfGFU5}Xu+b*Gt@cYDS(n3Bbn~;NvRD3=9#MZ<*($dJ%-O|f4 z%hJU9&^8#IfW5)I@T2$y;vrE0=2|YsqX8I>4nRd@D7p*N5}xEfGM_4@7c=Wvnf<}e zW|LWjIRSj`bKvRcQ!II&+(`~5PZ0z0@90uwoNb{s$jVx;pc&L|ewcV%^~Om19tvv` z-#Kk)L$UBlX?-JWdFu+#3Vr#6GOi@713U~oPP`L_`OUD6-Kncwj!mr zlU7e#95NMsgLNjND4b4U2e2O*iI%7%^Rb+%1*n-KLeMDdsTi8QUpv^hqq4 z@FpJ+2k^sK8w|#(5tVI&Wr1a))f;%DrPc?w#aJ3yL{*cQ$SYJc#=>Zsxzr-EF=-&j z5jXITcmVzwtizUgB{7t+Ve61n))SV0%o|M$OuMZ<l5fkJQ+cmW~hbtIg zw!bmmWJlwLy(kVm_%pHrzi^(JOO|~_E(9Q{JXRPyy(1!oWbfm9>oC~5hldHknc|)IJ=kZ3t z(=pKD@8}?S+0(i6w1pUqO-0PsLdz&qVSS7GxhBcXTY6gWS`XT;fH>oZ&O=fVPvohs zvF)>!w;r|~K^~$K)}Po<_N5+yIHF=JxbfU^_7{UN7uf{vJR8H#XUA|CIUQHUwqQlJ zoU=0zsWD_%@+k2PU1ynD|F|BvP9Z1TPszFZO5Y{nD-tfHwack2$SZAAp=}b{_)Mcg zr8i5)m2IdP(=@xvuUSg7epPFmCRMC2u4p(a^=aJr@YsMU9&0q8q)U#g_O|>s_A`Bh z+Dz3^E2%W{5#AENjPE8clgq&R6{&ydLiQ_H%Pag3ew#hjk?Gv&@E3yY$@Vt(raZ+= zp{^0j@C0lq^4NODBAfS`Zn?U4On{vW=`j@!e9p7wAM;U07S`LAFdzoCN3 zE_i2jJ<SEZ%<>j!#;LqdeFnTf7iVCHVFuvSjE{Q8-Z!u4psf;g;5(-A6Q_-D>m+i7;n#I#P z-dbi&v_7zIwk@)avmOI?|HHNb`PUX{Qz3!a8hkqb6f@!@h=0feDubF%rPKFlZ;-uS zW%qH%xdLticYr@_A7xMGhj9`YXYXro&Qt7imSw{@nB7P%!eMkc@(EkZoN_(YaGu|S zq^RP=Ee$TEyi5I^v8ka@@S^x$X-;`$leEgMm6etIDvwm{ufm)BQ?{qzX!gg{UkRbn z6=8>i{`3Fr6XiLmbJC5)VAI_ z$lBPp25Ey1MQ@>ItTl0$NGG1)i;1t~c#0-hkR2#9{hL|L+@wFzG$XKq+*Y;=yPGv| zIb0kU!ufC}c0W4-k5E2G=QpHFC;xGHH+gCS{? zv*zZ1C{voAYerY)Rjz4zq3P!)RTTkcBZ@B;3~30bTM{(UhVbl=L4kpO*kZZ(FrN5B{ zgbD3r!!2v9f-MYLY;~9;EQQu$>vFTk^ue^>`UshexU5gCd%;sXhNs~O_7(3ydQ-Pa zg;+~02btCl@(Z<$PNTa~ZK)abA*M02hFV3Qr+9{9FEAt}QNNjv+)8#7-ICJLLCgSp zCy|97wc0GK?F~7~`9?d%M<2Q|`dj>)gl2J{qTWU=ODNCGEK)aq(ahYceVdCd?=)>) zrYo9OFs5K#0h)gyw=nBPgSWAd!wtc#pV51WyP!|hv{d#x#;^(0MAAyFWqs`(1*?4; z{~z0&@uN>tr)V{I(%#N7+%XblT)Vk8>>jXs>)2l04)zJXn(U8vME?R&vaz+&))QHd z9JBSauC&asJhZ^p0oFOT<>)p1A5h_@0}D(eK9kAR333hDjmoCA^iT3L@r)QpiePUE zHJIu}{UpnQ<;Ro1Nfcz?Pszc+BYmOj$y#C#u@B6jF~k&n7y8(G%9K#w&)f*VEYW5RF0&J<*Ti;mKNDw{gmnVV@8z8AMz(@|$Ufq)IsC<= zVx{Yfkieg2@3F7A9eh*X$=~-k8{0mZGOSTO~UZXv*{b)FH%<8a=v|g}9qeg5N zR)W7G=2E|@)#NYy9NvTMOh2a=Q6k8>Hj^ztJ|GbTK=w5i)ctwXR`M7TM$9GFknJcf z6-4eLyvQ?T2l6MVfWojDSX=B8^4oH+enf3>?HrQ{zvgln3xdu>O^y#tycF+>-X1YN z@@c~GoY@uSZSp%ucEQ_Cs1l2><$O*LN=r-Uvg-4y3PFB7bF3L!V=J*evE`$Eupn#-a^0F_{b}_;JW)4n2Hu`DQ@iO(dNSorEujX{ z7WxUuq&tAh>mnUbccx5aIJJX%Kn08Ki|@{NpV)DH zvxCKXSux4ZxQhwz(mv3FL zq$#K}nqUdo2&Aj^o+ZxK38k=S_)$VljRs!-1)T`I?sIBBJsZ^EXPD*m5$YP%AJn0~ zbR#O7`aq4PL+NyC9yylWPo4$!>Ur`Pxr!V@NLV8*A7juZ2xsv$EvyZxSyt;~TgIQ( zl?GMEer@n~+WFLwq`5KkqyCFe&VEx7+x}3GjNW)x-&P(aozm6u;n7`UekV@PKpOfr zyqdBuqQYC!l*n1mO+tIeP!}Yv75{VK+#Py0wV58ujTD+V9nRgZhvE+@O#Udfl3yyp z@KLD0ywbJAaaq_WoE0VuXYAAY9PTQULygB?Tc=obwj=0KY#933w%xWEd4g=Pg;<+d zHzA+!;nWy9f}ReZ{&2bwWPMy|Hi+sMsumH)-q%CzY0CfPsIvJy|ltctUz zZ7=EFIS{d1tG>2zP;Btzm-f$D9qM za%rh^2w%xurn|C7_?JSOOD%7PWO%0Pld7wFy!yPV6?|EtCB&8BobJFK^PBINVlL7sttt#FL()HTQ>3t5k>qavViC74>CbdFb4)x9jR1uD?Sfvjum5elPMhAN)e$4EcFBT5SPs)6gl$)59eyjLIOGDR;UZp*HwXd!Co~eyq|Fzqxqw9K^} zv7EA8u;{Fmbq+EP^$~6+mnr8deQG`jG4w_3{E$uoz!Iz z3+l;e>K!$mK1ffZyU`Qrp1@o3L^7U_ezzVle>5M!{&F_*7#(FxZ&MIg8dcV*xL5A& z^l>RC(q9)`sQS`zNcZ_&-nLp*mYP`?{kPvtLm%}>O}?AXw_89P-^+##aveXB{)P9l zEjGoO-(e5!yfQ^Csk*}(Aq*MrrWH6z?EdcJU~S z;?A-GTr5A0&*N}98~FOy^cGgjJ!5z(fO<;hf_yLn_z@1EDfg*G%tU$_@fS7))SFHu z-ZIq$qaMy*9zv8~hOMAkBdH>=d`Dqs){qqcRH$L&@;j~kI_~Q*x>d7^mJK!W@BD6P z`il!)qoC)y*>10l2Hhz5tayUoh<7tl)zNjgk=yKD*9fRmBkIfCIvb~IZBPTbK;8~T zYG&)#8Xp+T4ePbOs-seCL7^uTyYQz(KGl&2W`Czr!RU)6h$PVPc@I&~=+y-tjR||5-m(+0TAUzsn{=L|6<{7<^ z@n%OdZ>go^Xi#^R0}Spo)sftSk3o&L^Oi@pg>(a`m+y?Y2iaoD*~WdF=o>Z6&&l}S zU~wv%v%9RWdDnJQyKgPA^0FKyriV`_)lWxrp+L-4?bV&rKh}n;f}|XNB{r#kW%Y~N zzilQu)wu~8t()e?y8Y6(Q}0*qN)x3$@_4wL_NF1&&Cgh(3s$M6NA?{|59%qYr6z(p zNlTka9(!WFP=BiC#h=>WuHU)Uy=rUg^DJMj>um#Uhpm#u3*aNSkY!jdL6HvHiyOgr z<8@$;?5Ed(Oq!>Qn7hmZY^bD`HyD-{pZlO}O0&>52iiVtHMHrX0$Y+ZaE@N59F(;1 zNNp2Cn6aJVtoATe?zl|`SX6_L3iS)9{mFWZsf(qzwVllmwd29G58IT@U|{eZXMv}i z${eTP(0ObyA89|r_vEIqz1TMFdS(#4m)Z|}SpxMHAB)_ucw1j$Yxv{v9#1B8dD8bB zL-DWT$o$I8;*_8Uw^N;2FALU}_iMhs?T_{`Z7wt(moqH(SipZC+YBkXM%sJ2g>Iib zTX~Mv!*XAiLc3YQOs;xgOFy&+eP8%bA{D(dM;zef9a`52alZ6Y4uDk}gYL9eufC&9 z0622JE8JythB<}`ZSDTt0cr!b%i6(|P}jb8Ywf(cBlXWrf16V+f6OM+`TF;D7wfv! zA2Y4Ae6ST_t%xfC{X9&qq1piy7^bqQtJE?21M>#hZWH%6_m;cBRdC1HP<98SVMYL} z93Thdxgb)#vQ(o(*-Ux8F(b$vw>cv+??wLj+yj}-QWho-O8k`aC#R^iuJTXo+z!zl z^sV9>xn(wvp5t$EA7O|yyfj95cz8YWO7#4rC*+~*L?qj^v#wA5O0!}+NL*tB99Lbb z;xDJlp%rcm51bFht5S_LUj9e<0xgHW$sT~2n;_c77h;ubf?#F`QlqyCw+i@ z#%1y8d3`?-FQ3`M+U`$>w6@=yzREWzuMhv%_k!C>-Aiqm!RWEpi}wt6 zd#O$n2eEgtw$_g3G3KV$cW52e#O`#uTqUkI&JV6Z5-0VMev0R$B?m~C* zn}pD413Ztcq;JvT6oya5hma5x%1&VZp*JzRxZd_F_KSQGSITCvUjc6Qn|0D>$zUP| z-+=zGE-|;Y*svS+UFte-M|g+ih1oyyZxy(5i?fPTekBt=b?2Iq$5A8l7}NHNWoeB3Skc1+;4M&&!UDeCUy1-fPsBLU%QeKo^Cy@kga!#UAFcadn^<=n z%;j>^6?2~TjCHr=qA92TPMxlPZhe%gw|S8DCh{3uNjxR{Q-y%}LCM?HVtP1zmby!I zVY+hed@-ndcd+jOS9XcZWPO;q^lyp*xF?4Fwgs9`*8QnZL|)LHT)EnBzNccsGdmP? zFPc!$8AvJyr01seXwW?kYPhcCW7G6jrEMR!Zd=6`(;2C8D}&d2VfuxdrJ7f|2;+9+ z7yWU~V`!9k&EAD+M>Zz}VkI?=xy!W@COTKU-nv?eN2Kw}7U(F@cw|GPfhuIRyhH5n zYT%mfI_{d|GPr`BO@#j3zm$mO+V-37)|S?^s~KPWstz*6n%i5FtbSH+%R>`V|ENB} zM4E~%vQ@JQ%=WxNv?8_l2DS(5S;~d$u+6 z59LG7#2=!wY$rhWlWFomYMD5tx4V0YIpJV7SrlD1x5SZuI@>3`EX6xTmHs%Vr1*K` zf1B@XmDjR&Q>Ex^Ms@7Xpq8FTb(7Sw>LiV?wx2dh+gy{UIwQAn_Tc|8(aansnmxd_ z;12Qs2%__VcvAWg0z_3^miF#zA zWl}v-v$%Rk^^ckbb-(LBnbw+pEP0l9W}UgYDFWa#jVzz7jgSQ>hdm^AQ#`$kdBga# zd)e0950Ig0_&r=1SH->H#_;p_?_4|=$Q|KoID)m&FUh9FW2`GeoAc{i)ep4hQ}xqq1J*tsSp3=?>}^ zYPYI;!EKcl66w0)9OBG%^$;hCube;a6WMwaFcB7_&Qd+-Pxn8wtB2MOtAAo$QEWnu*wgbD6#o1ZheQrIM&E*3Q{Q|GD z&)}DEG(e^)mS9}eA0is>ihi?d&6Da4=A-CCu0MR;ON=<4dLn;T*`0E+EU;);4xbv5 zoSgh3b!&E2VZ-tsRX19+Z@#0#C*LP!T4bXCD!0}e1AGUXp}MXK(!J5XRd0a4ienw; z`GagbMgY}72yYUmxRRt8*+l%cbPoP7BzR!tGGbY6(V2O>>hUDrSg*MMO&ek?^8D#N(fgNs7hP-Uh;tLyoqC90#XC_Cx%JNGawjMg{s@0i zy;0Yym#W4>Y06IJIV3{s6jVx<&qJ5guQbio6;PJc&Ut}9N~lRE&Z(FND|(WI!^DQCJ|4tQfwf05UaN{!fdZ{0qDo7Wlv>3v{7YI zzXUcJ2lbJB95u{!e4Wk1(#afbd1L(>3B@*GA=r7Ow{?T5q|UoGv9`P}+w{l6f$F;p zKop0PD)KO);O=-E!2O=a_mchT3Z^H(eHXAkyqEnb-elaZkFg;upO3m z`QEW_%j2ajGAd&R1;6*78Souqf<9(VN7RZOV*Tf`n6)o`A0P7n@Ut{kB z2Efn0j@!WY=lNiZbLOsqCFz=W-Yyo$cyAD_=EpOtl*k9P&*$Lj$9%>)R%j|pL3l=kx%wlQ_ z{?L|eK3zZEGM3C1-HZnU4o2q2Z%$ChJ&9Z&9u>7D@mAKRlA%?b+l+2s)Hc6)O?lg* zz+81!MEbkb@U$CgPg8Zt$713_ll&??cIv92H;zV}jUm|AAh+mEA0!%KpKX6x2$N_^ zwVbf7K^EZ}TF2^{1acT)5Yibxzz_as@9Nm?Ld8a6k`uGX^C#GC%uJ>;8xA;CBXfe< z1{6KVsOdxmHXD6_^&x72u5K1Fgq#SJ8Kc3q5SFhOb+nx!9ZnX1v%bi zejOjpNAYg}u4NQR`+WNgKsB!9W&*zUHe19Tpw5xci76nDep5fqG#h*DSg-%?cR%!e zL}BE=5e*~s5#u7;#@$J)DpFO&wQ1ZIZ5dPbv!X@W^& zwf_blF`La{AA_-r*-AAbE}{inwjomA(WvhzscWHD&RP=Birhi< z_z`>|-T}Xkzaw(<`8n2D`QX(A3k$5$@*X({ z=pVG?Y3evTj{gD9R>b|VYn_)JMq!Qpn(*1N*3n(qVLxy0XFtGA2iVqsKs6y@sAXDx zxTz)5k@@Xvs-ABc?Eb>T=ylBdobRK6z%YBPoO-?Cy26MOW7(@llgmdnI#@cUB(g+X z(ych5sB@uLA(ua=;qCPDq{%Ty!^47&es4Wq8qVs5Yjf3c%5g_B8$!~UjJ5=<2XC|5 z1|S$>N4g`YZ4Yf$^)5!MZB4RWC8f$>ALSIbOW)*Jg@Sr$ zX6ZW`!`*e>-2&f-_m62Cza=RoZAsS0oToV@IjV+4?u>jqUtKV}z`gK&!OZ*-xwhqpPvE_%Dz*-p3B(fuszyfP+XYxeC~WWiB&)+4r1@|H)6}aekyQ z!0|@7VSjGVcZ_u64i8}`@8DYrU4;QW%*u2rGm42|&N3JHHLlUhXwX429=fHvtzBSv z;YNGh^Iq;hJ@`S`!N`6w3*v6ZuS>X-I3>9-Wlrkgv@_{5GIwP~XT8qQWb8>Fk`|eA zJqb^s z!`WW6iy~+r<|gG!CJ}kWF~F~H#9@3W{t;h<-^S8`s&h0p6kCrTLT)0v(K-}HF&knt zAp_C2$Z=~Y>jqmb;*UllYFi^)6EqCpi~T`NNHC5AT|*$?`}dO_=xC-Es7IgES?oEE z$(=YE!r2F zTWT|W7>b9N!z+}z;#k)>ak<#ZSt$$yJg2YyE0@M^;x+>ujH5H@Sn59ckXVlcgbXMf z9-)_jW<3aP1?EXtpd+1YJC9K4S+v&H)rNqrE$9oh15n2;!3+eB2jZ)Vaw>~l3pk!# z^agS-c?jqP|D_A)WT1&^1aRY3%w}c-Kw1LWwOk1|jy^%J=Kiqj=@aZ20do8lMmbly zzPetB$?|k%4)hJus1|8*bandI?rC1@Jp4SXd_Vaf_FnGi5sU}^3K$jIBw}x?8hlPyA3=;lWw!@XbmKK6U;6XIRxe?DY(z&4*gL2JS$1m5s_8(bbXASgc|B)BS!3Yik{#(z%; z6M7&(_MIB|KIoQzbHDxm9syna{&;WnHiKRgi~A|JRo*YXe;7*)0q&FCk_^oaP4(|J zjAn+`uC}Pw!{Ze-+(nJZyPXT=i%_U*kYl&RyN>f=LTA?p!JD7U=kP^*k?@W8pj*;W z_N_ci4ku5td2As$oVvt{R2ANsf&oL5i*tA*pg>L`USjSbS8k4HpjViGx#iesbP*fR zV(3u(H#44oNX#QIlB?+uu0J^r&1JjTN1}_+&aTcviG?Aa!&Qzs$bEhzRL7=Lo8?fb zkYoANkX@;Aos&9(z8?*=6Mm%btqD_aQ6JLd#!P*#cCewu199(f%rQ3enBaBZ!^{1r z=Y5}NUSB+}d!O*#;MKx2#&@w_i8tz{_@?`A^19@6&@aUsh$DSHe7Crt_N?@u?Cs&U z&@0=wh1)tqrgtx|di_)5R?m6H?YcHb%5Y70TR%nDNnNYGt?#Zn2VK;j(%exN!iB06 z;(MvPhEWvzVNs(x;umW{c0+b=(qHtT>22O>eiSO7UQ7R&w{HE_@39 zgVT#>{>d{UM5Uqr?EYNQtu=8ntNd133{E>4#d@7%yH~<4wL_Nb#qLSaj2De-IWXV zfxp8M8Whe~c(tFt8LWa98%o_8tB$H}yT5juqFSZh>s98SrViAHc&&2}Fm!TT>Au%} zspm)clLn1Pg_p*dWW>EYdfw2jbbsJoWHcKj??&zknkycId_|2(mFb({lL|jpm-|e0 zZwBwsPIM2{Z&Q!f=IS=;M(AFs2C60i7WB9DPPwW-ufE`1F5l9Ls$H(v(hvB$+)b=h zPRYlds5lNp$!``LxVo~t1sM7vlu}!qH{db$chpDMDCH;5vw5z?V!Fd@@8!7Qn(3+(6nmr# zbpc_xKmF3 z?p+Lt>U_f`_Z>Qm#vf=HGIi7R;krkfC;DK+5RFSysOL4!HQRIpHJ{-F+Sj_%@On5( zSE=a?Syi7jJakzxsCm_5rMH5^@v1*c10@u`3V#9h%4DS>bWu7X(Mp^$MH(lE%BMwC zzOKA??sKNVys}p4=jy7uDw*xmr5fn4bFrg^@>W8Hlj1>WwCk>b%PG)g$8HC%JOVmQ z#5IJVt!$Y$yJdo636u!xt@2 z5p_}obYAS?7!0@6br6#UMD;_{#o5ReryZ?Yy)u}oc>;``X{ebV_`KoZuN0m{v zLw!!Y3BCa4^jY;y5P2fh(^OILA(g-SC_rTcRKBW2xFf7UPoZ7#A%Ni(!(UV^hz$nS zSj`f7fJ*~YYFcXQiUi0nCpLG!g6F9hi?yx}un|TjKlz!YlC$6p=%veFUI^cn>ctW8 zPPw162b8TEpH>xhsDJc^wgLf(Upm(=e&ViPz?XoFIpQ>6y=fv|+ zQ&n#;XB%j~Loc1pl*XFJ@=RAPd_sLp{N!?1*QiHGX<{_IK($4QR<=P8LC@!N=o~x zSuU+ASR+bK=S?L;T>u>bTy?N&64Y3pD3?R6;C9L}k(B4CAHzB+SQ)RL4c!x)LX%Xo z)Iu7qYN$FdwNcIi3%-}m!!O_kQkF7OJsrxHK0(t}PvjJ(foi5AiyNWSaG9K^#DeRl zq-7wJJ0zt7=H#{#B`ty~RT@Pt|AKD9laz3Ix6%p@f&Ar8$`{Z@o1tt6XmbUuz{}+r z(GMP>(aP1%RJBfbPW;=c*Cc6ffG6}@BPz39rK;=d>0+potpgp(jt~{DF^EA@hV~mY z$MpmLq-yNyCZE+xN;hYFXqKv@^i2L+RireP7Q=a}Das<{Z+WMbt(v82AbjQC2w(UP`u%p zVwp4x#-+y2FeMEBCQ9>1QjR!8Jzq0Y>8DIkdBfe5J}?K>i$BB)=#lbA zYNl+H4>`MtyI_l~ayVRzpuYgV^h}~fykW7S+R)he z&CuKM%+STi8O9j87%C09hGquXFkYXmS9EK2dAcCoR_!fK6HTJ#gSwUai>j|`EBpys z1$6})h~Sn=!rtOaN1sGHvwk103f~$_aFD3 z`@mfRh<`Qr6a4myd-A{SV7?JR;J@$*cF=5Q?`%H+I@m*m5TS!`Nk9Z%NC)xzkArq} za9W%Zt{4~T>Mdn}{|G?H^~yEiDRWc?wU?$qD`|)50`)q>Ji{SFficnTvs;z>J@>92 zZ#)t`XLz!n%e=mL{qfR!&+-1|{l)v9_aEXxOMft-GaVHSwAdwE!Y-0W?+F0_w1{;yKqA=Q77};kz9N z_m~LKA1h;IfzT{4Z_4XS^n^e;mQI zcyG`d@(OQAoB*9(6G5NEbm|vS#0AssfL>XkyMvyte?jdYV(%ubaGY{JaUo)&JWY86 zg#yLwK8>X9uRmizjcM-lJ*=L+ywCZ3@%`!d)gK864QdfQJY-?$`mhz@QzHgPmPA>i zmPRMX+>faQT#+L-E-pFFCk~Ij7ds)Ai0Kw{KH3%~MY0j^!ncLB31xy;1^EUp^RM+y z^BL*2+v6X%&jz#3r1_=#16e>PG2yfe67R;PGQFr3#6Oq^I?h&Y8Dn-c-KtwyJMjN; zbe7RkWL+0d(k|)lxVyW%kOU$?fCP7UcV}>SXOO{daCZiGcZjxIRhPDRC*OI$m6ad- zNEW$Wb?>?7?EP$6PneKzZ+YL$YaZP6v9WvOw}!DcweAv3r zzR5YQz1FpeQ3>yEEiYRT;qEJzczAgUePq4|{JRBOg7$^vg?$Yl9N8STFeW%|Pkd{F zS5j_reo9!XE%jPjbo#LLE$LU%|4HwX{w6ImZE@=Hl;g>>l3WtI#?Oh}AAK+KW%#Yo zEy4W*L;Rb4c6nuhb2vwMg_puP#T2`ZZGYs*w|zDLWsIkO;YSS{kkz_9nl~yhWN`0d_L^4+uCXNTX506Zu%v^Km)WNvgx?ACbCq?IX` z(^h2IGwZYOGqhl{d z2_gzZ>w?z#8-4nC4VR?4yYNkHH)eoqW?PxPlVy(}Pa zP1hSb|B3n?{j2)tnIEY?_J8Mmzy0mZ=WYs2p; zf0i{YXe?^_*|e~E82tVFrS-DsvL14O1xu+?u2$Vq9aODTT~@iON2s;x7R?fHlE#7C zV;8m3IMF=SddU98+1mce^(%wHuHX*hmk47;BPC)l(&x0lJ`f3Z51SLw9yKTSLA)|C zHRV;>%*?ylb93M2+4D;ZcNU#4jx6ygc~QKkcxZ8O@%W;k!a4chb35m3$(oq)IaQt{ zi+7Km5OpQ|T*%zODSl_YaSyIoEjYp*z$$SoYfpDrEw_xj$+MUZnW3Z9OO(;__SV>z z^-Z>hX@3~M-~DX)+(xOt237zE}Qm{dxIk@vjHJrv4uH zXG6o`#sy72&9|Blwp?g^Bz-7bBDc#oE2b&?s+j8aYLmJP=)Hrr9kgFHSsI4sf~K?X z01^YLf_oIpwAf;?l{n*pF7U$5kF}Tck+((Qk;8S)UPmbUhs<0rtnQswXv=7 zu}QfpQ_@(O*RzeeoAb{U>@Pe~bhmhI$(54wlD~_2#aoNig>MQH@_XeT%XZJI$%sok zn0zhaVr+9%QbcuVQjpN!?2ULBM7ZD`cO|RbEvTKaU$e|N4kUYHBavNNi)x!XiR<8`t zw7g_{d}E9_YC!nN5dXjvzP-E#OOCof=j)eh$E!Ok zE9C!3$F&Y^DQRwR{MF$6GyhM@AL{p&-vfX1f4BYW`TO~A;h(TS5r2yQ?EI75@Ufw= zaYW z#Wp!xK2`QxdO`J;04vb{59X%kaU zCA!7aF@vN3jT{<2Iix=Dr60$4wf92LEn<%F1UH#|hMCD|b~)j!vK=uCOd&>uk`sNg zB-95u8?;`c&(h|o?aIx{TIC)^nS8&r1Wt(^E&sL5ZCTTNr0GEu+8o_Fvh{PztQJOV zj?`P`AzLgJNDHMBS*ZM`e3E>JEK%kHio1H%c9ltaQ#netP~B0pQL|5TRI^i4r*W!Z zf)?wTx;#nA*@05~(lx;0 zQ|lG$u|zyw6bzMvQ6v+KJz_mCcuw)-ht5Q zN|e+O)ScEQ0&SrfvFIkMFUkGo-PI2ai6-1W#FfosdRxOgq@2jzTU=JQx#Q64WnB(+ z`MZkfaHYVJQ;}`Us7Uz`+bu#6<`%Ax2#!1!))XWN=ZoN?O@vu*JgG<;UYDHDR&G{{xr56;GTC3}Uj=*jbYiN-fe%|b7 zIzksy2Z=Yp=U9v|^)5O}E7t`gU%Uh`NVOvnGu8Bs3le4@K&U0RRxwM%NED;5@1 zr&Y!ThE)4;y%vg}3V#T4-FcF~JgdEydgglgOYVz2g=07gu6ElS(_v7!m@PYPeI0c! z_c=KpD+79lvcoTiFNjzg;TG08faS53)3@!SDTsKCWNUiKZ7n&i&*i?F%it3_q<@4g z1D*wKC_o}KRf;lMx$LE^i>y{^lkQg%x--}lvW*^OZnQoEwW_zh!UFzXq8b~9&ec6v zCBQBap(@hUXkTlC^$5C)I1F>M5aTOqH-*qKpt8*)*J87=i^O?qA~lf^;O=mG3^FdG z1{&hD37Wy^etSVE1(v7uJxrtz((F)eQ&Nf;cyD^E z_i3K!h8il!sq_@6&G*u&CZmZkg2$MwAo7VTFuz?y%E2vJ2t0-cdOAH7SLvcOS^9a@ zT4#Uu8TXSur^DUSR+WzJn%eVWeYe_gl`qNyiZ5i}if05mgbP_*rk-_(bAq#tQ^4OT z@$g?2+!Q4B6MBQqP&`*4V){F-nr0iHnV;J4w3jjVah8a<{^uip#)%UGTDVVY9U+`L&kOO`DpsTE{CUYMA<$h!}Gc2qmG`VGHzf&13aZ)m_C)StxiC z6{;cn2N<6!1^$NK`U<)eN{6TQZ@N1%5!;O}){Rh^Wew6?`7dQRjY4bIEkH`KKGWud$bSK6#65B=+KC@P`DCdPQ_Wr)p^R0K*<@4r_)e#5X1E zO;Tx*ymNN1GrbFXY^j=B%FQ2_aXzj~V7TzS>o2FNeKEM4?(o(NmWYq}{1-%oo(MVW zKi6}QdvE?7_A8e(>q;t@%C@+-PiIDQ{}x)j&W3D^9g@UL;>2kpQbIEQZIW4Ri}jE} zqvFT|q**OVO-CEcT1dreeFo@86N&HmZz6~qNX{|b*0?J^%jAk{>T{Z}YN>j?{xJF8 zI?Q?0G0KKnKig+I@7Q(5g@#b=bx`Sj(&Vb<$|O>rLaz&7eN3 z2Xgu#)w2rcWXe+;FRD=86hjGC*b#l5_MN7KW|C?r)JMMBIR-s>$T-=&(wghQ+UB^7 zY42x8j7nk(ejfXVY*b&64U;y@*Qw6~Gb2*RL74b!y28S@t~Wn}d-zJT)uJ>{q}y-` z8%>O+T49}2@LFsQevh0=?Zcnw?kTsb9$-(~?z-ps^THD1u4K=z%Ik{`X7q3G(pnsq z)turIpAo!^FR>wHow>lZirb$zlv^qI*K2af>B!yT9RkLAGzoVL^t>460b45B8CMuD zwDn|7=Sl=x$-cnEn3>6cQa&bRMMZ_w`|tG}!8_#`i;Yx9wsvb?)pVdSuTj?Ymn>ep z44Y29rb#-}I0u|CDdawbr+%GgsCtL`i{_N>3$hCfrVOSe+kA)7xvA};bEG}qB7nMd z38vC>G||dTg-Cfx<)ew${;TVYa*6A7g!u<3(P}`Gx5Sce8EX2E`X4`SDUnO2gX)om zU%|fOf5`9DIU)m@u8LQ#)bF-@;M92=g4r?t8H)0wJ@W?dANZul+w$_<^3>_E?*fAb z0S+U@HQumWT~k?CIb!!oK8Ya{!l#Aa4yf`;@yrnC^C!8zwr0}F^cu@nXN${D#!+s8 zM|IHDs84Y};~V1sj`Xpqr-GAosA>RGQ^B%og+82xply)G^q0!z`t|vGKZk^(T%#E>b=@EMwIIs92sv)8JnF}KCGu?*!7{=b(%s+LSi^0#NE%t{b)+jPek|F z26B&yqC`L48D7c3%&7Xvsz9bBk2eE4O%oY~b_R6`_cqOIPvRWq#|r%=OZ**?2NRzq zVX^G+gMqJnM|%9j>16AVUQ~Q*Eo{s8JlY&XiS?%-oVoy!~x>|*;oJy*_DY?o&%zN>ip zmDn-z6%|8YGO8`Ujz5lVHZL;^{^}kkzUc!+;q$OuypnuLO`&!XV$id!#LpNSwa1kj zbqKwkBMEvR|2w@?zM*5EUMGj$8{Xc_Q2I1+Y{)&I0ixB6Kc-J;5Ne|nTz!SdJXiQ^ z@LLerGh|!H&4AOMTtOD=s_WqP=Z+iJBjyj5>^3iUuzL@W>s~&7RPeu1XXBQ}UJqCJ zkMTIo-@<6M4aUbS-!!)T+W31&Bi6x({ergVWmgokm)~lom&%kfuXYgg{4D}587R_Pn zYLE4?V>74Z{Zo?G<;MVQElZ9sp(AFAe+aL2Xysr2j^*a&p zB6uqRJVPYMIp-J@V=QyEYk*_4MQ#Fjkvr*pat|~#C$b7>~{50rM z-_tdwdEjCr?1A>vmVQvFkwy>W3D8MA0A<=TeP`WQ^-|R$^+#<1vdC~2Uqe>V_e>Es zog>%T$-WRew?($KR<%h$UjcRRQR)HJfm%*#fLS)2yh&~&JPq$OOBFwq2)>v#A!KoS zOQB!c^Q!Un9S5x+99OR`N{cS{7|Y)2YH_}^7-65iX*fbaYQ3%wu#NWo85f(AR{W_V zv35}(dH=ONdv=thcL^Qiv4i)MIndP$n6K;HR&W{x;iA2wzeEQ_n?&8+XY)fiXIYOp z^+JVsrMQP6hn>OjW*ldxu`TS&oD}{q@h`uWu!N|CQKpFZp>Y9;l7E=9E$dK;vb?3X zq0g@uKf->g8avD9X{PIEqR%iJkxCt;{OG;(Dms`RL9aD#G959+nH1o|*=-CnYH1_= z8XWUoVJ)V^`Lcy9#y1&iU^VV*xta`hrrH(UxL#;acrHFuM~$y7Oh=_N*Kxp>0&m}N z`+nOE%LCIrBikf3S-{b?g6c?S0^4spa0Fi&iV$zjSVfLXO1x#i51p17TB_=Ju*TT^ zTW@wBtgEzeV_blL7x4-~6>m2?fr&9TvUB)C;WNITGm^cK^N@F#--Y*-_1+JHUqm)X4~Y62#tL$JUE<|Bw-8dzD;e6<=MVRH z_@5(TeC*bL?5D!hG{rOjH0Bp2L8`1lfPMN8Ef5TCc%$$j!uD_JDYq!?4WiV zF99bin_LZkdLwX&qV-d>hcpL)Ld4bIK?WIC;=8C1CavX`wc0kuzTWY}QSKNIwAyH! z6qMKVtXYL(HMBM_q3+S-_s146}=B;Sn!Wz=psF z-ZO+F*>jj3SYz2vHqJ5g86FmYPWYv${ZR=KwIO}{Mv4*`jpo^gsaMVIRpg zy3=ogW+OG^8`dMZ)}(%-cGaHN`5;38vyevw(3I(`rH{22OmueIH#l(nINM0;AlN}v zmM!LA#!7k|)sM=j++aQCz^Tdvb>K6^OSeN+E1#<9hgq0$VXfIC%HLMyby?AUPtSKf z*LNLR+>#I*G|aouOY3pfy%+BwH&(Dt7|g?6o%R{_#&#iVA!h|=4HxL2-V=gOg@gzG z^|#88s)ODkRXaRE)A-+e=7Hw@MYF7%6++`mDvT zRViJj_@>IzXf-_DMC3m96?9FJ<}KDwKxe6Pbg<929kPa7dVr6KM=t>8-gRmKFmroT zHRKy2lnBQnk+)iomZ{yM`KY$5H)`+ewP<(TP6mTEvemTNq63A+ReK!hFLv4%!ag#_ zVlv`%N8mmoR0<(8Yz3Cj7VrSxMvCDak3_fXXQ(>ME~%cGcYCZ&Y%L6}m|JsHmiDxZ<{C1zCcSHCwtIbLhUABFvep28JeZ`YO99;W4%&CZo>!`Ziao81q3Zt^b- zITW(We}H5xuLt`o`w#b`z#_WuY4+V7^g8Tq1Pf%!$Gx|UBDuX?u9-{md3u3%Ie2`Y z$pd7=rF!Wy`BY_y`lRNn?j3raIAUzE#M`&P4$#+m!ZF9*(^g=;X8vJZ0xrML^jG>L zwThGxr-}Et*>DRvtz&EXng(@|W|!uwrb6qdC(y&-0uG^4=>*d?%L!Y#-NRmB_jSB= z3~|u5b=J9-?&enGMtU|?M!o}*;ZW=^EC9$A-;i|(3XDjF?wzVy{*N+-6ms?9aXI30 z?<%A^qchgIv_??TD=#c*L&WG{uRxuj-ut%4PSGa*W#$lPs;L5eK+(np&LZ|f;bF;C z@9_cAp?kt?K?l82VI8*F?)aX%MDZp$Ax8twFD`B62(QlzRXkY-E0Hta4Z}d z0PM;v%|>}f%k!qgEzf0Llws<>v|Z3%brWdPNxyLPdri`t+{)vVAh1h!Kf`UBIDZ;cboS(Z90WvhYx z#MLQuo(CuD4_kwEn*}pXF&5AmSx8*KO0av_5iHQq7qNr0`VBe|?W|X-e<)5WlMKV# z7yB?1`{#4Yj1`)Spz;Z&4TZX#(`m&CiP7fpy`h_f*7y;g!`+Xw?%OrQ9{pf_8F9f@ z%4`w*<>BsY3>X-!37YBu%4>rpNmB3G;G+(Zh15n2iNYfXge?uI^-2=;q-93#5%mvm}*16W9Hj#tj-0v{j zU)fc*ftJ6F;lL;|5Yr7OfSxJWS)i=rg-a6cu?heRlqc!oUJ<-m{z!Io)%8a<$pucL6$51EJN!z3Cw{7*XTXnUJGyS1E14UpKS&1JqBpAjSVhsCYomk@LE7;JQ4j-b1cq{N*z%dPC~vtmf%GT2rNeijr6qxH@T*!%Ui&fwL0_k{ZLjA9{S@#tLvc{~%6dwde}{P~BwhK+Ozw zg}MpmNNcs(;6(A&Z$tvIy}$(ON8d1RHovf*wdXn(+3(w0fyfo+SZuGc-iB%}-lQ_x zs9ppcJB#iHRyyA>7b(%b){KC2tuwkYQp*tTRbO$$jktYDhGZn^Mj}7u zK=z%|xm8_ibRDHdqq11Zb7JO)8htJa8(F^?(X18hJ?v0+Ci^FxMSRv3MkH%1@3#1; z@8{rU;Tt1XM_iAHk8F*25;h~aD3BA#4V>s7;ImH>D@X>7T8i~Dm4)$g`vE>i zkv>X$O+8ipTOFtQp$-8r_Zy8?$3?m$@#tdg8acxF&{SjAn?tO(YymVJy6>>xNaafutC7dHNb5O~9Lxe$fh&mg{<=fj=C+Q;x@ z{Ba9l6tEWaio|DqoPkq9E{5C;i4A=n+B@uPXix|qloy;6oEKQ^dq*;t|Bl(#ZnkYV zD)G%gyS<}3uRWm}DZABjz2#}^Xz6Zgyv$3{Q5~+ks&7Z`8QA!7d^bUmM(Q7^c$Wev zcAs%IXzAYpn@DdEqI{%MKR~xf`$U6)BB6(7jOMQ9qxKH;7|x-ia2{}nhSF}PrDgyC z*^+F%Y+r18+Y1}V{uwCi(dO=^C{qvPIv{JdVOzi{`PDEF7=CBiBu%Sw zfoe4(wyb0~du<6m5jHHWHMA`JS@fP{Th6{xN2e>*tjbX(jO$%w1KcHVgZ~vA4Zvv-A>a@&C1qWvfr`;GKpNM9Ikn*lj@f0 z?(6dm*YRCITK<>11U}Az^m}?7y`BufUl>AxOI?c{L@@1q?LJ+Mey}b=dqs0!dr4n` z<`{x88`g-gCSTIMP47&hX0PoUB%M zh|8Ujwj}j+`ihJpsYUUtBDw^R@RxXh6z$`!W!(oSe;Q{O7w3NAeHSbeU6Vw5xPs#6 zh36K(f?#IY<L&(bESZpv8FPSSGO zf3iGKwuDK4OY7uAl-<+|HPawk#vkp7N%2(T4jw>EA&0^n@r}Gn@QBO6sCFf2=$RFw z%g{svivEKL5eHIaD8g7kG2e~d#7lw8+7(z3na1zNGvH|YYI$rvXZ~(pVeVzNnO=cM z&`fE_FT_P~AWy+gp>p70MALIsE#Q%%EMun*;~exGZYF%f$FuI(Ac%1db^&5J~h+>X)uoX57>;s zkeo0bZkS)dKerUtE(E^|#BUGWi1o$3K_W#HR!`W8ESLlhBeqht#<|9x^cLy@{mt~% zY&VTGRsrD-F}48vERzZ#zTxrU?`b5|1`NqW$AT;M3_4k_)BIAEs$#S(BGd7djkuS4 z-1i9dc;hk2cYk<%+WwMnRnKdzosO1<7DVJtC`c*oQh`@EO0)82rM!*`2)^z&$hW{Z z(#PnL<360bmFePk+$9+lV5G}8w+<{RdlKg_nEp)QM)Erf8{F@S_~QMd3GOchFL^6D zCsrIFat%Iz9<+u%4663%Fx|cXaKBBh3U7(DpB;0`Pc?I$b{>Az5yq%-of!AU^ zkS~AJ6{g9?kJM1=U;4PIi}|Y&ho6~6_cV?(meHZ)HM|s9T0O~?cz1&sNDejw2h$+g zx*$!fDpM0|xN8n`d&=J|R*IL4#Ns@!&q2y~GWStg$4)CN2A6o{cgP)+f4BHV`Nd94 zD>fIlq+N~a5Q+z04eS}%?pNl$Ph83G%i7?&plz&UkK7rTtT1JcMgA_JbNd6bB+^=`gGWp(=RzBWp zA3GE%<23mf&PA_k2ddX9SI9R@*SBh0b<$+n2se)=74qa%&(Kwxnf7DW{ zlnys~8y1y7GY~6-Cyfhz@NXO;r;*9V%(fiA&9oTNW7? z_9iGRz~4{lIY$`Cp6Bw&zR5ZVI*o`~X5MUxh4i8t+k5K}+uzQ7w_0`xua=*|@4>GZ z6bZ)(#|p0rBi)1CZwQuf$1n{pi`#lSwAL)MfL?%$(Olhg^;~7LoG*RW^13CrwY4Rr zg=pU2@}qTutXQE@9)PO*k!}dG(69`@3IyHZ)C)S&)am~-mG+qXn$Lqf>0irM%OY^Z ze>aXdO*Ne}KBk{j(eRcf(pRXPkZ;li%)#AcI5mOXPh22yqBALk39c4Mqfz8<(goPG z?}+1YF^dGJ-VmY$D?@C$Lx#&Hf0s?%y`tY9-rgd=Q-K9xgJXZBl@#nOo7XA0Q%ZSO z2Sta$6=^*$|Gz0B_7Z5M>7-EfE z5}p^2r+Mf8T^LttEPGec+VOU$la)KF(5g3;rz+5ri}|{&#Po`k>505JP2}WIaUkMz z+oR0AnOn~)XB==F=C;TcgRHHaZL0Ru|8u%9W8ma-aISIhb4PJe&TCFO_YU_uw}n&B z?!_GHcE^R${>&k=buj~V6Ug==!xnv#wqD(?3{f6Z#438pd&+K0)zY`JKk|W~%{rlQ zQ{7g-1a^Ipc9u3@m!g;JOW>}#6*&vpOD0rdNCjTXaf2K72U`tum2cQs>@3z5{}*2j zF8XuWHOz?l<1N@A><3Vp7a1lS!VQZcIm88dsK2Hkr(dee)ArX4)|}UbXaZD2<(H+e zWWMSp=t4TxzS#8wCrtFsdq~jG2wt2rIVEdm!S2%a<%255S0r})(W$b^z3LL|R^i3J z^0(y-$cRZc#Y!W^;m1P;1fKHgBY7`)%>Bde!P>)k;M&`zsXYbK+I(GSxu$_bX$xx- zXEm(ROl~|k7`|P&>o~vIR#pRZJma|QIe3yT+9z9In(UBeFcJG3ov6R5DOWia28FkB zhQdS6l2ypI$nPm!mCqFpg}3UkS_-KKceR*yEF|i5L?S>b9geclvuG3Kg}lRN0-1b2 zewRoi5#SZK6K|oy>I)RBr=*Nbpt@6@K#S86`=N&DL&$;0k%09z{6r5zYR-M+yndeU zruM!zN~hKK(=;es$x9VHf z_Ug=<<<;{lD=WsA_9`sT4anS2Zzs=F?t6G49B<$#lrg`!HMsug zTH$7OyU*CpT*lhU{>>T7YvvykR6`Qe1YQg;n0Es5n0B!aGaBJU{Mh!vkzgw_U!=d0 z1w;ro2)U`%svA`frAXOLVUgS99*PRZN5xa+UDa^4QT+!d52$WC+{Z5=>5vL^8Vxdh z0;RChQ*6R7pe`uziQa9xa;G?!^k7-d&llrb|k8-qPhrCQNL+yk9rbgOsx&&}*J$eLniC&w; z&x|WLSJqUO(?!~)YnN4>*Vjm^26y6>tBZ|!=Q97KyiRx$V+cPKl;XF?JJ+j^hsoW_ z%jPU*BWxBYk?qPl2_B1fwjEBiL~tZb;wKBs-G{rA!d1d_;VnS}|1d9syMz6iDR)!5 z=-VDR3T@}jON@7*Zz#i-p>_I)+RK_7>fNf{$~ff^Wg`#==c#_Gu0e$vqHEEO(~FQF z$S8CTP~4ZJ>(QQ&K6Dd$rF`@=QipUvd>{v<#qbcmnXteA2jr>sq(4;w6xHdF2e6T9 zgo)k@aw}vf;BYTohDAeC&sMZIT8)spRGmnt&?ag_HKppAszl`wd7{iIt5Id^ui-~b z$DI-EL*n#+-jSY3XR^4(XFJ~Qyt>=h?jGH_wF^5RtBUM6t#nrYmyDH3k+G{I_#q?U z78maEN!(jJS9DK!oj;D(iFb#m<89%!aaRGC^9$!B=M?8Tr;(%OBypir&2#0?=S}5q z=J-Lq^nq2tjB=B;-*Miz-?C0Nx6|lF)k)PH)mGIu zb&2+njt`&n6#ZMIgW;Yb!>}C)Cn1J%!+)qJ`U3fmv_eAMTO=86MvnpCq8U4gZzfj5 znqi~;afKVV@C*TU&`6C5m1fBVH+;!~jEC*A{2z5K} zvd`tQtHv#eaoKI3nH7LJWs7|cU;G{+Z@a6$809cJ7X1@fIol~fVt=tU8<&8 zeMIwJBi0O7-v{mCT`gZXL01n13tw~u@Xvoi-|PtF=fxV9!U-k98qhn)8{`2boE<~I zAkpY!n4EEmFQ6ox1w`6nV;%4d>%qs&Fuex)@JCuqGbj<9|C2ytxCJXR96_JL>FA0c z(O-nzFiba5_foS*^+3T>*cFG>*Yp_P#q4PF=6ZQbf)~f^PF3a&FZb{AxNc%^exH85 zYU;Msrqvv)SW;Aw6`oWUy&@Fz=Xl+4|C`@|KV2|OxI`!u3>VB0u!M(%JA@Mj>Ab`2 zW6W_3AGb&C7ab!Z*X*r*vs2tQ-ucrp)#=~Kb1ZUSu6H(o~o%hOkv02IgFs5@{KDh>YFIADow2cEJ5YsF?k4$}|FCNiN} z@K$|=Git2CZfJ(xx`yaUUZx(>VXz*lrtQ%Gmz%$W^FGSF4;;^zflB_I`~xJ+zW8rL zwSj3!LSN|5>w4-!b;a5{>P{+e?5T9Pg4TY=&zW7@Yq%%9>cZ9~?9B8kURRY| zcd_sM0cQu~^l#|hu}6K)oN{Sl&#cJAd*R~(o_PCu{3qPb<#9ZCtAxEp|GIY(mI^nx zPZF(hPZlf(y25bAXqPOf#1?1%MZcnY1CJ=uyvBUj++azxKLqEo8zi)@bqRBgaTU0b z&NcS=R(Fe=`9GtEdPBrQu2O%fWAujeaC`EFyJun z^o00@uffJb_Di*4wjl%<9v9J#hTFjINFja_X0jhW*=ROqn5LL+0PFS|_`Q!CKhsaC z^U%RhAd2uGhMDLkpj#|JZs@z~#M(=mml~GllJbTu2mAriY8(+!D(eonB=@!c!=nZR zwR}Yx)w#as(*9Ql%^#%i$L=}5YnMu-l#w?)abTZNYeOZj`a(X8GslkA(!tLS{97Da%puhjNJrW;;k;gBtrYwBv%*uOgC+Fc+U zwvWq$_I+)?9Dmz-TaK9iHM$zz=#`{^7>eJ=j}g`6L~;|{4vx`H#*U^I<9y>0V;jub zSw<(Mz1$|*WHazN9zgYT*Ra|UY+xC-gX;AtWcRFtd)!(o2Ka!3fTWUa(wU~4QPU~o zG04lxrz?R-QHD2Q*ReYWK1%6>kr~J${Zy?K2%Iw2NyTsJIccEkKXkoukz)(d7&=NHuWm?aqVwJgRO(^sNC|GBd`4nFh*r= zGa0noSl8k0J)Pn9jh08oTIv{3%?K<5yJdJ|D8#N}9q}w;DmfoIX(p;ObQ}$^E`8|| zm;^V&^K+QI2dPwQ$Vgd)FNF8%x1kM~@Jh&<+za}OTR@CNO|#A0&2!DO&1ZlV9Bytf zj-&ffPe5lNCi-JHVHdsw8J}~YH*`SLS(B(~Qc?1zR%J_`EK*&I*3(W0!I|iNDSU5I zNltyKqv}@oC%ymbUpL@+Ur~KU=c{F=yw9njG53OkyeZKp!A))~`!JJa26D3b5_e2= zMZ8=5K*V)V;yYLkF6Zs3<}nnBeMYwHo@%G)vXN%PH1dYYZrj>6!IfroWS{3O=4e@V zMnAXZE@zx_YX{SQ=+N}R?->q4^5}HPB)f*C;hB(yafag24yaiM0$a?B?ni9{Rn17q zZP|jy0=J|Na~jkJ0al4E#a>|Pz%g6`+TAoNlI8;$yUawG8qC`*XD!8+chEcAWo`$( zOFz1lnn&&+YVdEc|K3AB>!0ZEYaeK;)ePl!*(m8F*(X&KGK^mB{D&*?nGi8K*_d;_ zR8ht2p4m&^C%vz-*XQnG)rU%EWo=4G3!mwy5HIBK;ml{tSz}p`SnoLnf`y`VNxkHW zxLKqY@_CyX`<)UClbURJtNQ|cs(D(g?l1Hx!8MO_+;j0^vN_v%9s)nX58hSI9cGKm zS%=ZWGG$PD>=|hC+jXv>UH1VZ&>({nn+i#0=~OY?6^B6rB998Bgyd*^Ce*?Q@B{c& zObh$49(Lb>cs|rA1A(9|C;E|NsQq+r<91*f{xoqc4=mR#qb=2zY{-Yzo1DfH;|icm z+liz2cWff|$gl;?K;rd%VOMEX^;5XYYZM1GGciw#yIZOI;DAdpB^fnEdpq{;^0G%$ zuk5~leP8sH)Sj)N^Kw&+ky!yb5*fFedE705v6}gn7098vR|L;Ro*rjBhDqMKZ|9$8 z&vE^3drKM6I9&(LL3K}!K;MA8k%1_UE`?s% zS!@Gz@@GTpjTi7n9aJNI%ouIj2Hw(E^A1a*^{my^)^0swjkP`lf9ZI$(Kv(_Qtm*> zIe{?@+mU_x`?_T9KGiSz7}*GUpgI>VHu|?y`~m)GOk8Hi;(ncwE`95&dUN|m_t{cE zr)y@%p#_&y??o{K28*2>4WrGijj@rH&MD{V1$#vKk}8Rd7!h{he`7Cm%XRvi@8Bi+ z`|53~5+G!+LJkt+EbrQTvC9N6MD-qTJ)e8}czBD__&ZpmTsU@D(?OycQXrmaqBKtM z1#Cci8d7jExreHwedsJIoD>p2uz7|YAk69X7%-y}(R|4LGT@!aDbVjLGA;wI(HW>i zmQY% zZBv)4_AA!P`3i5f6bUg-Zr?A+52%c*%tlL1RkB*|`YFB6_PSYrq}z(>qaEhto=W~1 zxiG-nV-Me#^P5d_tw109?vdy{$w%Wo+jEWRCa(vxZyV3TCEF2SEmzeb?<*gn6zL}8 z)ng>4q7@{=9@vgXgp3&r<{reumZqAyNj)#kKvl(%zG9LVIgBs6@&)3WIW=<}{)s(m)%3juj>7Anxv& zY(u#a9FRHJ*<0d zQTDC2qgK=s1^P9L-bu;GQZgK7RyVPUa9(*5P0-OBj2za@*T(8@p-HsHaRg40Pm%E% z6(xA(p{}_-zSbFfEUN8ZJ*-?dLY&JAhV!vHwFkMs9_`S%qFX;pyc`WbCY{;o;*$^nn`!(gQU9PyKHC6nGU%2!WdQxb2?#AaM}!gL?O~ z=Dq$nc8ii&?l~e|2QkI$Z|s?zaPEAr7nfiUVy$8i^?$^WBA(uK5zT2B2S z-vZBWGkQrkL8DNMbv>|+=0$F;;;68Dse_7>tL}Ea)a_O6u+Bd_CY8L*y^|51x+Upt z!ngRmc(1r$(bk9_VXh&;foFWAVlLmvOmMkqTStqryLu&fzwV;P@SF4$>kems7rpCe zV69AcLtJ*Wk82<1G6x6@a@RPw6KN|4psIQ0<4 z2FSPfJ62U6n(AuG^iNJCw3h=r6pDfS-Ag2d2QYp7}DQ2ac)L$^6&%(bc1x6`g=&ZAs<@+%Be8OKVXo#FatOa`Ock8|2-+@5BA zV?GKj(*5R-ruRk~{sIAY8;?N~wBD-gic^qB(NXaRw3Gu?Q#8Z0Y;9+aw`MUUatCOG zwWA=5m(($l4(KF98ny!aimk$@0CRy&J_5hsXflF20P5`SwAg4eJ~LIpI)s|PnEo}* zfLY_;^i=8-WSRd%h_Ho_5;sPhjjp41*y~sh$<>hH1XX%{c7C=flb@EGd@8YP(#X{O ztONNwOU9Llc05=?jGg~e{JHveTFV)3y0TL)Qwnl{n5 zVETC-s=9~TD2+o^sD7^*t9!1)wOp-S6Qlj4xuQO%TBGW(o)7scU34Y-LPUq;qSMfy zXguTu>(TCpZdfto^UjBS$V_lMOr!r$;m{Ye&?cB5KBG1R?{qbFk9tYg6GF%|{e^7M zF|+`BgcT_y@c%*z@@vNNG zRBc>`s4?N2!fuDhMP#nsq$)iJ;p z1}bxuuA~RiC_TzpWrS>B+Jl}+zoPHb+vs2PY`8}~rxU0{#3@{i_s5nRf`Glz71-2Y z4HjT;EI>aZmh z3}PJ~2??D~;C5dMlj>FE0qQt4iHso{aDQSZuxKTaP|GmD{M{mI>&sN|S4&=djqslB zb=5P&E6Qi3|ICp4QJoW~rm-_UGpD7ur6eSEijR%e#Jr2UpD0Oro}x}RCML!&hzZZEgaHs3VWg2Ma) zq`9`x_o3pq(SyLZcM?ZnhR_E0#t7^*#=zTvJL-Ye80MpTx zp1}+8>DX%E``$z!8LEhI<4G&0ZJX-{#$DzGV8#S9)Nauf^+# zBwBPBXe{pq=LDew8PCEw#!6;^W4~+XIe3nNc8%2rna0=Y4^$!42>al+ z{E760gz^jIN8$ng8@hfSuu}N$4jS4Qur8jkPXA&fFn>r)zYl#Y4%P|V3S8BAyc2vC z;+gQ34;k9s@uhex-W@WTmO*b_NVpTl(9_!qX}x2i4!J?4(Hp^ycF_2*$;a~AdLMct zzuW(Ht%B^VtDr(>LH9O{hx2pXcZr{PT=webGupS=SMB@B*Wf$GFVJ7*|2W`c;PN12 zP)ksHkRb4t|7}0qcbG5Nm+~Qf&iZ`v9^-Y+!z^ARs&Vfr+#~Q2+~61TNASel@$BPF zBT$?AxU6jVYp2>?wNdY$q)_<`~l!<1FJfqqnKQsh5d_n?NZY0ymv%!idKb zw~3LYl8mDKpleVDDwSkVIvv4p;cmeEq+oh*mV8VefqQ2xBn5tibf$yEE@B&TlK2E# zj9792WY{#5WiUMtr#FFLVyLm%xYaZR{6gog8k-NyvXk4w+j%Y~7Yi^LMlxTqDmibs zCH&=rEy5Y@MIu6UT0BWo;^FU!dtUbH?ES>s$0yGx(_ot&vNf894zR;&*u4Z`JDgb=$xY)`@S|FJ89CiP21GA&9ORW z$F^;I<~z1++qSJXc5ZHLzx(&DyJkA8^G|zn?!n&A^LgS)U5Fb9U+`Rf2i!R9Ow5$v z^uRX%PhW}embZ&n;OXhP4&2BNu05a#-0G-;#2Ga(%&)@DsD<5bJ8A1@i?ErkYO4{F zrEoT)%>yg`bL$1@k!-PUw;lnt(^Km==%akMet>k26V~;Rm@~(^0CHm<0Fk%`s^TIz zIZOenQPAG#cn`GQ5LdJ7*8ktE%6)p@K7W@0C#Z&Ms|4%CMH2FeMI=5{^N&%wQE{}F zw9WJpj0z@|^^!F*L>1DFeVi?3GdWe9@tm!kbDSrjQF+4I&l$yu=e%MMU~5CBhnQI7 zSu$on<|{@H<3GBZRz_PyeMs?>>&Qz;&xj$!F@z_el^ctDf{nyZ#M}+CU_}1;+xegR z+WJ0vdwWHm(H_LT#Lai#aSe2Zy1qjtsfRNLI8B!v^BnDg)a18|?Jt1jeZaoXKEpo5 z-Ua4P&i^MxGwozM&hE20;k<~oGr&_`1{Lq=_T7+WgWBVvR(Hsub+mP!b8>*+?RL#{ z>)ew(PR}|}Qa<*L^s@uc0@H&Dn6H>+KyA|CPT>a=LWysPi%2Qtm*n9TH)Ru*N83;1 z(KpiV^j?e;3>71u*^N1i2{$O_9p-)JCFV}%Y{+H{VSa>eQ8_~bwA?)UN7`VThB}$5 zr3|MulY5ijky?=M6Z45j2^_*oycv4D->~f=38xHmJ6Hj|-u93|JIrtPt?}_;p7i$y zJZC%uJRHw!_cC{DH`V>wwcj-rswQzRk5lM;235B0&c)72&LPnK@9J#ptasKqYk_Um z0`}hS&PL~0=Rz38GtTEum6Pr&boGbR;scl76$j3b&F)X`P|tABRS(YF=)DPg?S($2 zuc!Z+za(%j5Fgwd48?51kgzMT0qk6y0XGW&2j8A>hmcI%NHh_zw~f89Ub-{JY~ zZg$JyyM8yzljN!P^zlsfZ1vpqGKnDC20CYBI~5!Vtg5_}_^R){%(X~aZg{sj*PM+7s2=D?l6vOq&1I)M5g!B08LU*iw=TYTSr zH-W=E-#5h9#+U62_u+j;ugLq!`xJ=Ur@RNe+r1mSE4+(=#yZbC+dBuoF7>YQZh~6N zDM(m(<^Ai`d-1*)UxlxaZ-#HX@3v3qBf_}%_pkBa@~a>PxNBf#;2sblvxB1`m*;no zh3SA5OBVP__N{f1^x%{Iqvqi z_H+DGd@H;sy$^gEAHk>hZin?a*UN;FIP4|+vU~#1EjI?Z4NC&O0^|L!ygV<_x6aFP zcXS{1NqpJvT-OV4Uho^H5Nq=fcSSirdWQSY`)|PPko$gmqP^pMoBe(KcrW5YT~*#~ z_>j;ZQ8Db@_(8aNq;;en#3!U$3J*9~i)r8RBXOGu(}?wy+l*<9uQU~FE5}4jArzCQ z(V}UuDY?`V>TC*1nhKoNvp7HTKSJ-I#`g`QpqvgF79u9S_SJY!V{VhH$UQMC&u;js z2-v6C2GAn63AjNET^l@!58zS%Z%8eO4Q|G6_XpfPTo~sBm(iQ#8wt7AeFD#e;od%W z0@N97_D)WU=b-B|B)e0*UA_D4ah98=JH~6)wy-+ro#!3Dta#HGYkO~;`@89sX{QtQ zM0m!!`Oe3X8Ao&3d>_0<=V8c%z?zFJkRRe)Z=dPt>Fi<6Fb{O--Cvx&d>mRl`!~gk zyG$5NdPcrQpGcoiT0_~!E)31&Y>31sPKe(W`7Crb>pr_Isw(DCWO{UC{C}~#BCqh` z!)LI{nIU1MD0euPT^~{$b|a!y=u!re#-Y+-t(ip%;^q;p)NK?WP__$6b-0;6nR}rx zANv=J51`(^J{qKpZF9YIjdvcgEwlW!90%oC8{0MeCi@#xt)YkIg>AU0JDRQ68nzm{ znhd~Q>8k&Piga;sE*%c%@^89f`eZa($APoxOZ^pK>o^*tIl75Z( zkYkT9N5{mn;`=1EOPiaKoY60RSo-pGXSz4jp7S;TZPAC4(ZwC}ZP_(hGcsA5pp6~}HGV955aA>9kVxHWT}!B8j|76v z6YX9t6;W!-v?*FHq+~osI%$8YKdR?zd+Mg>{GcIvseSyvv?U}Tw9$)oKXln>Gzd!HM}&eH2l*0AR%w9X`(6INHBCT#2bx<$uK$};bzK! zI`ms)oGuyW!6@W|_8pRnMxqzsU;hyBbn9QBjkIEpbFjtU4{B}RY-+; ztYW+3t+vWK(N7@uqOurISV8tW&QA6$_BZZS{?^o*{QR6-hvbX$6}8S{bqiR7~4YqplmeMBf=0kR$Ys`nHBG#$Bdfkb`3~ zr$Zk()Rb(dTY8(TjBoU}40YxcmOOKTVLqCtzX}-{a#V!$MGAFMXb-d(XvWroP6rEo zpws#?<2*wg%7@W>Xd+v_8y~@c_nO5 zR2x1mx@F|x$Y{PgenQ%#JWd6t#iTYL+sIoTZE?JAQq6@*U)k2u$TD~N=E}0l?q!Dx z_vR#KY)oDjs}H}#a#9N^Tggh|MO;R3r|%`)Gj`bR<|l@8@KarcTneLZ5`6M}WC}7E zx)Ef33;j^NMvpa?m>!#QO?pGA!D%>UItKnwo+%mnG#<-M3ksoD+}|AsX}0;--6P{8#vL>X!f3d2dG060bw#x=$p&}|-T zt~ZS{-hsaQFIZC_=?~~uYOky3sl7;vbz$HUmB+mm9hW#gH72bwnVaw~{z!6S)~dq# z%EXo*+Q&2$b(+vl*s`IPUco8C`fn(c_l|*xTSJhD`Q7R7fYANoes3QRXwaQ zJ0mY?R6IHERLoRdEHLSciS@9_5QeSxf+}eb{X7? zMq3q@3QI5O<1T?rOPS-1BgYwVj&xmgQXKtkd61E?!CYiIY?!WZ&?g(BjM>H-Ly_K! zj)C9o9Ga!eMU`VK(O5JEc2D#(^<2TH*H#^vA;cxCDV zx9~;gSo3>GxA5;;WoW&Wq3{U3y+zrKn>K$#lUEXwS)5YAbV_km9-OL9Gis%uMR$k}Gkf^Zeis<9K z`zXe?z(}&1QzK{}5QW ze5e;`3ofB{Kz0>rWg34JVAXt4&v9JpzunHoSNd(w2=G}+W1Gy(ypLeWu`tnp}Unmpk7z0$oz3&Az?S^pf~gSGlch6SdHX4Iqx-Bq#q zG-!?%nsZHTli75|e9l}8%GXUMzj1((39I5i!wWsC`>B1ZHYpb{Rv)*OYWM1#& z{<&!J(bC4EJfJ*H=4JaKQZoU z+=ZAkki?4u8 z)wuz$H_W}=(a+w`zRLavQk@IUH;i$H>yQs_Qa)1{fo^zGxdsTg0wqhuSEZ?PK>zwm znW&ntZq&Ykz5kVNJ$hQd60~Jk3`)qVks5tQKIHlIgtfDu`HpF>>4T}n9BCe5+GtEP z_As6@PB$JgywcBsitm1)Ec4NQ$TSUBMN>v-R+-ZLT)LR|Jt>fVt7u$lY4LzuO(W0hr!5|MB+r=H7*Du^4K@FBh-wl=CacNTpZPUoIu z`)Un16@fv-XsQbC-*3sqq(Aucz}&C*?eSN_$h4pcDAUPr2wQ=cpW;2>y5-1+B*q5o zPAkQh227WX47W_vURd|}keC!^{BGQ6jD?-K3Ox&? zfJtZv-3{$|^&RC}Wvn*aJkj@z@+u6CpPczB|8C*@{LfhxY3Afvsok@`l)SC4?fA0m z-frzWuWoy@dRKly@}#J3VeIg>QKRF`iC2^MB*ewM4yUnh6H5al-G^Px-mTcJ0+4a^Dw6pBrK)LbxsyCHcYJt5Ch3W4x>T76A>68yR?(dD|1Iv`E! zcY;6cm|=k--8kLU+l({cHEuABG~^oHMzb;6mvgC-V%+_qx$gYKSo zx$3;UM1EFPZdmNvg`dH~M#+<=XC25DWLKm!l7)#QQtGqr7fr8e-8P}Y*!e)G^=*1p zrxxU;M#RpJl1F`rxs)(CWn5aj)JX}Z$d~M~)Oq-+!322gzY<2#C?U5v>7k22!O)+| zBw+}d_&R(mLNy84EVK{Q+vNF#_n2%y(R13#bTr!@L3R3t`Ln65@hQ4WJ5M!MF$6yO zuCn*iHPWlnY}rBCIaxc|9qAmYR!WnPRE$?T!7DmnZP27aCy$D}(@sQ=>E@$L(01rG z^rilk;WQ{Tf9h*NQMVf+pw{TWgTm^9p%wH6SLi?L35KWo5PciS8|$vi(mg;1X}_ra z@**G!-ZehnY#ga%zVM4Wk<_x5u^2teBQ} zD0O6$~1! znz8{>wLQcjxi4)reJt%Qc@<$Oc5lG%9p*MTlI@MQ9=00r77aIt8yWfmx=UJvdZKEa zQm%NUFe?TrH!B(7Zyq8)CGVl=3v}_HvflEaK)URw`mKDR+^D*y?xE?a8LUxgPQ%{u z5SgIMMR%inAR(+q?*W&ILw{9|>L(ixKnK{aw*qq@2)D3{Xe|1l?kAFuY|++gE+}8f z#L@z#7aih!fjvV*IPZAjF%#nM#a)U07K4pjoW#ybE8bguq~(Y~N4-nkLTN#2gCB-b`wxL@;udJNtd2Y<6A};S*aKFI#bWFMS;{NW9lATn z7-S!E9tlT`nhWX!sxQhELC-QbWHe_U*B-t!YJZ$KMVB=s|4CuTqHcwp{KA|knP)R> z8Tw3XRxrC$?&Z8S`Pun1b3bIc(jTU7Pnnl3ZXnkZjrT@Plq13S=I*p6n7M#6$fBk%9IY} zUX@K1s?Gzt#yrg!tyZhie$+CM(?~L88Y+-yh!x4xk#(Dp8l)wR*-Y*KxT5=%sPe9Q zJ3==gW`%vDd$sRH;3(z=ZY}X11juAf!)iE{vn#$^qm8LR7N#ElC3W@oe>_-{T zQeP(Ritip%8#OE98P~w3vWC!UR4wT-VFJz^oZ%;VpSg}WHrp0j=9$(2lW8fkUxQU2 zRyHW~vaQl-KsFj9_KK!L9)Vw&B+3`1h+2q_i0I-D;*sKQVpKd(@<4)@c9HIvewPwt zV`LQh897Fgt5~ZjR3gfuszbn^f2dxmQ9(t+s@b7kjJRQ*bOTPtbtFtj)a^iGk$>9d z+MAlw>S}e7rW>*rje~yrYtuIi%T92juASa)!Or-FBs1k9eM(4Q?tq9kkw{c{%=Wm4 z2?vwJDbLe)XC2DXCNQz|^%M$eJ)Vdi8+hUE>#lWH*@@N* zrs2RLt3)PfUIWYWh`ghWE0v06qIM#KaJw)=_(8ByFjuf!@JEm&94Xu+yew1+TZ&GJ zu;R|*En=CthvbgLA*q(WkuH@n<=y4$VBNW{m;mF`LUkM{?Wcjcy-M>_6RK^{Zqa_w z{?=a8?$+MY{?)G1uG5TDJ5)>6>owc8UyvIp-sm(HSZb{p`yXdLq`wqkDTJQnJG5)e z0qj^V65cH`Kib2Ok2{f2pZqqpMaIFbvpEfU!hA{Lgc3v9rwVFSMfKI1_&QvDzxsvs z+v_{lSJq9bSy1(-qJR0cQelxK|9g%mGcmnS%9zAqal`nFBkzP`x$W3nnRxmMN=MQQ zJPne1?s}KICpeSsyDenXJAISRplw$FQKkW5_K38%Bp`ALCki8k=LETe&&@}gk2c?J z{?u%2&J+w0EE1d*V1?s_mxLl=vFHtCSgaQx5r;?&k~h+F*ZXsks{SC*UD?NYJ=5BAo+G>`K8jA#qSD-<`2m!%^a7eNbZ<8Hf|(;a^&G~ zG_;z1lG&3ULES{UjW=Q@1&F=_?kcC!w!#tw=T(iag?6xdl5&k)E?q4t6AOg51T4X` z=3dQbfLzaO8q)M%)6%9nO%IydG~a91G*boB1@Xcc@TLtDeFBDkl;pakR{BZ0TsB0W zp*RJ*`Y2_-DpGx4ZB&P9c4_8l3!vH+j}#&gkYXJVydX=F6UcFdfCxZohSiMIj??9X z0yN%aFh8|6*xxz+x&+>Z!6w`aqMj_F^@b(6wNN_S0*htReY>eSC!Oc)DEt7)U>K;TP>`Nt>{KL#V{=2$c99yP3`rHoWDpae@9b(nm8#Wl^+| z_m<8VM~KRWs{{v{FE_Pn>fbb_sirCLkJ>b`N!66l%xsQq-r3w)@LHf1z?>4U78Qu6 zig$@2G*p@>yDM|a#>odMmctsnONCL(;H0-5{5LNl^`M(h0Xy|t-FaO&+8&LEy7>uR zwQe4=Lz@n77+)uZJkJ3pwq=v`gDoGDEd!oJc%O)bkE9n=BO`*1LMBKGPZ9Y&+8z@f z-;nqx`B~b`%puvQa+rCo3ZjZHl@2doQSq#DP&HEHtX)+nugj=6)%~pvsY$6?RN*Tt zDt%G(Fn?}N-^|5nkCVTHOSDgPIFB4w%$d)6Nas?wk(h+u*qMR-UZ?9Utiio4&x|73bQ2oT73%sS5^W;F&}~C^8jis_)7QGs*4Z)E_1m-6e*h!J$CE>8N`^mV0Cz~l zl1NL`X8!xwK?xs|Qd6I&Z_TpgY{>hUf32{zC9l)eo{PnT+IDF#kPk&Buqxw_ zTQD+@bm>q@_kwF+B8*IL{W_pvWb0?5m*GTn98TA@x@RbAh%x`O6xi0-vz^uMTiy+U z^H>L=k}{OOljY)c3$KXW0+peZI8D4Yu`cCyTHj1*Hj+Cm|4zYBIL&k~GnTU}(aKL% z*Q&?WAT@=x%-ZfXXqC0%P+7N5<~Pip`C65BK4J%sPYkTT?RD#P6LklWaoS6oWUT_3p!Y)VP-h#YlDq4@ zS>Ta7h+9PJLYu=J#m0mUh#VhN6#qVfn{+XGYN{t~d`3;y{G45R#RUrr?-x0XwI%&a z$CXYgT~oTcv`1+~sk`K9NlXd0_;{f?|8(y2EP48ApvSdKq9)9Y)kN=!oEFh5EF;t% z@|HQBuBMzP)f0yhD)Hm6DZw|sEuQVJmyT@Eye_s}2d#b-WG|uG>)J0Gjp~v@B0DE_ zNFx~a4)C#$XC;=icwrCP2cX%e)pHTRW=OH8^ zG}~&i-E@p|Klbr4UGWD=6KG#qQ@Pi86Zl`_Jc+kb+-ZrKo3lIS2@4)U_ByRJw2WQ0 zsAO!>q{4@Vl%n#&m-)fG#QZyX!*b4NEKe0Aze$oO6vkQk)qDZ}R7_*cjOdZPwqcz^ z<(#n4PMnI6K1>4hIJ1!Xp7w@ZLaHX+BwoXn1}1pb-i`iSzRoUaoq*Hq2y`aaYG*4FEHSPywm;{VZVIUgfh@%P5HPLw7a(hg_c%R5q}DUGO%tG&~5 zZ5v;!HnsPm>qspO=1H?=rOi*VrNM*%~T2&?=C2V{6wa|;Kqx2Qj(d6aCAXJ)$ zQjXC&GI5+E;j*anG2&QCT*ufc{9lo~BMKqItT+2P{Tq2B@i@K$Gu6v-3and93Y}ba zO+HyBll%~dHWQoa!b;IhpNe#kG#_bRt)h9iHGv$`$%YU4zs!?48o#iR!Q z7Z*)}{@gt9#t+mqt9n5mLoe+=IIWFPi_~qkuaLe#5v+qwc7}eiv9~GF3l_rtV{GXFXyKV(ntRV{Ks? zX+J2@)SL8X7L79@bWPZrNMY>cBwPvx)W&n8J3{55fzy%J6@Mfkgc24zgBl#ds00_`^mbT6cgPoy*iIn`l$YR!-k#} zJ*IbTS5Gb5o8K?zPu81^F{vYxRwr_jdL+Jy{Te9=I}kdLjb(nM5J+Q4pQr__CU!UW zeI^SM5kH31b4IcwLI~{F+*y%serVj&_`ZqW#K#H4W0{dLoW;~-xa+>wPNsP%Ql=~h zV#YXyQ|^}i1@6llc9S~a|7_v?6K_Pn*+=*4@VdlOjnW3BE zs^Pf7ismCFkXANd^Pl>QB2Knh%8@@)lq+4ZJC9QHkZ)*>F~p3qOo4u>*1ph{>K^DU zw0E$dcdl{|aAE8>Ez=i?Onj_%tx0*XxCR?Uj z@+~WXaz{7b)Q>|kx(494^;EJHx8xUr-1t`3Q(mS#p}wIV2x^Wc#&pYeTT4f_^QyC@ z%kCQLc0v{Gs3X+Dc6jU^!HGTy_ySp$8Y{#4#$?iy^p^~aOtr>ZWTxW0Y>}po;~Ry@ z$7I@y=G3(AFtvC8fu|blx-{1PD+s1m#Ow?mLz59768BSvF`kEzSq3b z{Mk}r=Q>9?+FBb-cY#f{(X>LJr1dHB%3@`S@zB z9y{}#!$Grp+GZ6mFXwl4N2Z~&|`lYkPl7JRdzW`kjs{u#_ptO0}UQv8vv z)EZoU8Czq2XVw;Pt5tQ3>CU$JBF_H>ByL1r!7QX2^VQBrhlWK#V$WM&jS(zNv##XC|}b zYa^4``IG~g6|SSkp6a1MS6nIlD^!b&lD3M|8ZQdqR?}|iTt0%%Wp7Yz70oIB+BD^ZP zk>8ttF6KsDXk24tcF07so{&!VQIAn|#F@C|_;A|ZP&aQ?Yi!mYP)QVQILm64M-_O*1tm}00Tt@Y z4NstsQe{=x$xg1L%Jv`BQ;va7`?aly<%l^7>P~&^Kdma@Bo8%9fzI`xF-HFuS*AIm zoFL1Veo)@Cut;Umy|M(Q@_J{dkiOW#>j(DhKDO>lc5K|@h}E2h^m~N<*gb>@rZqf) zKQ?Ag%&^!AakSVcJaNc<3WG=?4kHaD4InJX-NwJBYz<+BbK#!3CuU%RGWlKV^;CWG z?u2#xpJ7498A6edZ2ODmt6egicqLSlie#UZ4AFM0+iVL%ObG~?Q0wk+ega+%YQbVBjD(1BbwI&tDvpbcn!7;BgQa@J?Ub0W!U_P!%_UWDM_DF-lZfY z?@JgTTM^ZtJCtt1{&GpoqSEq(n&J{R0c!Tw-mXu-_q`iSL!ud zjRsLGGSHex421nw>NGT|F_KuCwMSXm} z^?mI}lx&)7X#%b3ddGX$a?di)Vb^neiS>cmX_{wDN6)C!6(i&y6)NRhMLSu#lqkQa zdZGOc{y?Sn2eM0VGd;BYF~0!E%vkeeAdYvm&9&59&RAo?@36y`ZvA2wnMB5;`h(iN z%K6I8=uXcbI*Bh%TU>CgYG}J&J-+m9(W|Wey7GjqYl($1FT-n@CGBYP&b5bJWSwd^!>L{pHt>KNrZMFiYYPHMfHmFnNBiE!JeLlWDNX04Lu{@J`LPPBUfeYa#E=s0vkmgw#Q$lp-4lHI;3kM)0U+ zYq-cP-5PY1z7Pm)&Bn{%7gd{gTTWQt+NRq_+Fjrg4nS(b8rxfQrv8j(t~v$X=0Nc8 z*-!b})P%wfwU;`$x_0flx&6o5sFHixDJi33R)#5=gJ@4_Hs%#hVc6<$X@onnIr?Js zV&3r3W6YPdsnjUSFJcEg6W0$PPL5_^IkfPZk(`(@2|rR^r7P1rr}j$h8e11d4f8R? zgf@P@v$dthfFOf3qf|mgrsA7ok?M(h5_ExdkQ_V#=Ex_NT{&AB1$56!on1f1_}h5b z#I;sCdbwg;G{shj$(NwHZ*osGvT5N}@itFxnEfDdaAL!8#VYoo9~v9cd2taqGgmhc5}6!@f>S z1nS5sVg~s?ayWS#`4(*xdl>Iq%=!4w3CT%aQXDvT;l$%jPgpjrl*xznMh+6`T6l$bS^LhEH~lWnM@ z$~nsM-g?LM%n)rDpfA-~)Zxna^1t#@MS(&jKP`95iv?sQ8>i&wQf zD*KiO3(VQvw1$K){P?K1JT`Ai6Y`yQ6Bf=bo&e4~x3Lv^#!Bydyu^?E~vX*Q5KzMJ#FSGE*1hx&k~SwP6PG1S zi9a3Rl<+$7zr?`_g!tIFpRu-BeB4;@3wDhj7S%bbHfn5S2(LrLm54JuZsbPZp@<*h zA>mD2l+!AN#kfS-LR901W0v>_d;U3BJHFd5+n!o}!1~k-6v}(RM1u$&q#2S^e`uwe zRnRwot9q^8qnV(Y34LRu%BfarT#%)=P(4Z=R3Fl8(7aTW)nRI-`i{l{yrpMQZ>~mq zgHpXSN<^D=ok5lL1>BQ=&@y=TqYuym;6&WlegHD~E$wz)P%kr0wJBWuz-)Xzb+9CE2bVLasPxL2gEVWntH%^@Z#5 z@p-H~N#3r!!kniWMQNi`C#611Ih(X7J}YKhl$UoaVnWzd_EGwE@={_2VIJ;&@VGC` zGsU$57+bUK6lg&fZZk4NGgXzTT&M`ZO@0+{ncG4y^r3W-?31jE?5y;tG#1X1 zk7Qa|w!EvnpZuV_R`FNC1zo{9=vmR#Yt?woSxvrnsCF4JIWHn4-8*Cek^>J9_?$lN zW$k*+E7c&SMbT54pgODGg8VUDvc`ihC?sgc&L(Uk4Fm1VKDvvsmUWWdz>SD_6SCpSEN)@DyLLbl)r;I$Ux}+X6IvamuB;_ zXc;S0)06!P{o|UW?GatMboO_~Y-(H5QoI(^BY^Yec>21;cAcfd6lO0r(hdg2w@2EI%xo8dPX4iZ*oIodfe>j z1rf(X>qDv;*;Fy{1Wp}%?*HrE@0K{CZ4b;_joHw7A!%5u<%)3m73osRRdKP{D0(Z} zCQ1?=6!sR*gp}n^!fnC{!W+V_A~A4X#o}cWAM{)9N@oK@4wx>bx}7lmFFq z)R)yy)g7RQkq!hh6cR*ssgpDjK-T`IaLZzV7~fJcL*>*Ip#4ojTUYl9pCqspQ-&Kw z_(8%^Ptr(?CgvVCJ!~8Adi3hp;R!>NE7NXee9hXDL(H3=A6{rL(v=9yu2y7MF{;~E zhgUzXDzD;K_A4J-`k{D4(dmK(d6hX~SyQOA6FIB7s5-B3ftrdbog?`!&JphsF-5zDgM}mD$q{}PY!loQ zj1bm{o`|lCR*Q5ZHzbbF01etC>0e++Y?b$dD)33gbw!qvq0&PCR-hWK>H}SFL?u#9 zR^0(@l1FZk>7=gIl|+^&S8lJWt6p8LuI^itUqh>|t(;fRF8f;2 zv$$`eE^k~;ViqCeTi)!5P7KL7nh~5QQH93(+9) zbFoSs10DY5a1JVw4S;&j33(^Q5>P-06r-VwJyw~dELYBfj`mB1UtS^a0QaN~lJ3&0 z@*k>?+WjbEY_jfk_Vd*GLIOpY`M5^}4tWxFKm7tTi@lY5k~fhb5+{z2Na~b&K7*UR zBN9Q43pQ% zT1lr!&WittFro(NL2eQZ5M&9k0#|dKpu1qE;GBRXTmo-IvoK7wMbseniaSU;OV&$D zq$E(_d1N-(A4pt31gk=^;wP+;Sov$&5Kz}dNRxouE>n2bVq~(OVKQ0N_VX^U=ZkN3 z;5eoXuOd#O+@^ULS3@}5%Mm}KddF;zlP4@n&P-eRznw7qYpx@|ps0Nbp{zxDM8&s? z<(1}2q;f>%@`{b+$I6bE4lH?5yYh5<(;HT`=Fj+f(g|b69|nH_$iuY(51}P z>=^E`@U+NX(akZ`_@qQa^8J*>Y27ktS^Kkjxoh*9^6dp53WpZGD+(>HD9$YYQ`E6& zcj4;-alR<;Y;Fv!e9JN}rM*nKo-{cj7<+&}A*u&25-Q(S>>tb(^g61Yw3iT$TOH*4 zHJ+EQ4UTrUSWBd-(y$EugCuJwtA8kmDwy&n>21kY@p92cp-<3WaJV_5`AAb)ljoo2 zpXi_XpBJ+6IyG%>GB)*Ye&0M;U=hp|`h>?}WekY-z)UbmpUW1>$%;h^3EaNADQ_x! zsp24O=Yn=RWb_;`zBj+H?y)a)&Ua7s_Vrf=W3Zw4Qb=RFOnpxO#q@=gh0YE;7_m08 zHd@cW5<4S4Iq^zTXv(nE2Wbfz!!sMRXxRs|Gjnd`q~s3E9g&-ttIau`(;??<_Q$Mu zna~qTADG5X-Iy#&EQQpnV9Y4~iKr{QTj2sOBQ%-KXL;zasmI7iiKifKU`(*HzoWO& zJUv3Dwrim5ja7!VQr=f z(gpnm=RuvYT^I`cMWpzKxR1mwIV^1>Q_FVCdn*{q%gQRa5#)dtYOZF0b||u0hoI99 zi6)i#taX?@#Cgv(&Xer>=U)}9!cy=8!Zp%5N`G23<2Q3_NJ|b9x;~5>v6g3!Y>3{> ze-fjMCB|bCR0*#Vwh9FFseMx^sV7p>Qnn=jPI4t;6aDdM z-1FGgAP|#BEr`VPR)xE`!$NPe?W_{!Z2BeYZ?d15N$7!Fgt;Ct`3fMh^Mfl+~prBZ$j zx_mno3Cg?5VbB5l1f8JXpm_TMj<#~p8Z6bv04rp=u^!IPG)o-N**4q1InrD!-M>AF zzG?nXKd z=X1y&Ru3kYag{cVil>|)bt0PaTXDJApTQ--%J}77?`i9%y8b%O*caMHK)!RMc{mUz z&ls9P6Wku%pp%1=?TxlG5Z5uHQtH7Mt|2pT`&v7 z$1;GBEysT#Tp?~IO(M6WM1q3;2HbDz7!Jl+W^b00wKF7@eV(1cIm%&&&JO(^n#P^Z zz0GxS)5E%ijSrh2wj^v`*wC=jFk;w8?jmkD_k3tY=uJ)xX9@dnNNUJ9)(Pf!Aclm| zb7;ev?hrkf&ZaME9fv3d(}(F%?)9b_=c) z-;7^Eh=+Q7BgslyPv%q3PztCwsl~Lbv@H4o;QCHrd}5?C=Q5u#ajZJlG}dm`HP#!} z4|qPZp0dufRfZhTNBQmPjRzC0xdv zaP_zySS6+eGc$M*T-?e2k&vy|3z8VKJ&)Y2+`C~hgw^#{~`>3^bh3wQkyVt(UG1JiiJgNymvYiH54G)}CfS~1fJaRsA zy@fo@J@8(P@X&w)Fx|V^JI;I6)5^2bv)ohQ+2a}Sm3#O34)_)RQ2_%ciLiwjOX^2< zle+@Vpd)2B@bWm z*b{R+DEI&KZS#?Q<=#fmaJSjj(Y@2X0#ZyZuEow4jyy-Xqpy7m^et!FH9)kbffw+L zZIbPUt;x0$h$uQ+mVGmDM)3B(*00tjwyQuW>IF%=We$a7BCsNkIh&l9od=y!uEDMZ zSF!6bSvcCTF$lOvF z-8Ia)0+MA)Aazk~?r#2Lsy4khUNRm4hE9Uf3Qmh>hO>sXhS7#}3xo9Rj z3KaNwpw+we%M2dFNaIgqXVVtb9@9-AXU{ip0uLky9HHCIDL@Nuw6ugjYb+ZrnHEU6 zF`YEbf;)DV{)1tRS!)~Qz8+Xbh@`z{jSuY={(<)}${xLu|Cir3CL*>nZgl*lgfXDH z{+&EDRhL$rLCzeTIX;t@*)PMMCQs>~bUB_DJ0$uPoEyTq?;yo!0{dkMD`XJsBJ(%n zB0ZP3o?;~}Ao}sWaWgP00=Ip6Uc~*y^~;$9?!BHstlsG?bMACJvtO`9Sy^U{!Hw4I zj%rIal~7;%2|DswYL3RB`2#6fx!{5Rpt}XmTV_}m!OGFug15{uUo?F-hC>&5I*_7KeJA~H@IOz`ageX-LCPjMU(rRC ziVQWTI^G79lx%K!%*f<-8LxBp<+m)nQ0Oaolm9YrWZu?1bzWqCW&ZPgYT?(S2_=oC zM@zStrj&ju4i@ao!Kc4Zm>S(P>@mwrZ6N)F*}n)EjmHqK66^#O;TW(ny5pmP$F?PK z!Uy?j?sC_2#}NBmTW?51)`8x#xAQuzW*M$cPJuns8Z^@N$B=EB{i-H772D)2CiMzYIyL*VcPux8dcXxLW*6!|F^}hEn7EH2MW+oI>x2~MCw|T2|D)^@T z_Iq$HRqS|c-(m}aK5!3khYU3>Hy$)-^)vJ-aFVf3`;U5!VjYm=MPML2Y1n302L{tO zMqCx$N?VZIw`f7>o3d80R(x4FIR9~OdhUSS*SQgS?elKrt=bPu~Xuh z4V)V~aR0&`8yqn}xd}Kt_O-TGmcu5iUZH7Lc9AcY?t>hCXJ{G>mfukeSFt ziq!AdFE)f3ZH7sPc*7z%v8aXJ*F<0~9knFezS?7em@~^+;OynxWTwHjpVig6_X$nP%Yxbe_(?HLx9G?a(w_|_YcQh7_}763t-7sI&+|z^3joLzhd1F z9L#>kemYFWQYfU;VSQZ>^TZ7CG|3{_N5y&7U3Ck{&0N)9*7ehG1z$y!L1PFubv6&Q z@T^zCsngCr-m$^i7skS4u5+#r@G;i4#W@e|wXe3-K=9fDF1SzTndVXEa5KeJWC+$p zY2K^O!0zK+{Vr>>cRYD!=pDW!m6HFm)Kj^jdPr5<@@~az^7rO6Wo^hhmi;Mrc){+X zU&Yem6GgEF{c@J2ze=1Wc*7kWx|Uf=iAGEP1Kc&hBwyqF&(+Q|!PgLAARW;zXd=SG z*7#aM2j`J9)cMLW(i!U>>CN(g^v?xf=4bCHPkYxjyVo+(L^r6kMl~7iYien{Buadz z`Bd}$=0b6~(yp~q4bY2#k|%MWZPta?Kl7~ygBeaw{YJA zhRIbpx4QupzacJ}^Os|%{TMJ>#o(V(ScI0v<|y+n=**tfuhY^rLQNmtcGDTh*FY() zP524X`;2{stIH2o@v6I3-Yrcpypyvsvq#21neVf`c_BroN(Pl}Dce$#Q^3fgB|jC+ z<_-#x(x;MG$bNskSLXiaUg%lkwforE9HcjaLA*hbqK^>{{@4G~yB|oXncn`ss6ZEd z7`lnDh43GmgSY}s-cr{iTa&q`@wsleW{HZbJRoPwHcRG;{mqi*SIxH}$t;iz02^k$ z{0HO0zdm|`zQNo@Ico)E5X;f!I|o8=)|=o=9uGhLHs<4|p~eG{s9|c-)SJ|&wLgp_93!w<^i2`vakUxbg3{70q6!*3#q$_&k!6Nf*r+r z;4z2>`GW=saiqtjR-_$-{$Pt59ry%}>rcLD|C7LU#6l<_2}lEo69_`okGXsY+^ZcP zkb~5k_8PY95;gCX(Fzt^gR7EG;)~7Y;&HH2-UEB9lcA>;t~jlzR63OPDuQN-wj~(d zT7fN~+3d7L*dE$sHk~a2W{b;!Y5&DG4=lH{!15MuIbeb%{ zeG{B4FBBZLN}p#x5V%3R6+Tc@o<2M8N71;FF~uVb#^#*PNKKoOdNu7!X8*jl#S_Zi zl~=3QRrV{R6#6qeB+&#nxs{wc)=j#W;vtIhzhjO@aD;OY26d?S)c_)g@I&yc5+ zCy_oAWXK=vhJUQr4s_tFHm-G&d9*QH|6FrPX_lJQ-=mvkHI;Yn>jvjL_z=jUOD$NPNeQi z_9Tr>DajD$5=w-Xy=&&S*jsb3a#6|gTvFPxxE8z?k-Is6*@sv+nQa-%=(+Ugw4Y#- zp;9 zugX*94aJ7M`I%Vqoj6JCwy1lM6;-e_jLu*-SV5XZG!jU}F~s{s6ET{UK>9)qB{B(2 zbO@4x^hGWs473*QhptEMXa(Uf`UshW@4`ZX!Ftr!#Y1u>IY!!AS$-LB0*U6VwhOpR zlC=}HueAc*5J*oi*70-;z^e3F^G35&L)LtS-Sg*gk~B%XM;oJ~=uz-n5sdwfe~sfz z?@UtDBhwz!UZB{oGckbuaUDD~p@ymYIlAH6mYQajM{z*bR`Rs@M)L~EG{te9+4duF znf5nqx?n)c%dBU45rvbBnu}_RWCd^X#rctibBmIT`Ne@EUa_x8T6i-*ICpYZeL5@k zT;dARf4t?9Lqc~4^`zC2m}mml+pqOa^@j!82Vw$Ve+t2f+x7D#Xlyz7AfHp(68Y=EhU!Mx9u04Hr+JqcJwjrz8Re+@t3yQ?%QpT=#PNMYN0U=`s)re(X!fH1?<_!=2hlb z;5cXrE$-O{u|7#ZL^nyBq}c+*<%6zonn$CGZ>E z#vHJjEA{R4VxTy2fF0uox6p0lNmCE=PxDMmk@YPQw5C`wU~XQ3c|fpPZOnq5npv=_ zYNr!xCa51N>*YdOytFIiz)+P~x5zrzn?O`ESB1-BFC~u5xR(2=Fr;)_d0yqG${Ce| zD%53NOAo+lLU_TUyq&q`oUu9Uvy-y=WTdB!N`4XFUobm*M)*?pQHGkl5xo_dpG3Y@?)d!zlDeTcmo z3}aWVnP9$|V<`lG$1gK#nPAaDuIoDNkw~mItJ`|nO0vdUTA8na*-m3DG}ak98jcy- z8=b(;VVE1tLDqJ*68m$YFL!sG0=nd1M}KfK9{{IrYwLB(d)T+~8rJ9|!1}U4Ri|hz zZzn&e$XCDA%{F&)<^?X3ItQH#J06n}*DB>t=J~wgMK4RpWh=^Nmpv_QRgw>_$%%Ob za<}B1%s!I!J9Av7DZ`UKDy>_JBrzc#7Yye0MKIurhugMrgiXpT3VOm~1d zE;65nD-mKL0j;9DWgd`#Ucu~cxrq&Hz9i!og9~!h<>0fps;3xApu1mf>|>g5?gXCP zOt@xF+Zp>+Aks5{!mhHd2mbO$i`dLG3xPE@OaEBQ0@{&ZNm1Qag=$*qHXGg6WcTKP zo-mY_9GnsEjphj_CnThdNsr3XWk1OIp0h58p3^0JSk~ms-5Gz<^U|-UEliuAHXV{S zuT$Hl-b`soUY>L$VR)QWz>Bp+^^2Gmx{!5(-aw8fEWt{Az1@EtcWqwF`Tvu zXpX?!d?lDxe*!c55qLyP?AO7-I>|c35@!Ai_inmj9kkZ^>)PpVLT^B&pKGWwb~7yk z>NaAX2+ockwxKqfZHd(gZ2kx422-Ok7@m=00|Mk?s$m9XaM&iL=_~9q6TlRswr_Bl zVNYcsWTu;dQn~{yc->SzF_ z!85o9^Q;doesi&Tw&@{^gDb!S_Al_fiuFbC{LcmU8v}Ysn}G5{htYDH@i34+p2Kd+ z4dX%Ma^qNIXJZ~%ACur~1^jv@%nCKeRMS+`3zNV+&U^}<76y3#+FJU+T~%q}fgNR! zIo*8D6mQxKR;7=?Q3{3e_dipF<-4`nUg&!5S>f-7ClheeJL)`!EvSk;IJ8GtXvFSF zE4L(iSj;rug4osk<%03To}w0U3GwFmvk5&CHHqVsUM4w{97zw7x+I-Te3Nh?esr8g z*j}(8c7M#dsQr=i!Uu3VhqMiD8#IWqkLIIHBYT0~-2)XMF6>9(rvI>SE0FA`10QfC z?4NZ2*25s{e9LLzFt-MmOP=wKVWFYM5M+?)-{~Jir|+izy8aeio!9!0!2f9g0+d-F z&@&B*hE_msr~{8yGw{ILz@F1bBLloZn;~V)F}E_$0VdFMU}SMDS(Z{rq?cJ@fDQQ2 zTxafTPK75f6V{a;^JB|9YovX*qt+Ge3HF5scz7ZjM+_osDc5Mj7;feYRv>s($ivX! zuzul3BczcrQN5yf#60EwiPiFL0;%w_XhIx4eo_4EcxOBo|2uwDyeNKk+*47ruwHPD zKP@(fcQHCE>P)0CVnZ0p*%%VXzQSr3WMb^4SJ9+Esq9E15O1QBku+Q%xb9!#>+DVP zklk7+{WaPhwkTVB>sI(lwm07aYV{Y`RU;X1!RTLXz`zl7QNIH?cMD*DWGcKl7V9_b zPs0`askiF+ux~WWZ~`da0%Lc$GLMZOV>;}o>;`WT!CVQ3(KF`XW|Ae=l3+=-BwE-O zn^^`#ZznKxj(~N`Y}MML9Fv_&SDmNEM+tn#4j_FA6w*0zOX?e14dWyeW%UJqvmqpn zGdyf}_|phwBmpQ{K`|&#&wCQPkS`GYC&&?=7Scp*MH57mM71KjaGQ`N93FSV5z>(qM*N2EK!)M@7!r8vJL+BM zS?rz-%(dl?L-v<8hqcJM0p2TP%?j``yMT6;Zv1Q50&kFD!*BfsxEj;-L-Ygw_c=g6 z4zAA*7*l_Pb3MT@(6Hao2-jwq@g&%!;(;Z009;$i=0T9f{%vN%`>La*zhwY?%>%L+ z7p_t#%OOjgwZZxY8eY}T$F6CfLLVBCVy}^XgdrpmTUFkm@B+}v2$U6@~>d5P$H}qtrndS9Td$Im5aU$TMM@fUh;p&zTh2+ znH(J#^&)a?1RB1T6BqiJJvQ{v5YZ*n8(=ydNh*L$pbB}6|BKBI^!Atg;=NRl z%jI(tois^l21UKRG}57@J_12tzgoa(SmeNE?qHB@O{VSWjBOcCr#^#EpFcli2` zrPeYMZ1j8}P+hcrvL`t&xyHl&k^)ar6dFmSlGT(uw7GENcbp|)uMPPb8WL6mS7lh_ zDDKRtdC{X{TJXYRf5fik7YIHH+6s>f4Z<{03sJs^EV>O><%)nHNad%*hVgXKr=z-Z zEfHJ7Rq}sx_-J~-EZw>*pr-~9SYs8mf8+5b6*4V(s$5+ z6zF>EPU>8`4!}>M8hQdtN(NqlDaK3iJhe7$GW~+Fcc^)n`5xRgIbT#c zu16n;S;p%b%i}llw+k|bH-vej10tg+I*uPliTf>DDAEh7g>wa~`IBN>@+dLqqe{8= zB8tM#!QFD0EebvzRKaWpI%`*IBt=EKMO+CZZx%wqzXwkG7x}t+c^-r7rSq_3xqU9Y zkCs~Yo4=SsOuda43{i$1K*71E8>5Q^63$8OTxe_&wO^rkz8#niOMz#x5j@PVG-eH7 z+ey1Z`v@{%{d8Age{+=njy~G36gYi6Si8N2p2q<21y~^&*~;7-J}Th^EdcLqg^39b zDZTlrMFag&jpK#utH@Q-(U5INP|31DvVMoHx1ZzTL!jXiUgp2X=IEzRuJS8aQU*rj6Rz^LG zG>0dI4GulXHnI8yy=8QuE2+CE9myP$n(zSKj7-M+VbuY?-|hY6IR@j(Kh9`}17>ZH zfF}0ZBnMMt3&T!mGfvStwX1^0spl~_=lz_H$y>x~GMAoGZqDUEajixDE>ERXojt&iT$3*`?Gj1*>zzKKT0sp3Y( zOX7zo{7LAWs88IMl%M=Ld34IJ6n<*U)QVJXN{5um$(@qK3FYx)MPmfTvEu0YTyDg3 z&is&);IN+TJHusjJhE@KO|W*g zw1l0mp~llNwl3HEbsKf1I|Y)R_67Pw1|pmf9x#Lyo7g2BU47EI#XMw^-2?@ZBP9K zwXG$|8Ob5Z!O3CCIms)MSt;jJM5*mlD^sIWzo)cG`6u~V;>-BOB934=?@`pNh$Eco z>;*v*`UdJ?@+jhUv@6mA--)GT9|PloU8n$J{Q^Jk>);#fo#~ke`yEus5a_L=EFor% z@vY&gelwhBMrUEbu~ELWWf4+ zt}2C2Y#msxYk?RuOjo9N>H8Xn82Z4>=Z4`eG|`RvRQ)8~1FalVb|osgB2r9?}BPm za-MJ-pB{MV8|p3d%yPGMA9fG+--gKp_%{R`++ih%|E$@~TQ*ZrXnnq}e{@8|`(5t{v~W<@V~E15!C zDoc?+SCpvF>dMT29W{O$k;!Nq`Zj8z2u&4dzbf2Q)}xBp;#|x4+O92!*Ice#RHiCo z<+HQn(l;k>Nyv^HCoF-vNN@gTK2LB{5HIX0tP;Kv{NlHX^+$(t?}R0Wj1F2&+e12! zc>LcyLtJ|t)9t_E%)8b;9Ne3S-MziIZ+)N%`-Qha5Tp_h$JY7xc-y;AI=)(Y<{O4> zx-FVp;LaPMAi>XXh~hr*9o0%Go~sW7-8mTu!BJX7ds>63Gu6XX*OmV$xk}i61%`u7 zsa8?o-Z-cWHyk%+n>SkaS-ZmOvK&@ZckQj<9Up6zfFoW5bHOIvB2BVtF0?md6+-0# zRZ9)7%QY(;q?V9J97KEs`HD4o`@l!9%r(@$*!)I+7cz)<6vgtV zkTmWf`63CIotE1ammyCcuU?~Rgmy)=u7&oXW;VDUDVhRxd(~~_LuGeWtonnxuZE&+ zqvIQxrYX>o`(}|_d)gN|UPJS7ojuqtwH<@E`yPwj^bcfue(M*(zDS7Xq_SGE4^H3$ zn(=y$>92LH`vE?N_HXEp7-mANj3Wi)$`h@YwikAO-Nn%5Q0Gk@wzZ+uoGx+a@KQyh z0a0y32QodB(WG@mDN#ZiM|nw$4?;qg0qLxeJA!*BvTsB|SW(E3pk`_|QHqW6J#s&C zP5~QMgJYk|;ko8Nj~^pck*gtLK%+0CnJ9Be)#&H|%QM;j({xy8Q@xfaOFM{XHt%d6 zA>JYhNN>sCDuXqt&@10>Aj6#W5j2V$3=8#Dx+PkQcC6;GIz>HJU93sbh5-Y*li`4o zY+ePeg^#u%*opc7oXrFnr@Pw1tpCB3~rYr!al}U zX20g#;jP5AL@I4GlNtPj-6JHNjRwu8B7_LP!ExIJeR}yO@vX*z4a*u7jY6?O+Cmwo zO)>tltg?%ov92TF*=TZ1u$?yN8YA>%ty0BT<|=wZ3*ob>kLJ5}k-pCO%j~oEv0s3b z>3|KjbKpFdVP%{5!W_?QoM^H`+w-2`z44gYV+ptRg?$!}d57f%tOCu(wt55bxmRhP z>)M)Rwxga42$%kqbCzG4cCk=a{rw+7r|hmDy0z?nw(Ic@r&?YrdY`I`ZR8X)f(TSE zX1xllZ`>N~y@M{KZ4UN_7sqBN1X4PrQBwxTH^h$N-VM!Qej}>SV{g)=XJXRZT-sKqL zEx;xr7myYBkHBl+PWK1fRAZ6$w`#4j1)MhhkmV?vR8hKk;{o#&>n+=D`(ZHs^mYhr zHD;+{o_@7%3gjNM)Y-tb+oB6Kb~d{#t>Nc*z}eRQ%WZZgIJ*K>WRJyY+GN_@aW z1iQ~^uY_47*M8O4gJfYkL)&tVvFqYHCLc}SlrU5m50kJ%BnB`(fwmx=F{^FZ^ISbe98ITa4Dbj&Skf(obnqr&aT;NuCyxx7Vvz_bBcHeghZIjHO4ZFa5 zzFaMaz12urwCt>Mu<@QdjTjUZ77oqMwDBbiTKDffr<T*uzR^ANO z77`P=?(gW{Vw-HKvS0RPk{emG!+S=L<5S``CcIB*6iH%o!_G6egERT5Kg-j`HQR0Q zPa;lcl!dj3OW%D>cZp%Cp)aQ=a{xCkOK)W-!bAz>oUh zxV~AA>EsHVIIMB&--~~?|83p4M-rn-H1KWL-2VmsLtB$NlYf%x36t><-+AX_tH?A> zM^gV#Y*l7z-*zx553cW68&l+ zCTSN0w!Gz^ zil&m<0!oIHKR>uVD)(^RjqZ&efp@ci3_-{|5WY5cOdLC5Y5Xryh2UZIJPwim9lz_j z>>A+KdJ_D-kW19Rq4lwG$;}yWv$UCW(w8Pb5#~i4qOS3DG+dM=G_?77^lQl1bKjr+ zStS;#51K-pMBj952(k>Bg|+wU?Ym44Z6dThE2NKrxFA&a(8`Q`Z6jQJy&L@WK$bt< z>vm4Fs`U$1IkFJ(uEs0%5%t3x#y0nrEmiH;?KAbW9d#yo#z8jE?w;u~ILP+-maj&# zVVZs+7zQ5e&lyDEOk4x~ucelNajY(0`^9+0Bc~{UBHLZmkQP=_)8=f~bv>qaKHADu z7FVFi>XdAZ{>$ix-EgzK53wf%J83PI$2u2LAsC%tP8bv?6m;WRxFb0O8JE!UzWbi; z{`1Hxl9t*SWDd^}&Psil)h+vU#+8(`gi8MH@D=okz$;5vjZ(Hy9Mbf);eF#WNwn&R zzQh7FddE%YT-b-LuuDyGx+$vBijB|;yeLbT_feK;pBqnGiye=hQr9u}IQLFxl`X}T zs9UcZB0nS<-dx`}1GsEj@h{m9Wt8Tx&SPw}T(+g!Z`e4tJ=S2W(_ChnZCIs03v>7H zz|@^$oM3_`KkOMbnD3he27#tX*-^d3@&lh7JSVy!u2ot?VT)EfyLRl=rR)Ay#1dJS zAXOWOMs;JZMA-h@Sa0$UW>5C#(ESl>dGq3;6D4tLV<&Pugg*;y&l*X&g5UQ`uxX^V zjOpwdVF7MOfjjAX=Ea;pS+7$`3HSI$E-{2cG5h}5x*E@GTdFP4TivE!pc5O<0YC5% zoJd64#FiDN)_S+Pn{u=K3ec5L%Eu^QsW<7S7{e@WZ9(Ar9N_$8FR-pM^*8Wz1ocSy zF3B?RhX%yO618Nx?2BT!`lI%;{yuQ~4;k;9^l(O%ZkhsqhgZfIu+RJoQeGoWr!2>; zKFb&LXvoBl1;<0U{+%*G-a$Rct|R4!?-LwJQe<^0-`s9#4?<5uhc=a4a}FkpM4h6I zAv!%) zlB?vc6&saT)K|6hbQIlc;2-tXR~R&U3FQ1x{Y;pztBl)>tKdZAhg(=r->%|UhE_N@YFStb+ryYiOJnFm z7RIRJcc;8g4NWyAO^yqTX&*X?t|q>LWNI2hCOAlg={S2|6fJH>@}-oP$rlm{i9-_> z#(DYeqm#oI0d08~V->w0bv`iTX|n4FfpsNO!&gN@f1(vo1iZ@i%7x0Esx0Wi zZU8D+AN7AQJ6)_z_rP26i0K!kWEZ5Ffz`SP zmLDI)yTeJNXCSR1w{*dWBPt3%=v_!{>6bI*W`wowi@P09ku_dIqIjXdS#BHw=7pWN7_+tQkHA} z)9Q6^4H;&MMGA?GU&iN#SKy`)X*Q~C%3xKwDpw_fY{NTMwR(!WrTU|4n5r{$ff46OEwKq2`DM_$A_Bj8TV6PxiI5U2BDm#0APVbBn2`gi^aegyN6fXKa zzz^KV#iVJBb-^t{2ZxiRrTkVRjxZ~BQA{lFS?mq|BL2bH5xhIm_qb~!mUC{ihEv<2 zQGwasRjwsAsp(&1SMWjaHB8lZh4p%~YL?O}3z2#yJ!Bz@_sT!&(XgfY2kfT-%VVp> zDukrVR%5AQ4!De(v}a+JkE{Nwmr6sA4%a@7WO@HL$ZCdig2$4{j{62eg*^XLA8%1qi z#iYDpsfzf5_;HDN($u6oiO-T(XN=03ksXoB5iWt*;tcusSCAv-MX69#VIN3+|4$T1%Ih8q-Jw2osyM)~wx;vtln;!Wm zyd-j5j8(8S?pIt~981(n(2mFBDns8hCz7ei2LEhtOLw|MXk7}fod{Eoeu(;pLL%QK zA0*oe1pX$;6-gv>;qb-L{+6etM08CsGY0B!CEEKUeMgtZUvU? zNEx#UYr3V5cXy<=;v9(HE$W>1DE~)UeZ~IL)A?(%+GL!_NX}Z8-74ox&f{EPUdQ~P z+&Sr!<2BKfIHI60lwL$Fp+9*gZ3d%|Sr(KP>|PCx@2!>9cW%_%+kkeEQ-mHrl10If3U1VzEyEi zwNyLXaM^U+d=}0Qx58=R2-|V6{uY{hfqQ10`Mi;(zoY4*E(9Xu6xk9+9Kw2&P4P1&n%DYMi&}*+ic9W`qsms*;(Dv4Kfxb*RKJud%r@!;Yy`4QOzGWTXxRSoh+*z_jZ6J{n{7e&RYBg;AE z!Gjr9lmbE|J}@xWFZTvqDfWftH3prwO#MVLSl(IwOaFeX=1^jii_Ko^-Tyro=7&CRrleDxWO> zDbvfM6i<}jR4QPSl&i~OeB=OW{|8`1gH>zgPI24j4B0h<(N|CVC$vT65}rN&QTpVZ z^EqR(Tp7Ky-sJWz{9Qb;v_B*_+LTTz^5pi)_?h^euZ`Rt`XG2{@GbV%(6<}~Cxt@} zeZpQ6Jd0%s>dkr=9205??sJmoW~6A>j$L^Oeo z-b&XkU@MI?KQlblb%OW!F>om)=vV3w7zP+6hEw`xolyS)&Q&Y*y>ud7z3!F%i+&R% z()z=W?htjOszg~TuLjz_NO4@zAU`OJkiCLmI~2PVHGdu0#g0_7p)6GeZZNYu+K zl|z)z<+G(n#ABLgHl1y%lrGjDb=<)RQXVl*vFjt&@xBNGLapdSd`;4ol+)=~v;XF& z72Pgel^>e>JByc*meM``5`RDpJDLzPIrf}jfN+i=j8BP8 zaZBtDK80VvBS-5ZkB0j~N3uJ!jxv4pzO)GHI7$Qg3~3r+Kd$jN!G3?8dzLE|%-Zej z!yw1g-p;X|u{?(P(;-tI<6pf_cOO1F>*i`7XeMh)HOJLwRgaZf%8Lq@yrrBf?;+nP z&jTMrscgH8~4j4P8$3uSj?GTCxyNuLD9z(;9Y>0fbE(~ib}n+{61 zt5+FoZBecu-w1pFu^Z(gtt+!%a9ZfIW3-MSL!bqXUmcVLXapc$c7 zC{g8K#T!MAqDp=oSn#i9D`j$NH>pK(Oj0Y+i@%69VwLz9bcM@-Ju8O!@;mVX@f|To zGDxyOvQ_d{5)S?Zfs`zb1CNIad?qQ)Lz}Xjmr1V5i_{NwB2&3F4A|%Iya<+#T8Psr zx%4wZzeD;(EQx*+YZLSk4UC%#36<-KE0eO4waNOFnQ1pNHfDFq6BJx6SXR)!U~0jO z0(XJ2;6_24g1Y=Qd7E;t*9TIBC9tS*i!8BE%*J>967yMH35$ z6}-)>&iR{>ntDI+R@@`OA>M|l=Mf=c=^+)YlZ>sjCzQLSBtkj9%fG;T!)?R+umy@6|%xmo4I0b%vzVUJ6y~bCKj>goc zZcX!>E;VVIB7v?jtoaGF#?J$HRt|LhKR`KmO5?yYvjEsJ{pG#nZ2480KvpiDE7>m5 zNLwhv)k^J5Lz8KWHN#URbfdVsk}eMVs=Fvi&7!aofVY{E<1oGYF~E z6Gy}y5j^3ok4lYjg!0*If+&n_)I#zf!T{uUAjPL}lU#j(f>dPrU_7dSt|e*8;Y8t^ ztc&!QcuI3^)AB}sLiQPm-7_GueoOIEAppkkTjeU{1;rJ41mH$9;LHM59n|#K-!tAa z-?p}LjC7y!UBC)uRn#8s0Q;8wUXIpVKs|X?N4_rY_B|n}fue;=$t6U`6O8IVEvIPkO&pBIU?N z%9>=Y?;8KO@OYd%VP*37)QjmIGPAPR=L!n87dDOl%n5 z(5qp6g8)bgOyH;VX?oMtw)sSJQ!`(@2=4ks7!xN;?m%;Ty7Z=04`cs!nN$`Fww|5x zcXBK2!2Shxz-{?jIR-wK_R=L1o@BVBhxDqflLArQhE>BP{ZnI|WsUuq>zS8=?Lrrm zH`CX!T8EwqU&nPu*T()QD2aQLkd-_kbw&Ed%=6jq+#v zH>Nk9ZtMl4;8hq0^P1(&10)3LROt=IxBN1E_k{`t@T5&j61d++ zsxGPCs8*>6s*^yddZA1J<4RkNSUXp5F^o0ESh8%(9D3J0Z($$>=}tUK(b8jB#UY}w zQxP8_5rFeb1j9uA;|mj$lUt^aOrMfDIJ-30mAA0qeBqg*8^wx}K#SGl-BOtPVjk%*-Az)&E{_kqtTRryspN=1gV;-26J=?a4Io8=8SjgtF#y|Rwy3d-W>JF+XB?e=Bt!$lCF6k(FAZ{zxH6Lv5)f~~R zXnNjsx#=LVwzt7Qo1220tDBcJ|7q?f{wW?Kp~JjrrYudakRMYFRPt3C)gARZO?PdG z?u~9LBp$o|FGtTYjtBeQQd2ol2BNH4ww{oF{pcJ@?6g~HAJcB9 zEl%U5ZA^WWQlI=i=~3d2gf{ViMZ<&*{7$h~VmQ$Yx$=ni;imvSTg~3Us%IuL#?h`& z9Hd<081yjyJHYi1^PX|roShsuZ5h@J<{DEoxNm#uGPE2GOC1aAw4gouvr)}2m z(6xsg-#$YsB(ZYAbed|u1bHjW^3b}*Hr(FT(azb%)d?~S3%#e{y5wSeaTdCkKnC`L zlu|?60_+89P$g?(@DBF#5Km}ASgY{i5ep;hxXYtvM~{s8hsTZm8@r8PDtIsGD7-8* z3YnrX5fw6>`-ClozXW{+_xTKdhuHbNT`?!4uSDJAUWnWku`ql@SQ&>9`j)*ZxR#}3 z?qHPAe^6&rBFLABtqJwWTs#_k?w{jJ^=jQuT?d^r!F4vkHr~3(vJ3hzQlr2)7Iuar z;nWV-)XCkrtSmoZ)=8-NeD6rDOnA{pV?1BPKHk5M1@_0tMx3rIO2Fj zNaW&3U1WdmV=gPIASy2^Jj%#@#ofmp&&}hSBM(PbL_UuwiP#qYBa9W+nzJ(WQHX+V z3HGvLS^b0dG2b#8=$~m1sJAI^$r@4+IPwYzDJU0V;@*JKkN8tyR=(4thepK)m&-ZM zc@MY)YvIIygDu>44;ac6>q3jpybQ9j)#mQzVepx54l@^+mqPk(1+*;|z{g_qXLE^V z5cI1Qa6S=koo>}ym5^3s!WCI!yKjqyWcD7r2O0}o>?->RN42A?!|jN5${jnL<6Qk+ zKU|PG^-l3t`o{aW`vs7LJ&G^Jk0G51rwJX2d1M=T0eL^=DJ`4+h_;6Qh_QvaDkz6l z&iczDva8sMY)*(a=U=alDCv7lV*%t#Jd4=tP zt-vj3P`Y zb|-!4KF~!3C_P!P|mOL3~zpuqx;!^Hk8Tpk~Hw;Ak#{HSJp3U+Q$qNlHBR z6}gUdn^aEvLbydx5z5dn_#xy8qQP>oADAbw-oMjd>BqgnKBjMsr@QC5x5mSB+1x)p zwP4zlxyf$7ZG>Z$`@KVEJ?fb3+6J9;pCiuM)_&8z%`w2-$AM}j19^oFMduSk(ND;~Bo}ERA(@mxwUMuq5Na;%2c;)< zIFKJ+(Js)JFfthn7<-rpneQ3lOb$>S{xH=+n^>!X5%P*Pn8gel4|JL2ATMJ*YYS^G z{F%M1{mgWFF+9_+X*60l#t`}*N-8y*R!^=ZrBPHA8SyVVoM%8yi1m41% zjyv{Q&aSSx@J!7H#_9#fV_<`p+E2OYo+A4l`yuaG_h?(B8~1f|KDB@JcJwZDP~Ek@ zf$j#E$7}J7cZd3ledFCE?=*js_n3zoSm@v8+2s2X806dQy%e|@sPTRF|HRwjhcFrP z6K%qoNCI&L@dmOVO@ut~FoKQLminDEn{=OAL@g#esSD|HN>|!FrV-v)Y-X3Bd9*6} zH|D?e1@v-OBXcKh74sLXiQ#5QS%;YQv~$egK@;e$>D`0IFlDqXMk#YDt%e%Tn8R2> zDJGL?RkXw84?yynN-_}A$RXs_XcU@Hj3=n^Bha9~ho8V6;O&vkSe?JwKNH&(2={e_ zc7efXgO&l?PxjJ1PrSRli`;zATW^AAx$C6orMKFB*A?om^$=Ylo@VcH*Dz;{cd@s~ zHOZX`8KY#^IxpS#!S&8P%vbJlxFUR=eGA-9&nADgx0`#K@1(!c^T5;BpYA{E-Raxx zAMT6t&GAq6Kl3&F!vitCXTE#bw7@3s=)eR->~Hj*gHfLykRrE;k1=JyL_A2C7$_pt zQ$8Wl_%Mo@ybp;X3+dwtE0JK@Qd%pbh3uz4q12Ms&@yR_FQENH zolN_Oae+3B(nMR&I7P{#I2d>7FG!ixQw%Naw6SRms5~IKETF1LZHNX^8F??UfM_5t zBa#U3(cOd@C=0oRb|e(xg?J3%5MJWHi+4a-evQwL+`-DcYXW!ii9U|+DE7hM(W~=w z19v=J-{%0!H^?*5&+=b)2fW>|W!?!cntupZ>DlVq;GY=C_N@2x3$*lSc;w!H0@2Xa z+~xb@uk_9F(E@kjJnW8t6*Squc#j6!1$z0;`UheefkOWezr&CDj|K(==0N&$TVPY5 zcOV&SgP+8*0@?r@{DOb|8}Ng$Zv2K3P&yWYEg%jj$oxdaPkMorV0;pvJQAOUye7XU z(hwqXJGqXq8Lc4yCC?#@AY3PZCHE%Q5ly5@l8RVB_LJ&~`$&Vx|B>F2=;U+6QA82B zi2R!HzNRkAxzAFU>y<`G!6BT?)kF$NddHb6`44Dg0cof1w{_(u@^(K(9wjK)3MXz{!`$AA6CiL?0AoRR1!!JUA zgXgOY-U)f@qX*8TDy+_T7vGG2_Iv!B(9O8Q7mLTC#{xubFd7MKi6Lke`qO6(oF>L0 z!+mG)YXm9Q2jiog@D123^gPlIdxmZ%e!_lZ6p|NR8R&`jA$`D6`~>knp#~oWB(=53 zapVD{l^-HjLNbAZ1kfbnNhARsO*A57@gnH&4#qRl38ZBRf^Q{lC(;8oh?Be%Ne=WT zohNPbx5w)#UCd;3euJqy>g!XNX*c@6SMMh-(4`fhhz!deP6o zJtz;~9q53JK#GABum@G*E6x&-xN@~C9>TM<59^KAA<5VvB!}=0 z`!_HZ-Gj=Z^Ft?eMXKQ6upw>m&-f2yI^G56pjVL@csJxcT#-@u3M3QRfLrilBpi|B z>0KijYpmYUf~2nYh)kBLgqmu48u<2 zkC4H52KF2oN{GO}2D+n_=p`U=6cEgq#oq!QOz4W;#dZ@WqGXJTe@5%j4$uk;L+&Bj zFgF~BQqT;{fl&wp&^Y`Vyq8|%EX0Q##JSiRd=1(exr^!Wr|_g0u!-mgM>pBVQBBOh$VXcvtzj;v)piZ? z8*QV)=#fYW&8PPue#kJo8`_(3mbQ{z&_I?Oa?-Yzj$kdPe-So%BMU+9+7}@P^c6&d z0cbMHKsq5SAtpqjxs0<^CYb_bc?&tz{t#4sQFI|4f<_>*^m^nPa+qF-v?0%_Mbv9h ze{G@8L5n=0XHj{us`jRbKqN}1SJ7qYUTAx7##3~GJ)87kE=3FNzQ{=SY5EPJK>xDB zZA+|hW@Rq~h1yxpFqE|DkXf8KdzICfwUkwCOD18*fLx*OGhQQB+h+O-V=&cBJ!j=I z)b>|sJ^Lc5B?d8rSkX47{Vr=YBZ`=8SHcOn#a2i(tF5bXf^W#6_F-#Ef9nf7E0+ z=`JVd62p)Bw{x!)r3qJ{5m~_c3?1PO@Jzak1Nm8SE2O z8a@kM=p2iVGM_~K*fFFt`qU}Rb(Z59PAbRD&+;1JQO!Nc`3Y*AHSC?7z1~~>wz-b> z=rDVIrS8A7W@z_Uvx$LG!j(9Rpf8XW2$Yipm>XCmk<3DE+DOR;?2+6urR?Yqm+}w>^@k zt9~k;h}Vm)a;s7Ubzi+TJp7@y8h0g{%r3f>#sSD^MhnG4J2?b9hdE#6eRcfcneTVi zJ<$DjfJg8`zxoh;+=-aBkb_}q(dNi;k-uW@#qEi^4}_k6(Kn*TM(zz?9_bzv7|}0q zX~5OsmBCWT)BW?-doA@G;oix0oAXG)8prF7(T>~MEQX4;g8Luii+v%vl`OGNw@k1} zEkkje;k&hmxm^2Cbqi|2b5*|JHN9NZjv)$fxJQkYZfs{uM=1m31)?NTH|b$1UmPde z{=a%6VMr_1-XLif?Py!lUM#xWHbPhfKCAQFH;OC7Pug#^eQh1vnjk(RThTthd3{^7 zeoMa4PZTX))YhlvXiHRES^Mrbq3}PUhcHe! z4*vBPMz;49TSYsBCz=aeMhQO%{|G}wq}beE*D7mnXc;CHw2f))(7dk|sIO3SzE?g@ zDwR8F2J38^V*`QP|uFr^v3+x8l#H)TR$e+m`w~ z^HagFVuymn+~;}o3r82u%x}y)STLt3ws2I=g7oRBscCiT#?=0a{bP7hZ4qlj9|t_} zec@B*eaOAD(^-e%oFA-8^oqTj;J^*yDC0ZrZq+BO6f2i^6W58#B=ORNq6w`!U=<&P z-1EHV2mdxV-DvsR8qs>NS<~d!($+d1R6RAqr)?pkO`=xlUv~Qmu|awQYLuLSUHD2m z3u@Ga(tLTM@~VP@JO191aQQ8`XI%_R&_&|ua$2!oHc7Op?YJmZI#6;_Ov`_1pW_E~ zL26f3f9(>}IGd1cfR%*Jo9&pu-y`_q66#g!FASU;oD;q#W_!YsBr5rJ>XeMp*)h2{ zbGzqri+7jhm7gj}F1(RX6>KV+QJ9sd$+l(Z<$TN>kTNY!5WO(!W27zgqrbcNRZkbs zD%Tc=`{-lp59LXYH{SwxC`_x+h!p)qCtG8LciJSvq?U{(q-jP|+rNK*0vk^>s{Sr$ z%4nMRFRCf6<$3F_*0z=>t>NuF@dEL6F;8liN#*mQI{J$m9&PHPS*HqzTGFSA2cj;JQ9dYK)ztSF{-g4@`Jbuj=^vk8ZyF9ZKKT3N&z#22 ze?PV4wD)U&D7+?oDS9o-P}~K#peV&#b&;N-cLh?oPBX(0VA_s9*WS|IGj@l(&oupO z?H7He(bwpN?}2;N0Xh%eV<1?314YO;(8hg+cv+@+3AB-7`3`xSv`hPw)|>}g)NU;2DbDS zZkDO_1FaiOhgHe4P?axng)`kL#I?@jo6i=1onLpq{Xs)xWN9V29{Hd0n+uCeewO?$ zzFaIXU0rdi`f;tjqouY}#i0C*wDRPsX?HVEq+lW(_+d1k!n$1)(jOZD3Fr$!}$fVFyKnOmHJb}}vfZPH1RTnI& z7Lf@tR)dS&C+#9lyjq2wROTowvVpP+*=K2g$y8By(EB8cSfY(>JzF!I-J0r~FSIMM zk*4!RS2L=eh<(>Bpic4qJR^LQ10DtYhP@7r2z?e&mn6&mP}*4jv8=94Ri-U_SGuy4 z(;>N9T)ViottzX7rl?h-Xr|!adigYF^VxldGOd)#{S*AOo4RQb~8 z*0cX^{29_H{MF;vhu^7x+yB0A{M~>wIQ-W9IrDE@)3uf-?ak7?pv4Q4DdY}nD^yL) zGW;;UwcN99vDevBh#VVE79i{4l)IbyK@Fw#RCjWn?J0rSDs2Y{t%YIUX?zdagEsv@ zUB31iaDXSMO~4Vzg__R!icq-{Sn2a+=VW`qi^?CE@mD3|BwW$iR-#!V+^0BU>V!;U zq?5NzStcQA=th!SvyjrFnwfR)JC#+RD6+O#{LIp&WdEV_UYhJZrt)ZkD=W*VVT)tw7$oqx*}u8oQd$SunA8CeMBZ~#xx3NXWDb3h zv(a_4|A@%2WNFrh+^DRGl)A*aRBk?6>E8Khx7MyBI(4mxEn?>73mAtc9ZipwSEJl7=GMS>q}U+TIkT)RWD6zV;`fRAyWR4kSYkN+G0yS2gaYvG>> zO?j=;+LpEtZI^&f#b0z-d|9eclxm_NPh`b~`qB6(V}Uuu!m(mR0Lh^`lTP*u+jqFD z_aK^xK)83+StpwJn^MhhOgV<}xxg#E5=eD&{AYp?co zqJF?F4w2rJTm{|h3)v0Te7%>kr*VmaVVGsOZ>%=&v~0ItB6954NF&+FzT5iG+}+|! zL;@Rpj>QG?xjCj*aGTNt4gRrq6u3zC*4|PNRN1jPz<*ezx{B?>wyR#L?P?dbvx=`m z)osB0OI1Gv*W4!6Vc-IH!lonp7}PpHNloNCu361`K%{NeZn_;KM?HX zyktq)?pz1X9>yyYvHaCOm&LW|Tc);MkTB#H>2lFjs0GaVTl3ey=}XJ{wglM8xF`w~ zp0cCjR`CaUg61jczL($=4KBcdziPZ{*kK%GNwST!2Lokg6HpP#fm$oDEVblYD$HF? z2aTr<2H>LZ)r`{o)OcvGX_}$_WfxG7rvS~23G}Z=su^m%8UfbR7$92>)&xM#_N*pW zlL)`zr{Sn4U_BLAfO!xjd!U_3_H;Nb5b!%V{NP^WtrMiUdHFAk*_d^=IJi5VC>D9JBm#$(v zRKCD3-->Mm7WPh+2`k4|VK-Dy)Hu|6J8G1g6WY7lRoYA1G9bJ?()9sPR;}if8hnkh z{)$Q|LmVRAq_Np7oNsux(IG;HtMUh;QLFe@@ z^{mXU@v41U8B|oA);(qjIMI%Z+n&%do*BI^WQxyK=jWVT$S-@E?V)Wo8A-X>$_<~f z?%m3#pW)(u%Fd5!Ee09%};P4Y0P@B604v17`q*k3hAcOA$`b9EM|h&XI~ zjvv$N)I+q-aLO2CoUi|+E!XAi_v)%OA*yTG2Ng>bufbHGl_bQw8w!c?lk%pbpW>;q zL3LXFMb(0JfEkgfm1ukDuIc{jWWWaOgUj%GJQ!R$zU!iZ{vNN{iK*p%rMQ?YbJskw zPGGSGBQAC?VeS@b2Ee|pTCI#gC$~{gxZDr3V33IS%f$5EjW!eQ?4=Gf*OoNJ*m-4T` zFY!{W#d6f{>Uz+=Ptk7Dzto=x59f2b3cUnqVykrD^%!o!>+mi5hq?*ck5FsAPbE`v z)Rf9o4a7^;2jw&%c-Z9(#SwXxe1SXy)_`N+u8|7Ljhjk_%3Zw+JVPQhnc7{tdHMtT zBl>)NuwjsKzcJo;%COb&8oUHj4IcPg*mKjG4}dhTl%ro_cHJK zKqOe>Rp|Z9e|dyoN^^c_MNw^b$D*1u6$Pd13Qp#jv&wTm=PxUMU*cKxH@iM%U#xG$ zyPzf?tJ`bH5qIN<@iwve^n2p8<(N6tv>yMctp*;JyEaZ)V z9b%0;VEy;F3M9tCH%i_%2(Q&Rxam)Gw0%aV*v1<`e(-Th9u4|H0{ z>%eh|Z|k8Z7QwJryUdb5Dk?}wZ5dxmYsi>9yUwcyzh zZ1Fc=HijBL>q*@Tol1*nqk$ddsCla1sA^N5Q{*UYKs?zBjF0}HaRZlGc`bP5spNXO zqhdaINX}K>R(8P3VCNVIKH+P%+aSht$M=CIW40m9c*1zexEvfthk`DJ32RXUuGYT> zb@~g9L_JZ}1nWf+)Da}L8zql`2CPT?`29R}0Tp4q$P1BcBZozGj_r~3HA9nsxh%5! zea8iL-gP5tXH|4B{+T;EYh%XDG*+@IZgF&bge0_AV1+m4GQzQm?aX*c9fQpB6k{RY zN8eHJrBBl(YLm1(b)$el@*LOczw5W)YhbUu*oc~#ke&W*{X|Fz7O~XQY8q~o6gRHX6^>;z_G8f*;^ zD^{zcU>CMl8w>IFCA^cXp%bWcIL5)oFynYbBM?~L<2~^z;D(lJ4{F|mulHx=e)%oQ zwDvIJ6k(|Np0dPXCl5Mo@mL!qiu@fL6ki#CHQt!uogAOGB+IuTqwHeU+>VW%Hgxo? zo?5=L=u+yQUAAr2zKQeA(2j(&#DPxlUJ z@$c}Z;LhBnC-J$)IMY$%9K&hgo;bkXgoh8)2{pNzYWSCUt zXM_Hr6In=JwWrwG_Aqlrz_Gb;>#}-gJL#gTtacgj)3Q$ zhn;N>6w?)%Lps{K+Wvwf?2YxZCCR+fsE4@JO}ALPQ7hHWGt4y;)(*#mwew~6JJ@5Yn*37R%+t+E2>d#99#p`z$6rdQoluT-y5O~M{1 z2PkhSCd&UwUrM%sr^qL8(-6zf04JvdHcDkwSs-S9R&|8E`j>5;M7a1Ngi{Mn7#ftpdOk&1H?-}VEof8Z{Z5|t|54E^bP!C1d%bCjtJ+~(gI^`;J%%hoZrY2--yD&m0dgPb8juR!wA zJ7^nv1@%ICkO#Ne7FepI#}EMC1p%WGpF838=LVv>qoA(V0&>yX?ovD#c-CF?Jp6u8LBN)SZAG?xRiDUDtOtJT;0<&&;bW-K}oKW@00O z65-ZD%RqAn(-A{I{Wi@6tU;y}WeM*#PivO8#!KAP{Vn5JA#UD5i7|hY>5S9aNX{Wx zEtF}K)AG}!G9}pp@D%Vbi7Vxo9xHxbFe2xCT1-OM$n2nZURBQF-0#Q(o3BL8S_#L0`5sOEOu~>X|@y)cWhyhJ3dM8MRL%QXc&4KnGKoXL+D{f5OWj55f#uk z?T4*vjGwhbuo(G5sUB{p_24O6E?=nJuac-UHCi=z0s#wE30!zYULzd zB59TWmG4yE#`*(~aH2XxW7YhR1S`?r)0N>-#!sdX=3L8N%S)@9?VD|jZ7;FMss#t8 z0&}u)kUl~40B9$EqUEh=&GF5N!jaOc+Ksk%+%iw!(6RAl>5e%+a;M}nbCzX|&$MLB z%8bp%a>ENH#Y;=?l|Cv&9<{G_nVKk4ijI+#=|_h=VA+77CRtqa*i!O6&yOJ=Jn5EWH)BXvX!~> z3ee(rCA~^dmRO4t3mUQ&sfM_#;j04bJf8>x*tt}@rGwEM_tr;%Ci$k}xM{3qzm>9H zvv#shxArEM*=F1G$!?T|z65IR1V$lx1!+QtqsP!Os2Z7zw9&_?8hg7%2?xyoROb|X zWhv5mlI@Z?Qo9T&d|0UJ9PFtFse8bw`UD2fr;1^)^SUK-au=v5cn%IU z-oTdYrTd^O)nC?U;fNv7*vmA~9ARm-e6hYE2HI+EFNqEWZe0Wv#O0>phLQR{+CwV2 z{F2zMt!rzsFi`wN**xfLjGX(DT;6TqUV9XPao|<-9P;mgVhvcT}p_Er^ISv zII)+QYt!4;0bTARasr(P-UOc*Li7*P83_d?%xQ{8X4vAbCr!ig%NlUom1jtgiqAu> ztx{wW7f43}@u@`7tjK{~#~fh1W<#~{3dJPFX2m-NUpYycf{j(d?Srg=H z{4!t$cR0ZgGwuRnl@FZgFMtPGq;;*;Xth}TSZge~<^iTf#!|y_{RAzdj#3IBhHer$ zO8O{9>KcenoHCDFp%qC7a+-=CmY?l#uzYoCW6_`dsN4(Lyqrh5rl;y~BV_rwYK*cl8o<~^CuZ@TBp*pL2F_xytlC6}4ibF*^+o!f~YX8*k zD(VTrsHkUp5km<}0b;QRK_@YWD!*rMO66~I2UK)W3`S~er= z9Zz|Gi0GF3CVzAJhU%xawY3|nMwe4XCHbpzK4mepWSN@GQJJ68T~fOy9*EI|O%2%P z+2GWZ%S9b+7mYo2mFh*XTU?9{S9j2DH9R&?BYJ^T-~`xP>Fg)%>+CW+o038Gi#IEa zy_H?hUI+@*L&!t&Byr2^XMC+csa>p2#(siNQjVluw7xy7EkpRJl_6X%H#VI_k6S6Rs!Nc#G;;YdRcGTb!y|*%5MxvuA`N}?Ls9W#YW$nsK*Y z1{9m4ibm-S@sRca;pOJQrkbWB&0AYmwOnYKEBq>QmwlH{QVdcKRhKWSg;2f5!pKBCa%c=j+ z<;+>^Hg2Nhc>XE=8~$`bwo{T5Q;_1A;^4s9%xp(`Q7n4{ICz~jp2BzPdTFHCNQF>3 zKuiGC@oI~0MD8W-f-bY(vWL(H}*8##-G3` z&8VBJ-mUPE?UA<20__l|;>>@D<9>p{afZN1uG6Q?&BORVnp3Yx(j$XV#{o41Pp z#fft1=O%Ic<0f`p=8W(cb9b?}qo>Ge*4?I4#uV^s8l!!o+Ku^vi&mTTyr@f?gRrP| zQ48H1+~U+q2z8=WGKq4%`lR-f{+c1uw8YeEY&4khskjfmM!#LR8>sn%q0VZtwm~yO zb44>6==#@z05x6512>Kx`Z@SQ;083}lfmmU1gb@zf{)z`oNtsGe&}y%-GE?Upc*WD z(;m|H5He0y^*kcgu~$G-{KuSS<-KbZs87_m5DQS|LN=#11+4y_iBp87%npU+?{a!r_+)C!F%QOm&3_KWmKn2=f zs6LCt!|?&Qk0A+8>5EN(s0U9Jli>x-pH+AQFiL-eLZ@DxrZh`lwS~1dwgyQ?Xqt(` z+=bqaF@bsaDm!($-sw?Cd#$$GxkGN@;jF^c=?O&i$B3Qb3BZSYAN(PxEckQ~?c2pw z&gp_wk%jgrHlb}0*%fhQ-Q?bI+%5=qY8I3`ZF3p#vC{jL_jiwZu7tC|g>c%>`^GqB zcd!AvqQ$WX3VWP-24dF4TQo-9}v2JT|I_V2>3ZCT=lvi*vGz;QaOsnTyS z$c)~mY~yQun?69_sJpJ+uD$|Zjt{Wis@p0KcoTiav?@}4LW4s)eA8y=58y@Mgc1#v zM8U?r;N8+6Drkn|8Y^4JQIIK4HIMsS`$|l zFe=MyD?3VSf7g7dBFouD%d75JE)CEUqSeHa4S z>hI`ev=JT4n&hy6Px0&ceFUg0<}n8<^On26aoyyy+&K}dTQ76D%u4Dv(ci)}uF^IF zce)EO@7G{Mm7f)l6mR9brHjSmMVj^v?K$nvki&KmkC*x@TCqyaV{MaeBVKAe0wmz^ z`rg`9xDlGB>7yPCH^$fC{-y-nNyY;S>^bN}x`HooZ|xB6Y&dyHbbas?!*aY@U#{;7 zQT4Wds*VN^m@usmWIM{R4xk|L2X$3o%eCf0;WcT6z6ZTYu+XnAW@Xm7($|$iRSzp0 zsy0>MsE`!>%L+>gh@TK$9r-ZgZ^WXA)!}D=d#(r$_8;s~3EAru~#ASw!7xRrX|Mx_%*Eoi03-Y z4%xXq%CRzrBv-scd{4YaG`P*$x>Ja?f0iW4kHWgPM-`+UpkDbdm^tyCPsFTm>pgbwj%hP-(*jVE8_A}VCCK7Ugxamq;Pt%C$jf* z*E{A4J_y!2t#)>VEa@|+UQREaPB@+6JMe~ZHS9oU6EzXE2}59i=M1r;N@G;rgKU)u zo2}R?)rpsg+2R>vGF~VoX!UwZac&Yb(oZ-qducj;yd3N9D>> zN5;oS_lnX*DI&**Ukeq4`i7VSyZP<&yy*Je`Lk1BfdPm&Bbf1wYfLsLhr5U?=CXJ{ z9Xkn91@7Rvp>$l!cM-_>v5o@n2WAkOMz@kI`z^Tb>w`blJLzX>->aS}T;-wiNbncR z6VGeQYF*#5uJwkn0BZdAwP%T)WLuO7IR7qGA5dS=oYi^bA^K#^PwWmBq|OEh(_)og zIUd~M2WX|>zp_*_5Lk(?^?XAPzD38>X>~L3bVCRp1gy2yy0Lmpw@mv4Zg!ujW~oQ3 zk`!eUkM_{EJEAEHoi5teg|o)}YjF4Y8yO4onWb5kO*LC zmKiOpfq=2T6=@fW4@x@#=jyz2gZhQ`A-Ef6=(ym^6Qa8bx~UTyRKoygD-E2)uYk+j zJnauqM=b{*)jv8u9*bv!AJ$3zKgb33((}L#5!cVvKLYQB?LeD4sy>H}P>ceV(_>ME ztVSDQi)0UU+Uo5RzB@^g{i5hndC#iG8vk0~s%xbM`6U^yNiXB77rvE=K{}vQa=OS#on*~r_lDZrm#puM(`ZjrhwK6G zLltq->S)cjc$f+B=~;>kb>7;p>OI(YMZC;iVilbbjTa$s0=?TV7qvY=S-INc}^7HvSEA zK7X~7wLakT^-)y@o=1GuXT?6?hzGWt+OCK$$^12)%oshwVXWJdfIm@nDKm2Fi&4mNqJRV#0ygD^dOt>ad&P!y<#D3L;m9-wRz2wAG*9cih~ahVV%C zD26NjpB=o}t=`rJ1k_wo{gEx`ZSen$pWu+`kR)KzV;KK1yh)>o2BL) z^G2ftx9a!n-|6;io?y+2e8oCBOV(fV1c;Geq{C$UWCwtXcn2E^YKJ!}FLi`^6x?-} zfDg+~|%thGQ^ou@etqt2@A zsoV#y+DoKWqM5>aVWG%fZc^778*C$)cLaz}ODGl>o$iwtTP!R!mGVl~6?V$E z6TNH&P)|6B{)M(NN5Fi@V7!2_Af}&@a+{F&kI-9fmI%lNPO$`7I+>rDdc$q=fB0Bk zlLiips!%LU(N``4=g-LsCva8i0V`yc>Mzz0n*iPy{jt5ur;0=c3Ar$?B0_OoF-jSU zF~IljsOGA+19+ml>kUBn-i*(LHKL>LzV4)My7sWT21Y?&l?5ENEwaUuej*NVh5m|y zWe>3t`nwhdo$e6fmg1ii@iO6F`nsIe`HF((0-u7Ad|~di9BJ0BjLxb1lbn(wlhaeY zQnn@aNmvnQjwy)}g(e2lKHogpE_Z>i7>PKLuR!CaGkaM!SQc5PL8bS5+jBCC>P0r% zCz0XQS(37CC5{uPZC`BXh^tlyf^A!8tFVnF23dDlew()ffqOcx(Ya|?s6RsAjs>0I zNaZS}99%CDc=9JoU+lJ0s#u^H2v5IO?k4{z^OygV_ky2Jnhe8xYc!hanCejx z5Ed~e{!vQ54DaltIe)I^9>`7VmnK#mxr_M-;O!ZEyN_&=?lX5e$EZ#BJ5cxB7 zO(54-;{L)ZiT9560wIBu$G2U!K7qU9BXDbZhsd+l+pgH6>>2hSwxPB_Te@wIZK{o7 zdqOzaw%IJUFuSAuvMtm$8r;s-gI9WuX{X_j{-Ca1TcI7GNmX|PJc6zG zbbk7yv@vNr(iWu8$!N%!mN6;aHEm~#DCtDPmDrM~{b8aYliyvh&2E%aH2=24K#qxd z3DweSs+!tH{#mBlTb&48A7x{i!36NQTf^YCs=@oE| zG0AFxxSOQ-peRtbC|6_m!4I#m>aywxxW*)_J=Dj**X*n+1Wq9+sO$D94#-1f0?A8J zAF)!hU3N=xQ#D8@Gmf`8pnW;0!0hJfWApDE93M6;VpP<+nBH+4;+H36CoWCoCrwLs zPi3T4!k0aDBzR9d~}TBZQ&MjI)Fd& zGUhs{u{(@BqeJMkR2iiquabjE%)ZIq*)FhufhSyg>Szwl%hMUe9U&8%i zHa-hhxp>`5Z6heb#zG$S7M#)3lp_?63XF*Fmg_?yw((%ATiIYbI zwRNLBO>tPU9CS5_aKmi?ubJD5QHs%uZE)lL7xWe!`EO8PES6r9td~rb^5sIscPw3# zq(5QoZS^95A(vSz9hCf?&JSG0?s;A-e75^_3%C<#391M=6eSbV>Ba% z9V3sv5S<=9DQZq+zX;Fp7op2Ts)HSZ8Un8Sukfq%b@4HHSv-9_Q#|6`gWcj>%Ut?6 zk9V3Qn8RQ0c!GD^VGnl%RM6*whg&4Wf?TJUP&3Ghpt6kvw=%B9Xwn)n!yo*EUikle zr6lk)Sp+p68Q5`fDVwd($ydv>jlhB8K33tq$%ln0eTKoerBdul#ud+YW1J>yC9F6%;DHrkORA^M#wno9}p+j|n`S^IXQc?sPj2EaoMi<3VR+^}gg&=ljw(#BY?}8o$l( zwZU(WUoXFSKdbK}-+8{tzKuR}eH?wZc?Wx+@CpY03)gd;$2s>uZltT5Yp6?>bB)t* z!D?`Cd&>LefOBb%4)`xP6Rxrt&QJ%NPG?cMptJ30>qjhrEIVe-G_Nr=f;T{a!y#Ov zPt|YKp>S%9)V|fs0m5{XQ@-A({sUK!9#wdBipeL?}kH$ z!xOF(w}5kjUC2Jo3S!-0hBAl1uCoDYq&=Y;`F{%BYj%J8SeuMEMr4EIu)p<)#o4mQ zTxs0< z%z6|rfS>e)oz_0XXyb8kQ(b5h!=1OQ<%C6InQfgw1lpF^SoZC92l5uV4JuhT&|{GG zXivsx#%yL3YcA^pYc;!oGn@0Avw|DzFwQ~j@Pt?AINkA)qZ^;k59MFsX9{WsMS^@m zu3)}EA!ra33k>`?L4_bxfC^soyYe0QiTpzT8^?G@8Lx%6pBKry<522wiCfLJaK>+uzt?Y+s4punwgW zR_jLVK*_UVigf>yJyR{y&(G8GJ)^$gj`CsL3Qv< zWIHk$iACMuGj?S-F*h^UGQTocumtQe>|g9boPQiAF2>2_COBMonC39sfz1o%-FNut zP|6#?OXlVC7V|Fgj`3#mdhv32AmQTq@!mPia~R+d?C^`*$i2!{a1uEN_H*`P_Ad4Y zb}MTLYYHoYHG#w76caNEgqUJ|WX&NQh-=n)R%dIYCBPbF{bXsg1X?MJ zC#+@ttS3P2u>$Y^6sWbS8-W7-t~I z!0Euf&zZ=1&k=D}b5?Qgan^8#a!zq>a_WIE(vy?TdCab3yReO{SFHK0zN|dfBk-Qu z&OFOxFc&g*F_tqT8Ga0RSdE;(uYCv-fSBlcbSnLp&I3Q4T~v1}mvW)hZMx#y0AGAMI54WR_81ET(AsPp=UNhe@Jy<81`OGs+lKF!f%6iPY z!m4Ba$9e&bkT8gxf0;L#-svC=TsKg;E3QSlal% zoj&!Qj3UpIt>hGP4{0LjkWV2W*$gi656HpLdI4uUhxt{z^?j%={7f5i6f%iQ{&7^k$K_P%1 zNPnPn=nUFQYiN?5fzE{f^k8&j%!G6JdGswh49#Zf8EoLIoMyx@&p~c*2{=ed7+08) ztf|bW@Z^h`tC*dc#ms1Ma{0#?$=t~dVg6>=p>>xtQ08prO`xZzF)lIsK^^3WurZS@eqp~r`Awo>2}JKl2MnSzP^h7O24D} zkdTL^MgS8go%%uEhwo$PFTle1NQud6@(yVs_riXtH}#ZyLoKEpDLGk2orLd?z-Y{; zrck@7wNyW#F0G(~=v?{_^_Y4<9inh5hh9$AP{-+N#EHH|G2#6^>0waqUxn<1_botj z5CjMzaBBygs{_bqBpclT_Zcq8FJvL|iM~d^rhm{XXv08cFY*>Si%1Y>v_G<$UJkuI zm)=XuX*KPITt)224CDuWmEK49q1Vv<2nYBRHSqsi=`4B_eU1iyYx*6$b|G+BfRg~6 zi#8++5g=daZ&W6Yg0AZoFcS8F+A9Hc8%bygG7tHRT%_Y*Ohp6Pq#XH#OhN`AACa-h zT$qdX^mzIr{Dzr!2ExZb_=_FraC#bDf^0-QK<99WJ_|p8L$9NwfE_ZNz70f;+d%KQ zigZK#5jDMwenBrndII@i7}6Q_gAvmkIfE3y6Zb^tA-(AaWo z=<0&>LN?F_dJ@7zC^`$dL$3h--5_|~by`bzr}Kblvjd4jIH(B}C>pArno6I9*4s^Q zr*2ZsbP#eE#^o1UMt=grfFG_INHP7Ho&}LB2?;|^ z(miMq9Sg*f-Bc*;j;umvLJQocCjniArf(zrAil){TVfaV^cZA6@MUOvF&cs1Mru$; zl#l4?CYq+a=?r8f5)6)F?Vu#7hVReQ6QFOl&=aWD)HwP9&4%^k8gdY6h9~m@uFFrF zfDs-;vuGt%OFx6Za*{qqd&6&w=|4a@8UQVMk)DQlBMI~(dMR=Q2}e+PvKDx)7`z!K z0wqQR>jA-7@lI76?!qlNI(jqo)GBC-SYXe+W3VIaTgTzIM&XvrV&vsd(F#1(O* zBj7#x$Q0xs%)?X|lLn*!daMpU<0|;1Q;{L`I_fmlpZ-VxgqiRNT?@O&*))o@AgMrz z`3^1A8~7B55izv;3TXL@bO}8J-nBdO9ctNd>VH%jXS$hw1<#^|@iQH%hZXc4ZHARA z6zN1W=t$_rRJb7>iGDyf11DqxJW&>KUB=SmkX+PG2f?Q*N1CYDR66|TCYUFSkomA@ zI||S6fIbfI*%j#u<7pU-k})t-aTrUD(9+J(TgxEwyQBADCU=6T-3~KM2ludUKnmf( z-sUfT0A}n8WD~UM8rn#wBj2F!@4zRxNuQyg(3fF^TmXK{DB!?chSuMUTtQZ#Utn+a z5b2Hdq&AQbVU(|-V(AqS5xi*vS&JGd8}&b4NiWI=>4`3Z`9?sS9)RAw4DlinqMj4d zon8u2R|LAWRWLSwQ#XKl_#Em#*CWZuU>MO)=vw%sgXjies4Rimu$@OJc8=+N_kvw`2)fZaUPE&Lu@)t&XBpQltL6i`cxIklB z0dB?V)K_@NW6=yzlUat~%u8mt6;=@@zw5E#EuY;eHwNBJ=3@(~i^>nLd7`LI?! zK(E7$1SSjkhR=o(^aXBp6X=Zyj8%F7Dqu9w%c#wC1acWTLMRYzf?(VYgU?kCU&BFR z=0sbm@$^l)5cvsD#6UX0C^N&m+=I%Y)u2+`3Nd3F@P76IvnUGI$?LEm-VF2M4m`0D zo@XHs~|{!sK?BosZ3gdlHW z6u*VP(v2QW&8AuCYKXRrASz^m0yza%iGEZr^zR91T{Da(3H={1e7?{+swe#(M$mMK zt#{}(2n%NXWEe&FkyF65G6I`tEzH>obUN^3#z2ox1|74Su7lp(0=?NES}X$AnadED ztiVqC13z~GlG7xZ>knbAybk+NEj1cemVpqxdqR6&rAHweSXp<%xXnfElnB^Hw~*aH zKzfMmLMGD~#7By%pnoGwaA*!hxj;GFLOUX(=qyah4e)iAuphh zTj*#Qlh+|a`NCLVh$!<_a zGJ@h#TgbiCeBgm?2I5f&(huo_3`RGhqhMF#4YOYXy8*ED1=aKfakG_n$7Osv#00#ygz^ZuHQ8m$OT*2vu4ejwVw65?;CnxHgNWf(TT`zaJy;*zCSx4 zC+k0$;oswn;0%m#evO6ayj7?}Y={C)hxaE!`=hPECo31Ngb{xV%zwqanv3Kk;3<2H z4~G%YhN!WvSYNa|(jEM>4ueOhh7)nK`C3@fp>q9!6ua ziMUi$2JdAedI3RU%ov2;L{30Y+{~xI+KPpB@lU~nJVCF+b96C0>wfSn_^xnYHv^Z< z*Wf{R9De^G51E~BS0d${M|t@Rdbi6`KkNG6FCC1PjcPFn=8YmfF9mV%eub94^0 z)fMD5e+3*$r=h)IRPuuR*i2!R(3=x8M_`X4ogc<^fcf@LZVyiYT|GvK=MRF1++(OL zHD&KGcj&?F1ek#rakKb2@QkiNq~Hg)n%~B|If2!}N^KOgfnLv!fIim^)-`YVIdHcZ z!js=5G=(|(9B@`@3MMLr-T?+3n#CiDS19M&W) z(ZS$pih;*uC9K-k!M%y{2lxx5#?GbVw?t%p20oR*7%Xn!i-49$@`%@RG5kPNF zrf-1TrH9_koM6mUbMl3Eq4xxFgt+G&NYY-6q{&lME_I9qEjv|B-Xo)_WmF!eruI>x z^e^fbl?qjX)^s!Y@S!v5mvkYsm<&^=dRF!b96|fYmdL(JqxX0bFV`RIinv6jkz`^3O86 zbc3Wo{9JTZG(qISm*Bs#PFNVm0}HdN=J|!G9nDb_7oRF98_zXQM4Y%wx3H~N^OO|0>@aXz?jN0~aCzgqsb-3Au9t#>;ugER(z@fBGI)dio^ zewsi{FdtGH)*&J$a$ICVq&CtU@h)OQgfzk&?jJEI;&jBPh?fiK-Rcyerk%DDp) z=pV2t>_ez3^no09t9LKa%fq^6Iq%td>r}|odt+Q^NYMw@x7S^->rhu(JG9nYa}ClK z=7Y~f8SKT6uO3{TTK%(XbQM*3uks>%ysxaQrXOkPrLbwci=&j}gA$d(*>#5P##6hM&XkAI2NH& z&uaba4%GFl|6sgh8|)d%h_M&)<9_Oh#*Ml(@89xh*5bS;h4(wX=yJJ>t5d6@SM68k zW@bgTeBbN#Dah(RA?D2K*PeAn0PyhoJ32odS3GJ=CmI zY?QpjzVRz*KccyFGB6HL%MJzgVA}mmAONpVeQ7-Lh@Mq(GX?))A-pKYZ_(R09uHK zra;pP(-GK1ykap}E?SlL5soP51gM2yaCLRZxd*wnJ9$SFVA)SuE}Ke$2iyT(Y2V7l z;IRwd*ANF4wb*ahqln<>(Q47!`ULfz-Mf8{N1Zi= zIXMf{)05QkbE4)4*J&Rprigc=TfjqgG^|hJP!0Y`TqRqhoU5+VXtdw7ceImyGS%x9 zE2UylFcQKpA|c7ZIUTYy--EwDZoOgUZA+Y$9tqWziQ{?-FVS7N6K{k^VeNpwecc>t^aob$I>Q`l0%RhPg1_RT}%6tfp*pL)axuHJ>s| zEG;eTEz_)@Y#ELmr^4mq-r{NRT~E5n+u-YPhI~QF$vEP?YmLKe8){XU1--s5zox!& zU-|K07k)~A&G@~gVqx`d-C}cJ*ERYUW>gLgYTlqrVrt7)ZC|v%)pG}_t}XqO=<4cjUu~@beOD7uT3@v*UGF@X$TdtNKMo1PI^m(Bp`y?D z3~V8i!k=NjkoP=ConLK(En1VtaJHV;J=2{56=A*ZPQ9;Twy~#azv&C;Efzw40&X2; zVa$0J+OovDz&g-cVJ)=FoXcI?-A6o9?{U(P4uxIqrfg&OB2z)@sOzNC`^w!Ke)|`T z!}O1#OT7(JgL+mStw@G+v*dE3;zV_t?mr{&Ti%zvRpO&f5BuFPtHs^S{Cq>ltvx>W z9r{Onacc4GKdSp~?eVDNyS(U3q(y0?ph#KZ1$Cfw2h0)~`X^OE{h}YS{e@J_D*99U zLVi~HQ`Jc-9$1%&5MC_)1vf;39 zbb_wbqx-D!V23o&ucbWOsVUFdd2KZ z7}jcAZtISG_vpT=KfV_?7(8T9pFh(2EbVG905`CfmlMn}--CU$C31gJKVbutN8Kd% zP=7G{xNIZ{|1M6K;fh%0RAs4Rfqbo0BL0E;@hl}H7CML79++Pm9~rJ2#u?XvU)K=Z zaOWeBf!xO!ISnG9z3~$K0dRp{VJ<(O-AaujcDbfHHrsYtPMUIzmw`#?VZ33CHkFwu zb4zPGTY&wxy`^IV=(8>0Rk6?!=Fr;-dn@=(wWG-CbV?yl;sN3HswgFmG6v=d%&NbG zd(d#E7&LZ)MNL3WX>Ei)!+gp1LLF1n`qP4W)Cmg)MH)0B?EsAsu?_HFg0*uKWlfaNSiadW%q>4m?J@q`jUi2 z%2{8!54q0!!#kU5&6WsHu|DE1($BIoxm*5J)>o2`M<57yl=|VRc4S(I!nl7ORQX|s zdB(lwB3oDIWX}XLp4rHOhb>Z#w!vo zy6GkJd`pD&KkIH=F6g26+E+q)V7+abtv$@iR6zLNvxeA|_MVQ#&WWy3?teYIy?v-P zpqF3F5X^Bl6!rstF)Qc}R3X_Oa>g^=Q=LETJ8fI7e_QsN*O)HC{{9a`ks(gMLD#k} zvTnPsqv3-&%b_PwK22iLR0QWX7@DX^pP6^NQ$ep!{ks?6AJlNrra!{^3f<;(_>sM- z<;#ToF*VRSe?xX|FHXTI>0EjpvzhxTG{eS;21(vY*UE;-Vx(_Gf1?Owr=K7iKuT^W z%YAqjwKW|woi=~9`a2JKUXjr6cn;};uNKV{?Zf|ql~WJl0(+D?MpU}`Ii$8=%P#P~ zdIKr(c}72AoZ6c!&2KDK)+;u+c_Q%knx`TzW2%vq2jJ`CEYJ1Cgv zx(>s5Q%)3<`=|n#fvt$X^3o(v=5T-Ia ziS|ybb-1~kQK!GE_Zo(qC)i#&rx9s%DVKyc677=Y$Ueg!?pw(lQGaZtpkzzPG*6^+ zfo+tf6lkpvhC;(=LkHtRNH-G9Yrr$B7+fE&+A?e|Yo&F!)of{Id2Rk|)|=l0geV>?m-w za=7em?IR#x=cDz$H4WNHXWC|LZy2E81w714Yq2|%*^CcXBO!ZZZ#6rZemZYKXLoPa zz^#Kn577^vI54Wuj?RWWHa)a?|2TEnYv1<@iFg{aku$Je`Rhm){zSY^)>j#=wyK^e z_Djc#qQEb@I~niVYfU%l^h4^e>4w)|Fm$n;aZCY!%)!WOQ7hRhg+e8$wCYpJhcb~k z4?WIRQ+GXU9N|{hWH*j93gDSiVti>zu}rs)vW3_W*e!Mr#$3Y2TM=t>%T}|$`J!nD zc!`cWHwj-W-{`RbdB#u(|7;tJM zTt7g))yQ=P99LA%<3OZ|p&w1MHL?w{cCqBatkPwm^wSJu&2JpH$TNsckrFt(fvcG@ zy*iid=SQ}#I@HFW{~Mx)Gmbi`XF6HV;|c#PkZ(f)=^faTC6qto$(*$ zH`V99@~d?KWSrSkmjq;Ep4{1*q78CP@ENY;@#f$j&`QTy6!dqRllpbQ)knAEwkMbOc`=o z9Ixo2k@-&ZedNQc9>{--W6@fMCj4Bz?X#@~mQr)Jr61fO;~npvuUu*FK=7|w3;cAP zO>JpxI&J_5N+5;$>Sr4wOhFci1a;8PEAF<$K<{Jke6O3>P4ptpgR4lM>lG;f)_~i{ z2-g>v%dH`ngF8|QQ9=A7ni8R&F79tYYAK!n*yDg^?`VHx^SAx74zsy!KW$BI8?8y; zl{Cr30k`*uzRa}7QBOu-Gu3m#zBcx2IW*^=4oA9Kdnfg;=Fuv&DN@oJ zBT;EVOSK);gH(a)8=7a@VE=J}`k*~Q=>gNVk*ZWno%Y8KTw>I00gOusA->`z=Kk3g*Q8bASk%KMPG;rR-8OtI@T zQ29$-!`v-AZqHZn5V%D4p|%10FqQ1-?M>|Qd~qKGV!h5j*?t5F)q495`yBgE(0PPI z5845WsVZ9!+k5LL%P*6kVYTjd-JyDgxyY%d&G=YtSyV>zZmm1DtLiB0e!o{xpMJeQ zcm3F5bl%<8i&INLGoBf~FEH5ejMkt{^ta|bru%$Vc9nhwulK9od+yiHR0nT6WRrsq zVYA%{>%WaK3z=>bAnSHeoxP^0`f+7cy zL_rgM$128Q`OFDVgrmsv$hh8EYCdl_y7MVL_Z%ydO5q;dphf*`z5})8YE*eZIubw6 zFQ>0~8xkp=#qICvm>b*{yD1XMq3}a`rqu5w>2k@e$ zl#L7}`*?Q~I5@W4^jz?42VM15PnGArr<-Sm`yWuuZFimqHOxYAe7Xv>v%U{b zfwaf$xIPW0#H7VsiO!9(hf7081#Z{QR;VZxYftveB;CF429%EkVuw^eUB9Ko&3oUWI$C zGv9gA)rUAs`EaqweyoM)wdkqni0H6rl_)}V6aS9S6McZ}u_~Za9>Y#Yl&}tbGn+B% zA=&8%ao=^`UTcw?YU^uj?^fTdx&m2;W!2wm+SI+(HP?4GbcCAJXHyp-6(?B`YqmAj zIutyo^6Y_*6^<6pYpyq*mELaTb}|-x`|r|==(j*>9i)x}b1Z|==r-hX)xbO`7ifLn zJCndX2izar6G1CG2-IcQVCA+H$ny=B)1Yiiwv04OKz+E>tTo>?egc1;5?up*f!X8K zum_}T0$(?5m-a4aY=@%Geq9nf)fC{lb(vq&7A4z~@D^Fg`%=WIWb)Ib`ozK}!Es>` zg965@tEElwsr*YSjA#Su&yk)aZ#QZ$xW~3b`eK{$0(>H*06hiw%rbro>=U;}rJ@um zC%Yu`l@1oy;^Ejn;XONpzDM@+{`NF?Cpa&IeoSP%Td&Zaubp4>q#CVhUHh!gsM}rN zR^Qmr!1xjz{riKb;5ai5XEfHD0j_m|JJU_Lb6X0z_G_O#YmxE>tvVLEr16SYs{CU`{+-$u8QYJ{UDJAZT2+g)EhMSF zt!gq#vUavjYOBb6l$_b*SoHNEUUdxr%x<8zKvm_KcM@3(soyPtk7$Sf3;WXoa+V*+ zHfLl|^&Q8oWqS#;@MY3Sg-H1gQUKn_@+51q_54_7B{hlM;;kW85rIURd#&?|EznYC zT&jOims1m0eYF~`_0~?Vqv}-kgYw_ zCK`HcyeRqA8$pufIqEdH(@@lSaKT>9mM|J-H6t)>m_Mj%FmIacMZEJp^{%n5%dX9? zWzLR{7m%^I71Z5EYbm&Oh1nWf^DR>?13?Fv2b#eX;3@Ak+_yY+jb$%Ls{#(k# z%do)o5;8{9p$fFdR&D!cyAR5}C!lwCISx4Qx&k~6i4))w5e1nUI2lh8-V@$&-Vxq? zFn4f(>(g~2h43eAo^WLJ!*`tJG3r+G{!m72kKJ z?S`lIRp3P$Q?E8&u^*&a@c`f95zphzNq16jrT8Qvi5(MWB_3?vEX6nDb&j#HywiZCYK!!nw90iF>0Z@l}SseBi?z$Zg`drb4i>H9SX+? z$4JLH`$pSYYjyY{&Lb6s$s^Y|0bJ-t0yo?`gyb&qp@cTIA|xN4kNoU1`E z{lU@8LEGO!-Kn$vgRQ5H1_jIy>u2jt>oRLsYq~Ywdc#s;)|j%5vrSWNuRX)LX5!|6%4p?o>wwGpiHn{cfukK^+YwiHgLRj0Cz`WtQr_A%ibH}sKvlZqLE5Qw})YA--J4X;L2$Y~a z0Yr16F~PyBjGkyBolp|DJ@etUEs4e6?$k`?9CuOZh)&0ffHM6dijkP4`HCoYrk3$D z1XKqG1pNp+9(Xq}Ehr**U&z6*{~~ThkOEIo8c$lwrlXjoWK_|fng z;kjW{uq&X_cZknhb!&Bh^*wdCrj_QVdbFyO;*wM?wqR$_BFNKfkCkCwY!d8p&Jh-J z2bmZ;ox&ljrjV8}BS1aXnf9U1c>59kzuov-W+l_`3hWa zPJ*H<1QI&k-WKFGk{~-lmgyHrxco$|rkYV!vW8Sr?WtMRQL2_o2bEJ_P%+J<;$ST~ z1AM$_Dwxiv57RA~8W=CTa13{r9|}wpfs~?mv4i+p(RT4uiC3zSo8{LO^OQxZ0QFn- zXibe~kk31x=Gw*Dm)bzzA-*NPCwzCp`mU|7SG!9atG(cp=kr4|Sz}kvP~+-@szlX6 zrBM;CD3s5W-IR)@lO;y+5^;$5xF}q-5f|f2fL)!08i1}a1L?#8L8|0;0&n~q6j;mQ zzD{B5nRQGf<~0pj;@|=Ii+V=grS4JJsXf$qDg~Z`Rpck~AM!G}hulDJg{uEssQ<@N z{a}9a7wjcB0SC8I(f}m*5SVX#r=)ZY9YHhHOGq1Sqfk9!T*{%mjFj9sq{?3Ui&g%AAI;OW-)b zT!RtV0__mMhCmBwSvf0WrSMf_HisR+PGy&|8$o-qgWba(Vvn=u*}Lpppsww#h|_XG zTo@P4#d9g}B<#!$;6`#&xj!M#aTT|nJIj569K4DA3*bd30kioI$kJ%wmaZY;=q_MK zPGfPvM!&~#Q8Up%(JqlrR4BeKPLS-9SS0b%0%;d%S7|TlQ0WZmD(N2SS?N9LW9c>N zCTR~TF1;^VAsHyikZ2`8#k-*m1=zQrD2f;9@yqxkyb#*)3Ra9^*d=goOGH`V3U31a zH3De}B*+$EaeRQBe$HRw5AqxMIs7ob9UsLL+lnKFKpuuaQnFuZUr}kn+PAvpuJ9kO5!p1l6%X2;z}X?uAHmp>Ny*S@WFgK zKZM@`R1d=^0fDz1D3X`JkTnIC`7WYD2cgHn)e@38fUZA{{lt9mB1ks9j@RLSqBf#p z(M(twl!y+BP6M}pQ*>8!8~!~m+6ifDQ$&4Xr4S(!@GATX{x`H;KRgk~@gLy*w+S1C zwZ?p~TJ$cu6=;{Xs1~h-gv9^pJGufBtVJBaAYTF=a2aX;WJf5UlJ{@?K%zVnkSgL5y1p7RNq z{O|Cy|Fruvw_VdT!N>-kDZA^O7q2mI1=p!pvHr+*iSv710v zU4gSb&!2;9aQgo{&hTgdf5&+k-7fK0;kCEn|2=_z^^*S=7}k$)^~!)%uH#L-ohNvn z7XbyV6@p;=iHFR~OrYbt!+RPJJm?zW2TuVH{|XpkGZ5hZz)iOWa&asW_gkPX?*NZh z4P37r#EfUlemBq`)A0s~tlv=iC@XWSgxHwtJ@KcF}z0t!i> zZa9AnZ{T&n-dFIyK`-+IQenUGrSS2;qYRE8@csWCmC)s9QUqlW&1+y#w#~v~Wn+39Yya_~NQ*>~)9=H~Ac!dRirvAVGtAfu8V8~JU zt{DDK5d4*J;B5c*9WXhAk0#Jw98mECp$(hEUy#Gk{NN`afPux~-8dj)HWZ%YlyC;F z>O6QBZU$|D61X`xu)9IX7|<2C;Qp%v?Mx^#3VJ9CgzOCH3&)@zEd=hmF|w1t&h6yA zKv4gHvC3Cy!tdkz!zj@gS&fcDdjn_v7F~~Ok#<1#x`j*_an>LMf%GnecfSp0gAat0 zpe6kb3D-A4tMZTF3%%?sG8oy*S91oif_e${w7vWY;9Y~zFfgakV2~R8vY^>#ZK-H_dnM7EEmqP{CiOFTn9okoPR6K#fBqqx!uSY?7Xm; zUx_uv+wdjqb)*-537O3e5K2Wy#Ak#It{FZ+B0SuMQ8eNafuMORb5JVUW$r3INrK150+WxRpDEQIiLsWAv8%NC7c zu97}9j$hXJN#{5T*Jr6maTR#`+x#gn$2ZhuOE0@VIxceE#P=l~k(q9A;1^w!g&}je z7~u)oz`I-`@~y&7JKmAWKw~gyC(sibkI1dIjGuWP=Lj2c#;`;qOza!a|7$5_S`@qx@lFDeEIYCsu%$u@`?LpUF3I zEn!cn{*=BZ8*rP%@0k?xkvvy3m!UjLY$W<8WrL)leUSG41NU$UIST)w8jl7ND`_>DqZwa(u{9SMzCwpb&#BX2pPc+;N`dn9Vgh4eDt+&232AX9><=E3z*vu ztuVu9p?nX!3Op%}@tN9WpP_cDTFeIqq{+*Gm7PtW#X9(Gl`+=cx~(+tlN2~qKE$3~ z_a3c@sZhPD{aK%lMT%0eg+8q%?Mx@go{^mbwpm9RC^}a(PlNf7<9}G23td$+P=+ZK zE)k#HUYS(emHoprNs^&j%&#NHQd*{l9gNvTKOt%P1pkdIL|b5|=rP`G?6N|N&!@NZ z3q(us!Dt&HgV{un*@O`j`a-(DmyU;Vj`Gw#l)ylTs6zgfJ zUudp0!K3iTgYJH`_`D>C61;wr5@i%z`$AIezJ*;qe9-JI#azZ5j(G3DIX-1+k4sUc)!3!ilwekmi^3r)nwm> zXsMyloTTuCwO20l`guN4@klGhH~BR(%kB_d8eICs>m^zX`$4653p0X zFv3L5M3)FF*b~A|yk4Zjf3r(SD~sU`kb#i7F%y3-8HtL8VvzwaqN8c8xGfgrQjmF? z_S#f_615oI6dH5$#F*?P^V_3B_Q?*2Pjh#@52%TFM`;2--rd==j$bW5tzwZ+&c>cl zk*~VDT;Sfg8&aoX3=%jsA?GJczDb_KD6@p<;|`PV(hlVsTW{0ZvU<@DY&&jXvZ;2U zH6G}_PK`iku^)w4AB8y8blUUEFHttYbl){e*-bizRBnMs3*Ah+KZ}B3ZdDs;95waB$ zg`bno6=f2L);GuY_;(5WD$HN|4-*eHJQ4F)k>&H# zTVEeZe3njDy-}w~FL@R@dx?0(S_WJ&saX8EthuH@F^hRaq=t3SSrTJ{HtY3=m_G=^2c+1IP%mGS)zxYp3 zjY?s%(Cf-VMCdo!p zoxJ10b-a@}OchOy&`0xZ$T(%DdkgnP9>dViEPSX$1-!^D<^JGVssqeYhsXUNK2I?L znMI=fbL5EJA0+7{p2WW+^h}WW5Y~j=MKmSHp)Q{tJ~QYeW}Rmi@(wgkm+-4> zCwC&cFmQ$TlDDC`9T6@*tFBSt%q4phR2MoXsDk;|yuy*g8I;5QmtpISqnv(z)uGRk zbC5JOn8}ovt49bAEh9Z4s%btdB%PS+N_4Nsn+5iiYr(CsOxjW7hkcM4 zB~g|579Z_>%=eMK!4sItte2Y_np2zVT@8HghVqINPkx%7A6z9D(-+yK$hS(m}|9z zdHH-|uD30I5sPHr3G>89V5YrW)EYfYlzL^zGVHPFgmf%&fT*S8L@!YhJB&YqOyzcQ zD^REhF^TMLAdqjM8!#(+kk8=8AsjYGm`^<+E!;AEhd3D@&L3gxg}&Gc;Rmn)--Lal zrIOcJJ(EoBXb+1t)0Op5(|sho=f>cXn*QQ=k1u^na#r*=k}sNwMYGFzCH_e0&feot zArb8(av>2+0KXo)#$?)@;Z*h>pUjJx_H;hG15~&*>^}Me zB2)MESNr@akqam{9e=Ids5&4|R$cU6?cd5j#qX>7icBS2si;>?_4%Q_t*!8H6E-re z(08dc79WdGmo`=Tt5W4x6oY+~+V{#$lIGYkSOX`6Dmw)4Cv79WfL9~%3}hyQMrsW> zQ@NPMoQ|0Td+DR8pI$97+pTqOvSZGz?v9@6?y;`>ju=}UIPVdbE7l_0UfaLcAWJ!< zcy$MD!8o(j^2>Y)SgfYjrNH_`f-l=J@M_BiU#^}uqhqtH-qqdJACiMxID?&Ta1M7u z9>{UR1(}&H`&Bz&NUmMp8kR+9sDiPASn%@gkmJ|_$vtIv?PI?k0kYtmArC@UheU?9 z3)4j0j@sX#XTz%a6-^E|`LoI2O-42@21mzxagQ6t#YZ*z8CMe<3l3F{qdJB64m}k- zF|dz+lwYjxRi8-B2h}>|Y`H=bfvpmFsAb?CvB#-la(HkYZy?IkL@TVSdO!- zFed?3YA|;(FEE^{+f%cp)(_MrYJI#eyH2B9uPdu9s_s!WwR%|X+d85yqwY;@MBT8u z1$FT{O6LF#PGca-Hye_S`@om$qlq+61x?o9kQ}|z)&;VfRN#j?)*kF^;yecqXk&pJ zUTz&|`e>MH>S!x)O(kwn@3~N@2v+l>xPC$l(J#5kXQ}_I;OvO;F`eQrHJsL{SNx8~ zQ=3{7FC=YF;aa}ReAgyEduLl!R#v(UOF4wK7D}po$nU1ZwTivbZPe_{oR&$_sZ2b!ZYB*EZxn@Z1hWcTKp85>kkGgO5 zql~LeHgF<4Y*=XAYWie~H{CQoGTAMIY^SYm^BMD0Ya{z@8)vy@Ic-}CPM>?N`z*EA zKY_ac$LfI$ta}!%S!%qh57MXTyBa;VQDhaEWu;;!iCwL^ko?zTTjUzl%HL&ps-(|yr5gYtL=}BJ!zpSjux>kVv_!8_9LOT z@q)Ogk!ynQ_}){Em%S8aU`N37!p$g1#Pi8za4xi^7-rWbRtML;sDG<7)@-SHqFZZ_ z>JQeWRr%L+(@E+d)HT#CGTbnG%%@GE=4I9wwnkR9+0XLBp5l7v*kRjX_jTX!barjF zt+1Z5-*MtVcId4=?c*G4>~Y|FR}Nl=DWJ6}Gnd%<+K*b68NV5aSby33+loyw28Z!) zs~wb4BaQWjSn~kL9V1K|4E1`C!PjIkb}-D)^{)9 zm!_-A14qPmY8H@upvA)$;8H5ow+$kha{PSmrKw>~mDQ`g@MJRs*Fo$dTu2>ilN)u^hGv_Hv*+9|9%4))r^~ z%evOQ)|_n}X}bZW#ZB`Z%NT1XYr18w`Cs!0V8d7wVzL@ng4f$rgIu?{W^e6C;}6#t z?ziZ>S$ZP~o#YW7dt9?~^cQl4cPG)67)wMGw0kSif==ffr{3|>F0voB^@Y*H zVJ)!rvh}q3T1HtKL*^`RIRZolY7MjZvvo6%FupfEvF@@Bu}n9PGrTlf%|k4>=~ul& zcToSvFuZgn)MXBdoxzm`mlbQ87ecILL zSLSZcnV&N^_h|lwf}EnVqSOxl?bW&aTW6+5Cj>S8-Jo@BRfDckpMqn22dgZy6{5!c z0B^2yxvj?X%H-7_)*Y$qQMaJ>ef6ixgo<(HW###meAUUCG+@7$Lx0Hxk6_-s!aCO8 z1=J(EVNF~@4xs~JuW<)k&g8Q^H(q!IN%Wf;GwTAMqcYRwgWMcq1k( z`gLSom?~(S)a?W8lv(_<%p`8Ri4TTRXb`n z>gx3w#w^nf@LJc}`#XL+U%2-X;gF=&p9$msfL(%gsEL9H4|oIkLj5-kyf)H=0sKPl z2z!!QNas-+vW_qTA@I(97xJf`+n3ot+85c))=#zS9=jr{X`~^1Z^&~0jhb@BYUw-dGV3HNoYl4!=3IRNBz~sV z462<{bGh2SloYd(YR&b205wS?azQ-nPE$} zA8=^ECA^Del=+x-8F&{rwj_Wf^ecU$Wq^b6@T^z7N~ zV~e^p-;B0dTeEvZkD~G=`QDtaZ69Yj+a%=f$xq22pL0BmZ#BI|lg8Vl>By{o=g6;^7?xBYHep{f#9WmJ6rT~ofIx<+@& z5N{+6TTRn#$6fWF3dk2dO?0ET@_N{f`3jyEO)y_cq%>c22G#`I@RpL>qI5(JRrd%a zAK_RF8S1_6T>`04sAq!{NO#9D=XHn6*4O%9#ot78cy?)>2x?kEBUdzM4ym}b8MKIbcJ_du6>+j84vGc+?C zGaj?GCR<|76~UT%%?O`%zBT@IV0g&Bh`kLB&5yM7q(@|6trKz*3Yr(KEUd^o*|t-g zy={l)oX)wP-7fokcI)hpnfH>qHk=!YhHnYI7kJ!X^n1@q$4HA)Ke=vj z&Auw4eC@BBzg|@Atrl08mk;>ushD1SM0cw$wk}eC(V_#@U%i8J-XJPy4VMnw!4^Il zy@-Dj?GW+cZILVPDoK(&6~*AQU?+S(HU%9oEP<4gh0JYwGgaoT_6&lA7!usws~jJJ zXkKN@hfI}MmKyUrOO9;>%#;to^DYCJ^ffldnrqL4v3{{*BBY@Tu9lt;uI-L~j&-hm z9;Igi_~I`ErBoyLedlTWIUw2pfjjz(t(EnIxxk!aE;bD@4AnEH?Vc;>R>e|vyjrC> z;kz-gI5;U}b+|aLTeI$IDd~^XM`uDbUc1j7T!r%dV{P!(6I-v(T9|dF^@7%?Tay{b zQkNyPjJ+0a5BlzRPqR>gOWLE`*>hwmKj)4UA zi`M5>f7=<`XuB`u6hCqp9EHw$=W$mn_Xu}m&}+!tUbo%z$}_-CJKMVI+&0fGcZlM1Z&vpEteKhLS_P$bO<9wYly)s`ZSv-( ze>Auk)*|SuZ-w%R$d}(jEh8Gao>{jVll7u7h zisGtc)thRI>!rq@rZtuzdy%WmGu7LkI>KtvTcVl3{`kst@(IeF%6kf25v{ncs8K{I z4$1ta63H0x9MJ~sw=kWnW^T|dxq&DEwcLJKa}o9g&vbbntG@R96*v;rDR(JBwYc>$J<_9OWGCTASLvzjs$$uDD$JcWo)bn#6WSmg8aK!{Qi;SSpw8 zlx4^a;8NUBREvKDSKd&R;P--0;~jPxGm;ARj`u8gQ?BN&YR3xucOcV~AOowDJ}s6mFO<1NVp!{eZuoPktdcQyb~=K$m_8&(v$oL*_QK8^}HZJV)n3m9><7L#oNO zL<7%LP>@JGM_n%*JmhBFbWU=zcHXw$jylcGQ%=Sa;~eaI@7@KO;3m&0;3Z42eZGX#&>CIY?YCAjk zNPa}Sk$JD%j>v4;@>KG(=Gznbrqi1^8&xz|5n&IC_bY-s{igV#Fpu(e{|2p4fBp5^ zQPo$g`d1yPSo-U9+4k?2?*(PTcklPGACbSf-)k#Ms@BwQuYYa$%T#8mwRdw#+@ven z)0;faq;rWt8LbsMV0XaJ<`Y_oentPnnqfzg5_sZ{05-1&TT8DdcM{>mc48Q4g8u-f zTI9?D&NILn3T@lPwF*+zlH8NrTRj%yrneku<0Iq_Y9_5=#)95@6Z4WuXV(Jb_Xya! zQrbg*gx=~T+W}wx&fCPBK@9ZVakp{5go;`kXxbv2dmN>XTh1cacc;YJ!uhZ3oF|)@ z>M^;~h{xn+dN#cdnE7u^49u#0_%_@}ZkezJoV$v-smvFqpHL`fl!9+~aCpSb1|8$m zno1MaBz$b9O$lzbBYl0V=`H_lb-#7<94c=^{(i_^kYwj&?MUC1n%5$v`QM3KoBnLH zCB_z(9kkI;ui38nTRay@W3GDg>}O4L^ijI=HSWsG<->n(`t`@p&1LHE)!!n@ZvMFO zGya$H*Q4?sRhMeJ*Jpr#x6?e=_6AzU&t2*6Nem|!(JG+B4nwN!Vqr2kPR<8MFq!ZI z7^uBm5W9!IPIabkklVajL?4e4vg*oR;ouo_9C8&3U9(|jR_R*pUgtRkej~5F-N|C= z8Es`|0Ck6ODBl#^vAzIt(E}XT4B+TgB8=c0aED-49>5XI8_J*5{lB8F0z8Uqjqc29 zTsCgRHXb4*1VV!a3GVLhq40pBkBFDrB!@FD zyF7mJ%Ltxm_-q&*<{5b;HY(wGf+2og+~9;wN!?P4+PSCB?%?`GMQUBj;-r%a+v2{8 zHAZP8&V;TBT<5#UqrYpI6J>W&JA`x;cQI>CmWI$81(dO?ONW(wDsn9_<%Q*?=Z(*6 z%$=OOKi4%srGPJNE~+ZlmQMY2zw&kUX=HMrtMA_AW2`lvHFcrJL(jMevcZ{t9bsOq$G~_nVV$fx7~WX3~+pC+e0&o zxF=j?@=a$ON7a5)x#g2z+2)Ur3WtBVpQp|{kQl3CFpf>|P+Q zPS6*qd%#$Go0aBV(-o*--ZYhUXR2n^kxxv2Y*7I>foHz}>aHv8}+$DCC^ObI@RjS7-Kje(xQlAGO zxxboG-B%^5x+*is@!;vrkTwe=h2xTmysee&Y@DyV@!s14z6)9wl4}Txbcykb?G*Dx z^x^1lV{_x~B=lHoi90Ht4e40vN_bB8x$D0@DBW-^ zd}f3$q9%M-#G%NQQ3Im?h-qrQC}D7$AxVzOinjOK&Tac$(%^*Au^%EXhDiRyy+%O; z^S$kU%@H!)GE$hq22nS`-0{5jRF(Nte5tZ{W5KBW_PO7^D|kEd-OBg(at-+&7-`Rx zM3(C-o`3$e>g$?{+MK#IQ1N@)*t7YJF~t0ldcmCMCJ1F>2KXFV@MZ5s23?e;TKK!Ns# z@dEH@U5&?$+u$n)Q#YtKw1L?OKX(B;a?b>#IKy&4(Hl9h3(3REH>wTlztjOxvr5xu zYP)OQwR)|+_7_d5y0hA-%2ugWNU$ImArEhsBF*9?^%w79?CHp@LJM0;Z)e(b(!pL&+dQ)LES^zgHtq$n|6fGW>TVD}60^a@WI3 z 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 Qc}sm*3yv%0d&&3wA7vx$-v9sr literal 0 HcmV?d00001 From 6295eeb2acdff5967baf77f7587c0f3fa508e3ec Mon Sep 17 00:00:00 2001 From: David Elner Date: Tue, 6 Oct 2026 10:51:42 -0400 Subject: [PATCH 12/18] Changed: Restructure Pipecat events, move metadata to contrib.pipecat.* --- .../integrations/pipecat/test_pipecat.py | 22 ++- .../pipecat/test_voice_pipeline.py | 12 +- .../braintrust/integrations/pipecat/ttfb.py | 13 +- .../integrations/pipecat/turn_metrics.py | 2 +- .../integrations/pipecat/voice/README.md | 6 +- .../pipecat/voice/instrumentation.py | 152 +++++++++++------- .../integrations/pipecat/voice/realtime.py | 2 +- .../pipecat/voice/test_instrumentation.py | 16 +- .../integrations/pipecat/voice/test_turns.py | 22 +-- .../pipecat/voice/test_user_capture.py | 4 +- .../integrations/pipecat/voice/turns.py | 22 +-- .../pipecat/voice/user_capture.py | 22 +-- 12 files changed, 195 insertions(+), 100 deletions(-) diff --git a/py/src/braintrust/integrations/pipecat/test_pipecat.py b/py/src/braintrust/integrations/pipecat/test_pipecat.py index 624a14a2d..040c7a189 100644 --- a/py/src/braintrust/integrations/pipecat/test_pipecat.py +++ b/py/src/braintrust/integrations/pipecat/test_pipecat.py @@ -225,13 +225,13 @@ async def on_pipeline_started(_worker, _frame): assert llm_span["metadata"]["turn.id"] == turn["span_id"] assert llm_span["span_attributes"]["type"] == "llm" assert "braintrust pipecat integration" in llm_span["output"][0]["content"].lower() - assert llm_span["metadata"]["pipecat.usage"]["value"]["total_tokens"] == llm_span["metrics"]["tokens"] + assert llm_span["metadata"]["contrib.pipecat.usage"]["value"]["total_tokens"] == llm_span["metrics"]["tokens"] assert llm_span["metadata"]["model"] == "gpt-4o-mini" assert llm_span["metadata"]["provider"] == "openai" assert llm_span["metrics"]["prompt_tokens"] > 0 assert llm_span["metrics"]["completion_tokens"] > 0 assert llm_span["metrics"]["time_to_first_token"] >= 0 - assert llm_span["metadata"]["pipecat.ttfb"] + assert llm_span["metadata"]["contrib.pipecat.ttfb"] return pipeline_span = _single_span(logs, "pipecat_pipeline") assert _span_type(pipeline_span) == "task" @@ -308,7 +308,7 @@ async def test_legacy_turn_metrics_follow_speech_or_pipeline(memory_logger, metr await observer._handle_frame(frames.MetricsFrame(data=[metric])) await observer.cleanup() span = _single_span(memory_logger.pop(), "user_speaking" if capture_audio else "pipecat_pipeline") - assert span["metadata"]["pipecat.turn_metrics"] == [ + assert span["metadata"]["contrib.pipecat.turn_metrics"] == [ dict( type=metric_name, processor="BaseSmartTurn", @@ -351,10 +351,10 @@ async def test_ttfb_routes_by_processor_and_retains_unmatched(memory_logger): await observer._handle_frame(frames.TTSStoppedFrame(), processor=tts) observer._close_all_open_spans() logs = memory_logger.pop() - assert _single_span(logs, "tts_response")["metadata"]["pipecat.ttfb"][0]["value"] == 0.09 + assert _single_span(logs, "tts_response")["metadata"]["contrib.pipecat.ttfb"][0]["value"] == 0.09 assert _single_span(logs, "pipecat_llm_response")["metrics"]["time_to_first_token"] == 0.24 assert _single_span(logs, "pipecat_llm_response")["metrics"]["tokens"] == 15 - assert _single_span(logs, "pipecat_pipeline")["metadata"]["pipecat.ttfb"][0]["processor"] == "stt" + assert _single_span(logs, "pipecat_pipeline")["metadata"]["contrib.pipecat.ttfb"][0]["processor"] == "stt" def test_ttfb_router_bounds_and_rejects_mismatched_sources(): @@ -371,6 +371,18 @@ def test_ttfb_router_bounds_and_rejects_mismatched_sources(): for _ in range(35): router.capture(metric, source) assert operation[-1]["metadata"]["braintrust.ttfb.omitted"] == 3 + measurement = _import("pipecat.metrics.metrics.ProcessingMetricsData")(processor="tts", value=0.5) + router.capture_measurement(measurement, SimpleNamespace(name="tts")) + assert root[-1]["metadata"]["contrib.pipecat.measurements"][0]["type"] == "ProcessingMetricsData" + for _ in range(35): + router.capture_measurement(measurement, source) + assert operation[-1]["metadata"]["braintrust.measurements.omitted"] == 3 + retained = [ + row["metadata"]["contrib.pipecat.measurements"] + for row in operation + if "contrib.pipecat.measurements" in row["metadata"] + ] + assert len(retained[-1]) == 32 router.start("a", source, lambda **row: operation.append(row)) assert router.capture(metric, source) is None router.clear() diff --git a/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py b/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py index 3f019d8cc..ffb6cd8be 100644 --- a/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py +++ b/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py @@ -251,6 +251,14 @@ async def feed(worker, frame): recognition = _single_span(rows, "stt") tool = _single_span(rows, "lookup_order") synthesis = _single_span(rows, "tts") + measurements = synthesis["metadata"]["contrib.pipecat.measurements"] + assert any(m["type"] == "TTSUsageMetricsData" for m in measurements) + assert any(m["type"] == "ProcessingMetricsData" for m in measurements) + assert all(m["processor"].startswith("OpenAITTSService") for m in measurements) + for row in rows: + assert not any(key.startswith("pipecat.") for key in row.get("metadata", {})) + events = row.get("metadata", {}).get("contrib.pipecat.events", []) + assert not any(e["contrib.pipecat.frame.type"] in {"StartFrame", "EndFrame", "MetricsFrame"} for e in events) models = [r for r in rows if r.get("span_attributes", {}).get("name") == "llm_response"] assert models assert recognition["span_parents"] == [user["span_id"]] @@ -258,7 +266,7 @@ async def feed(worker, frame): assert any( r["span_id"] not in tool["span_parents"] and "friday" in json.dumps(r["output"]).lower() for r in models ), "a model response must deliver the tool result to the caller" - assert "friday" in synthesis["metadata"]["pipecat.text"].lower() + assert "friday" in synthesis["metadata"]["contrib.pipecat.text"].lower() for model in models: assert model["metadata"]["model"] == "gpt-4.1-mini" assert model["metrics"]["tokens"] > 0 @@ -382,7 +390,7 @@ async def feed(worker, frame): assert model["metadata"]["provider"] == "openai" assert model["metadata"]["model"].startswith("gpt-realtime") assert model["metrics"]["tokens"] > 0 - assert model["metadata"].get("pipecat.text") != "" + assert model["metadata"].get("contrib.pipecat.text") != "" assert model["output"] assert not [r for r in rows if r.get("span_attributes", {}).get("name") in ("stt", "tts")] audio_spans = [r for r in rows if r.get("span_attributes", {}).get("name") == "pipecat.audio_output"] diff --git a/py/src/braintrust/integrations/pipecat/ttfb.py b/py/src/braintrust/integrations/pipecat/ttfb.py index eb28b7faa..6bea0b33f 100644 --- a/py/src/braintrust/integrations/pipecat/ttfb.py +++ b/py/src/braintrust/integrations/pipecat/ttfb.py @@ -47,5 +47,16 @@ def capture(self, metric, source): else {"processor": name, "value": metric.value} ) values.append(payload) - log(metadata={"pipecat.ttfb": list(values)}) + log(metadata={"contrib.pipecat.ttfb": list(values)}) return key + + def capture_measurement(self, metric, source): + """Retain native measurements without a frame/routing envelope.""" + _, log, state = self.owner(metric, source) + values = state.setdefault("measurements", []) + if len(values) >= 32: + state["measurements_omitted"] = state.get("measurements_omitted", 0) + 1 + log(metadata={"braintrust.measurements.omitted": state["measurements_omitted"]}) + return + values.append({"type": type(metric).__name__, **metric.model_dump(mode="json")}) + log(metadata={"contrib.pipecat.measurements": list(values)}) diff --git a/py/src/braintrust/integrations/pipecat/turn_metrics.py b/py/src/braintrust/integrations/pipecat/turn_metrics.py index 5534da2a4..fbb141c08 100644 --- a/py/src/braintrust/integrations/pipecat/turn_metrics.py +++ b/py/src/braintrust/integrations/pipecat/turn_metrics.py @@ -20,4 +20,4 @@ def log_turn_metric(span, state, metric): }, } ) - span.log(metadata={"pipecat.turn_metrics": list(predictions)}) + span.log(metadata={"contrib.pipecat.turn_metrics": list(predictions)}) diff --git a/py/src/braintrust/integrations/pipecat/voice/README.md b/py/src/braintrust/integrations/pipecat/voice/README.md index 7aa9b9f7a..3512c93b1 100644 --- a/py/src/braintrust/integrations/pipecat/voice/README.md +++ b/py/src/braintrust/integrations/pipecat/voice/README.md @@ -20,8 +20,10 @@ Install `braintrust[audio]` for Ogg/Opus or WAV encoding. Audio defaults off. Ex | Public interfaces | `setup_pipecat`, automatic integration setup, `wrap_pipeline_worker`, and an explicitly supplied `BraintrustPipecatObserver` share the same injection path. `trace_turns=False` retains the existing observer behavior. | | Trace | `user_turn`, `assistant_turn`, `stt`, `llm_response`, `tts`, and actual tool names retain native boundaries and metadata. Realtime audio uses `pipecat.audio_output`; tool-only model responses can be pipeline children. `turn.id` / `turn.reply_to` and `continuation.tool_call_ids` preserve observed associations. | | Audio | Combined stereo call; segmented-STT caller clips on user turns; generated clips on TTS spans. Selections reference successfully captured samples. | -| TTFB | Native `pipecat.ttfb` measurements follow the emitting processor and a unique active operation. STT measurements follow `run_stt` into its eventual span. Ambiguous/unmatched metrics remain on the pipeline; arrays are bounded to 32 with omission counts. | -| Turn detection | Native analyzer predictions in `pipecat.turn_metrics` on the user turn; preserve processor, metric class, confidence, completion and reported milliseconds. Retain 32 predictions and count omissions. Unassociated observations remain on the pipeline. | +| TTFB | Native `contrib.pipecat.ttfb` measurements follow the emitting processor and a unique active operation. STT measurements follow `run_stt` into its eventual span. Ambiguous/unmatched metrics remain on the pipeline; arrays are bounded to 32 with omission counts. | +| Turn detection | Native analyzer predictions in `contrib.pipecat.turn_metrics` on the user turn; preserve processor, metric class, confidence, completion and reported milliseconds. Retain 32 predictions and count omissions. Unassociated observations remain on the pipeline. | +| Measurements | `contrib.pipecat.measurements` retains native metric types and payloads on the emitting processor's unique active operation. Unattributable measurements remain on the pipeline. Each list retains at most 32 entries with `braintrust.measurements.omitted` counting excess observations. Metrics are not duplicated as frame events. | +| Events and configuration | Initial service/configuration observations are stored once in `contrib.pipecat.configuration` (up to 32 source/type pairs). Changes, speech boundaries, interruptions, cancellation, and errors retain compact timestamped events; routine start/end/connection notifications and startup zero metrics are excluded. Event payloads omit transport bookkeeping and empty fields. The pipeline-wide event budget remains 256 entries / 64 KiB. | | Input alignment | Segmented STT input provenance or successful OpenAI input writes plus server-VAD item offsets. Clears/reconnects invalidate unsupported mappings. | | Output alignment | Equal-rate mono PCM without a mixer. Other paths retain recordings without asserting unsupported selections. | | Memory | Configurable 32 MiB retained call PCM by default; separate 8 MiB caller/generated source limits; 64 MiB shared source budget; 32 MiB encoded-attachment budget. These are retained-data bounds, not total RSS bounds. | diff --git a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py index 803222a56..0745e0ba1 100644 --- a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py +++ b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py @@ -69,6 +69,30 @@ } +CONFIG_TYPES = { + "SpeechControlParamsFrame", + "STTMetadataFrame", + "LLMServiceMetadataFrame", + "LLMUpdateSettingsFrame", + "TTSUpdateSettingsFrame", + "STTUpdateSettingsFrame", + "VADParamsUpdateFrame", +} + + +def compact_frame(fields): + """Keep event content; omit transport bookkeeping and empty fields.""" + return { + key: value + for key, value in fields.items() + if key + not in {"id", "name", "broadcast_sibling_id", "interruptible", "transport_destination", "transport_source"} + and value is not None + and value != {} + and value != [] + } + + def native_value(value): if isinstance(value, bytes): return None @@ -151,6 +175,7 @@ def __init__( self.context_tool_results = [] self.result_origins = {} self.event_groups = {} + self.configuration = {} self.filtered_events = Counter() self.duplicate_events = 0 self.clock_anchored = False @@ -253,9 +278,9 @@ async def on_push_frame(self, data): state["observed_format"] = audio_format state["span"].log( metadata={ - "pipecat.sample_rate": frame.sample_rate, - "pipecat.num_channels": frame.num_channels, - "pipecat.frame.pts": frame.pts, + "contrib.pipecat.sample_rate": frame.sample_rate, + "contrib.pipecat.num_channels": frame.num_channels, + "contrib.pipecat.frame.pts": frame.pts, } ) if state["chunks"] and (state["rate"], state["channels"]) != audio_format: @@ -301,47 +326,42 @@ async def on_push_frame(self, data): fields = native_value(frame) if kind != "LLMContextFrame" else {} event_owner = self.root if kind == "MetricsFrame": - remaining = [] + # Pipeline startup broadcasts zero placeholders for every service. + if frame.data and all( + getattr(metric, "value", None) == 0 + and getattr(metric, "model", None) is None + and getattr(metric, "processor", None) != data.source.name + for metric in frame.data + ): + return for metric in frame.data: - if type(metric).__name__ == "TTFBMetricsData": + metric_type = type(metric).__name__ + if metric_type in TURN_METRIC_TYPES and data.source is self.user_aggregator and self.turns.user: + state = self.turns.user + log_turn_metric(state["span"], state, metric) # pylint: disable=unsubscriptable-object + elif metric_type == "TTFBMetricsData": owner = self.ttfb.capture(metric, data.source) if owner == "llm": self.llm.log(metrics={"time_to_first_token": metric.value}) elif ( - type(metric).__name__ == "LLMUsageMetricsData" + metric_type == "LLMUsageMetricsData" and self.ttfb.owner(metric, data.source, operation="llm")[0] == "llm" ): self.llm.log( metrics=_llm_usage_metrics(metric.value), metadata={ - "pipecat.usage": native_value(metric), + "contrib.pipecat.usage": native_value(metric), **_metadata_from_processor(data.source), **_metadata_from_metric(metric), }, ) else: - remaining.append(native_value(metric)) - if not remaining: - return - fields["data"] = remaining - if kind == "MetricsFrame" and data.source is self.user_aggregator and self.turns.user: - # The single discovered user aggregator emits its analyzer predictions - # before stopping this turn. Other sources retain unassociated events. - remaining = [] - for metric in frame.data: - if type(metric).__name__ in TURN_METRIC_TYPES: - state = self.turns.user - # Turns assigns role dictionaries through setattr. - log_turn_metric(state["span"], state, metric) # pylint: disable=unsubscriptable-object - elif type(metric).__name__ != "TTFBMetricsData": - remaining.append(native_value(metric)) - if not remaining: - return - fields["data"] = remaining + self.ttfb.capture_measurement(metric, data.source) + return if kind == "StartFrame": self.root.log( metadata={ - f"pipecat.{name}": getattr(frame, name) + f"contrib.pipecat.{name}": getattr(frame, name) for name in ( "audio_in_sample_rate", "audio_out_sample_rate", @@ -375,13 +395,13 @@ async def on_push_frame(self, data): type="task", set_current=False, internal={"instrumentation": "pipecat-auto"}, - metadata={"pipecat.start_frame": kind}, + metadata={"contrib.pipecat.start_frame": kind}, ) event_owner = self.user elif kind in {"UserStoppedSpeakingFrame", "VADUserStoppedSpeakingFrame"}: if kind == "VADUserStoppedSpeakingFrame" and self.user: event_owner = self.user - self.user.log(metadata={"pipecat.end_frame": kind}) + self.user.log(metadata={"contrib.pipecat.end_frame": kind}) self.user.end() self.user = None elif kind == "TranscriptionFrame": @@ -392,7 +412,7 @@ async def on_push_frame(self, data): type="task", set_current=False, internal={"instrumentation": "pipecat-auto"}, - metadata={f"pipecat.{key}": value for key, value in fields.items()}, + metadata={f"contrib.pipecat.{key}": value for key, value in fields.items()}, ) span.log(input={"text": frame.text}) span.end() @@ -451,7 +471,7 @@ async def on_push_frame(self, data): ), ) if self.llm: - self.llm.log(metadata={"pipecat.function_calls": native_value(frame.function_calls)}) + self.llm.log(metadata={"contrib.pipecat.function_calls": native_value(frame.function_calls)}) elif kind == "FunctionCallInProgressFrame": if frame.tool_call_id in self.started_tools: return @@ -463,11 +483,11 @@ async def on_push_frame(self, data): input=frame.arguments, metadata={ **correlation, - "pipecat.tool_call_id": frame.tool_call_id, - "pipecat.function_name": frame.function_name, - "pipecat.arguments": frame.arguments, - "pipecat.group_id": frame.group_id, - "pipecat.cancel_on_interruption": frame.cancel_on_interruption, + "contrib.pipecat.tool_call_id": frame.tool_call_id, + "contrib.pipecat.function_name": frame.function_name, + "contrib.pipecat.arguments": frame.arguments, + "contrib.pipecat.group_id": frame.group_id, + "contrib.pipecat.cancel_on_interruption": frame.cancel_on_interruption, }, set_current=False, internal={"instrumentation": "pipecat-auto"}, @@ -483,19 +503,19 @@ async def on_push_frame(self, data): self.result_origins[frame.tool_call_id] = request[1].get("turn.reply_to") tool.log( output=native_value(frame.result), - metadata={"pipecat.result": native_value(frame.result)}, + metadata={"contrib.pipecat.result": native_value(frame.result)}, ) if frame.error: tool.log(error=frame.error) else: - tool.log(metadata={"pipecat.cancelled": True}) + tool.log(metadata={"contrib.pipecat.cancelled": True}) tool.end() elif kind == "LLMFullResponseEndFrame" and self.llm: text = "".join(self.llm_text) message = {"role": "assistant", "content": text or None} if self.llm_tool_calls: message["tool_calls"] = list(self.llm_tool_calls) - self.llm.log(output=[message], metadata={"pipecat.text": text} if text else {}) + self.llm.log(output=[message], metadata={"contrib.pipecat.text": text} if text else {}) self.ttfb.end("llm") self.llm.end() self.llm = None @@ -509,9 +529,9 @@ async def on_push_frame(self, data): type="task", metadata={ **self.turns.metadata(turn), - "pipecat.context_id": getattr(frame, "context_id", None), + "contrib.pipecat.context_id": getattr(frame, "context_id", None), **({"openai.response.id": context} if self.realtime and context else {}), - "pipecat.append_to_context": frame.append_to_context, + "contrib.pipecat.append_to_context": frame.append_to_context, **_metadata_from_processor(data.source), }, set_current=False, @@ -532,7 +552,7 @@ async def on_push_frame(self, data): state = self.tts.get(self.frame_context(frame)) if state: state["text"].append(frame.text) - state["span"].log(metadata={"pipecat.text": "".join(state["text"])}) + state["span"].log(metadata={"contrib.pipecat.text": "".join(state["text"])}) elif kind == "TTSStoppedFrame": self.ttfb.end(("tts", self.frame_context(frame))) state = self.tts.pop(self.frame_context(frame), None) @@ -541,7 +561,7 @@ async def on_push_frame(self, data): self.complete_recording(state) elif kind == "InterruptionFrame": for state in self.tts.values(): - state["span"].log(metadata={"pipecat.end_frame": kind}) + state["span"].log(metadata={"contrib.pipecat.end_frame": kind}) state["span"].end() self.complete_recording(state) for context in self.tts: @@ -550,18 +570,45 @@ async def on_push_frame(self, data): elif kind in {"ErrorFrame", "FatalErrorFrame"}: self.root.log(error=str(frame.error)) - if kind in EVENT_TYPES: + if kind in CONFIG_TYPES: + fields = compact_frame(fields) + identity = (kind, data.source.name) + initial = identity not in self.configuration + if not initial and self.configuration[identity] == fields: + return + if initial and len(self.configuration) >= 32: + self.omitted_events += 1 + return + self.configuration[identity] = fields + self.root.log( + metadata={ + "contrib.pipecat.configuration": [ + {"type": frame_type, "source": source, "fields": values} + for (frame_type, source), values in self.configuration.items() + ] + } + ) + # Explicit update commands are changes even on their first observation. + if initial and kind in {"SpeechControlParamsFrame", "STTMetadataFrame", "LLMServiceMetadataFrame"}: + return + if kind in EVENT_TYPES and kind not in { + "StartFrame", + "EndFrame", + "ClientConnectedFrame", + "OutputTransportReadyFrame", + }: if kind.startswith("User"): turn = self.turns.user event_owner = turn["span"] if turn else self.root elif kind.startswith("Bot") or kind == "InterruptionFrame": turn = self.turns.assistant event_owner = turn["span"] if turn else self.root - # The current LLM is not evidence that a concurrent service metric - # belongs to it. Keep metrics at the root with native processor IDs. self.capture_event(data, fields, event_owner) def capture_event(self, data, fields, owner): + if len(self.events) >= 256: + self.omitted_events += 1 + return if not self.clock_anchored: self.root.log( metadata={ @@ -574,13 +621,10 @@ def capture_event(self, data, fields, owner): ) self.clock_anchored = True event = { - "pipecat.frame.type": type(data.frame).__name__, - "pipecat.frame.id": data.frame.id, - "pipecat.observer.timestamp": data.timestamp, - "pipecat.observer.source": data.source.name, - "pipecat.observer.destination": data.destination.name, - "pipecat.observer.direction": data.direction.name, - "pipecat.frame": fields, + "contrib.pipecat.frame.type": type(data.frame).__name__, + "contrib.pipecat.observer.timestamp": data.timestamp, + "contrib.pipecat.observer.source": data.source.name, + "contrib.pipecat.frame": compact_frame(fields), } size = len(json.dumps(event, ensure_ascii=False).encode("utf-8")) if len(self.events) < 256 and self.event_bytes + size < 65536: @@ -593,7 +637,7 @@ def capture_event(self, data, fields, owner): def complete_recording(self, state): if "last_pts" in state: - state["span"].log(metadata={"pipecat.frame.pts": state["last_pts"]}) + state["span"].log(metadata={"contrib.pipecat.frame.pts": state["last_pts"]}) if len(self.recordings) < 256: self.recordings.append(state) task = asyncio.create_task(self._publish_tts_clip(state)) @@ -659,7 +703,7 @@ async def _finalize(self): state["reason"] = "pipeline_closed_before_tts_stop" self.complete_recording(state) for owner, events in self.event_groups.values(): - owner.log(metadata={"pipecat.events": events}) + owner.log(metadata={"contrib.pipecat.events": events}) self.root.log( metadata={ "braintrust.capture.events_omitted": self.omitted_events, diff --git a/py/src/braintrust/integrations/pipecat/voice/realtime.py b/py/src/braintrust/integrations/pipecat/voice/realtime.py index a857ba277..b4688b12f 100644 --- a/py/src/braintrust/integrations/pipecat/voice/realtime.py +++ b/py/src/braintrust/integrations/pipecat/voice/realtime.py @@ -177,7 +177,7 @@ async def commit(*args, **kwargs): ids = [f.get("result", {}).get("item_id") for f in consumed if isinstance(f.get("result"), dict)] turn["span"].log( metadata={ - "pipecat.transcriptions": consumed, + "contrib.pipecat.transcriptions": consumed, "openai.item_ids": ids, "braintrust.user_capture.association": "aggregator_consumed_frames", } diff --git a/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py b/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py index 3255c15be..d334f704e 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py @@ -6,6 +6,7 @@ FunctionCallInProgressFrame, FunctionCallResultFrame, StartFrame, + STTMetadataFrame, TTSAudioRawFrame, TTSStartedFrame, TTSStoppedFrame, @@ -40,6 +41,9 @@ def flush(self): class Tests(unittest.IsolatedAsyncioTestCase): async def test_native_frames_and_audio_bounds(self): + from pipecat.audio.vad.vad_analyzer import VADParams # pylint: disable=import-error + from pipecat.frames.frames import VADParamsUpdateFrame # pylint: disable=import-error + logger = Span() observer = NativeObserver(logger, retain_audio=True, max_audio_bytes=10) @@ -56,6 +60,11 @@ async def push(frame, first_push=True): ) await push(StartFrame()) + await push(STTMetadataFrame(service_name="stt", ttfs_p99_latency=0.5)) + await push(STTMetadataFrame(service_name="stt", ttfs_p99_latency=0.5)) + self.assertEqual(observer.events, []) + await push(STTMetadataFrame(service_name="stt", ttfs_p99_latency=0.7)) + await push(VADParamsUpdateFrame(params=VADParams(confidence=0.8))) await push(TTSStartedFrame(context_id="c1")) frame = TTSAudioRawFrame(audio=b"\x01\x00" * 480, sample_rate=24000, num_channels=1, context_id="c1") await push(frame) @@ -66,7 +75,12 @@ async def push(frame, first_push=True): self.assertEqual(recording["state"], "omitted") self.assertEqual(observer.audio_bytes, 0) self.assertNotIn("audio", native_value(frame)) - self.assertEqual(observer.events[0]["pipecat.observer.timestamp"], 1234) + self.assertEqual(len(observer.events), 2) # Changed metadata and an explicit settings update. + event = observer.events[0] + self.assertEqual(event["contrib.pipecat.observer.timestamp"], 1234) + self.assertEqual(event["contrib.pipecat.frame"]["ttfs_p99_latency"], 0.7) + self.assertNotIn("id", event["contrib.pipecat.frame"]) + self.assertEqual(observer.events[1]["contrib.pipecat.frame.type"], "VADParamsUpdateFrame") async def test_repeated_tool_frames_keep_one_execution_span(self): observer = NativeObserver(Span(), retain_audio=True) diff --git a/py/src/braintrust/integrations/pipecat/voice/test_turns.py b/py/src/braintrust/integrations/pipecat/voice/test_turns.py index fbe372cdb..bb7831369 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_turns.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_turns.py @@ -86,10 +86,10 @@ async def test_tool_continuation_preserves_origin_after_next_user_starts(self): self.assertEqual(first_assistant["span"].rows[0]["name"], "assistant_turn") self.assertEqual(first_model.rows[0]["name"], "llm_response") self.assertIn(tool, first_model.children) - self.assertEqual(tool.rows[0]["metadata"]["pipecat.tool_call_id"], "call-1") + self.assertEqual(tool.rows[0]["metadata"]["contrib.pipecat.tool_call_id"], "call-1") self.assertEqual(final_output[0]["tool_calls"][0]["id"], "call-1") self.assertIsNone(final_output[0]["content"]) - self.assertFalse(any(row.get("metadata", {}).get("pipecat.text") == "" for row in first_model.rows)) + self.assertFalse(any(row.get("metadata", {}).get("contrib.pipecat.text") == "" for row in first_model.rows)) observer.turns.stop("assistant", SimpleNamespace(content="", timestamp="t2", interrupted=False)) await self.push(UserStartedSpeakingFrame()) await self.push( @@ -176,7 +176,7 @@ async def test_interruption_ends_tts_and_transcription_does_not_guess_a_turn(sel await self.push(InterruptionFrame()) self.assertEqual(self.observer.tts, {}) self.assertIn(state, self.observer.recordings) - self.assertEqual(state["span"].rows[-1]["metadata"]["pipecat.end_frame"], "InterruptionFrame") + self.assertEqual(state["span"].rows[-1]["metadata"]["contrib.pipecat.end_frame"], "InterruptionFrame") await self.observer.finish() async def test_native_turn_metrics_ownership_and_limits(self): @@ -201,10 +201,10 @@ async def test_native_turn_metrics_ownership_and_limits(self): await self.push(frame, self.user) await self.push(frame, self.user) # broadcast/repeated observations do not duplicate predictions metadata = {key: value for row in turn["span"].rows for key, value in row.get("metadata", {}).items()} - assert len(metadata["pipecat.turn_metrics"]) == 32 + assert len(metadata["contrib.pipecat.turn_metrics"]) == 32 assert metadata["braintrust.turn_metrics.omitted"] == 8 - assert metadata["pipecat.turn_metrics"][0]["processor"] == "BaseSmartTurn" - assert metadata["pipecat.turn_metrics"][20]["type"] == "SmartTurnMetricsData" + assert metadata["contrib.pipecat.turn_metrics"][0]["processor"] == "BaseSmartTurn" + assert metadata["contrib.pipecat.turn_metrics"][20]["type"] == "SmartTurnMetricsData" before = len(turn["span"].rows) await self.push( MetricsFrame( @@ -214,7 +214,9 @@ async def test_native_turn_metrics_ownership_and_limits(self): assert len(turn["span"].rows) == before observer.turns.stop("user", SimpleNamespace(content="hello", timestamp="t", user_id="u")) await self.push(UserStartedSpeakingFrame()) - assert not any("pipecat.turn_metrics" in row.get("metadata", {}) for row in observer.turns.user["span"].rows) + assert not any( + "contrib.pipecat.turn_metrics" in row.get("metadata", {}) for row in observer.turns.user["span"].rows + ) await observer.finish() async def test_ttfb_routes_only_to_unambiguous_processor_operation(self): @@ -226,14 +228,14 @@ async def test_ttfb_routes_only_to_unambiguous_processor_operation(self): await self.push(TTSStartedFrame(context_id="a"), tts) span = self.observer.tts["a"]["span"] await self.push(MetricsFrame(data=[TTFBMetricsData(processor="tts", value=0.09)]), tts) - assert span.rows[-1]["metadata"]["pipecat.ttfb"][0]["value"] == 0.09 + assert span.rows[-1]["metadata"]["contrib.pipecat.ttfb"][0]["value"] == 0.09 await self.push(TTSStartedFrame(context_id="b"), tts) await self.push(MetricsFrame(data=[TTFBMetricsData(processor="tts", value=0.11)]), tts) - assert self.observer.root.rows[-1]["metadata"]["pipecat.ttfb"][0]["value"] == 0.11 + assert self.observer.root.rows[-1]["metadata"]["contrib.pipecat.ttfb"][0]["value"] == 0.11 await self.push(TTSStoppedFrame(context_id="a"), tts) await self.push(TTSStoppedFrame(context_id="b"), tts) await self.push(MetricsFrame(data=[TTFBMetricsData(processor="tts", value=0.13)]), tts) - assert self.observer.root.rows[-1]["metadata"]["pipecat.ttfb"][-1]["value"] == 0.13 + assert self.observer.root.rows[-1]["metadata"]["contrib.pipecat.ttfb"][-1]["value"] == 0.13 # A speech-to-speech processor can have model and audio operations open # simultaneously. Usage must select the model operation, not become ambiguous. from pipecat.metrics.metrics import LLMTokenUsage, LLMUsageMetricsData # pylint: disable=import-error diff --git a/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py b/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py index 34bfcee31..354cd36d5 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py @@ -88,7 +88,7 @@ async def measured_run(audio): self.assertEqual(len(capture.batches[turn["span"].span_id]["segments"][0]["boundaries"]), 2) stt_span = capture.batches[turn["span"].span_id]["segments"][0]["span"] metadata = {key: value for row in stt_span.rows for key, value in row.get("metadata", {}).items()} - self.assertEqual(metadata["pipecat.ttfb"][0]["value"], 0.12) + self.assertEqual(metadata["contrib.pipecat.ttfb"][0]["value"], 0.12) return observer, capture, turn async def test_opt_out_keeps_metadata_without_retaining_encoding_or_attaching_audio( @@ -109,7 +109,7 @@ async def test_opt_out_keeps_metadata_without_retaining_encoding_or_attaching_au turn["span"].rows[-1]["metadata"]["audio.recordings"][0]["reason"], "disabled", ) - self.assertTrue(any("pipecat.transcriptions" in row.get("metadata", {}) for row in turn["span"].rows)) + self.assertTrue(any("contrib.pipecat.transcriptions" in row.get("metadata", {}) for row in turn["span"].rows)) async def test_user_audio_limit_is_explicit(self): observer, capture, turn = await self.exercise(True, max_bytes=10) diff --git a/py/src/braintrust/integrations/pipecat/voice/turns.py b/py/src/braintrust/integrations/pipecat/voice/turns.py index f3e8f18c8..135399a93 100644 --- a/py/src/braintrust/integrations/pipecat/voice/turns.py +++ b/py/src/braintrust/integrations/pipecat/voice/turns.py @@ -28,7 +28,7 @@ def start(self, role, trigger, reply_to=None): type="task", set_current=False, internal={"instrumentation": "pipecat-auto"}, - metadata={"pipecat.turn.start_observation": trigger}, + metadata={"contrib.pipecat.turn.start_observation": trigger}, ), "role": role, "confirmed": False, @@ -71,9 +71,9 @@ async def user_message(aggregator, message): self.log_message(state, message.content) state["span"].log( metadata={ - "pipecat.content": message.content, - "pipecat.timestamp": message.timestamp, - "pipecat.user_id": message.user_id, + "contrib.pipecat.content": message.content, + "contrib.pipecat.timestamp": message.timestamp, + "contrib.pipecat.user_id": message.user_id, } ) @@ -94,9 +94,9 @@ def confirm(self, role, strategy=None): if state is None: state = self.start(role, f"on_{role}_turn_started") state["confirmed"] = True - metadata = {"pipecat.turn.start_event": f"on_{role}_turn_started"} + metadata = {"contrib.pipecat.turn.start_event": f"on_{role}_turn_started"} if strategy: - metadata["pipecat.turn.start_strategy"] = strategy + metadata["contrib.pipecat.turn.start_strategy"] = strategy state["span"].log(metadata=metadata) def stop(self, role, message, strategy=None): @@ -104,15 +104,15 @@ def stop(self, role, message, strategy=None): return state = self.pending[role].popleft() metadata = { - "pipecat.turn.stop_event": f"on_{role}_turn_stopped", - "pipecat.content": message.content, - "pipecat.timestamp": message.timestamp, + "contrib.pipecat.turn.stop_event": f"on_{role}_turn_stopped", + "contrib.pipecat.content": message.content, + "contrib.pipecat.timestamp": message.timestamp, } for field in ("user_id", "interrupted"): if hasattr(message, field): - metadata[f"pipecat.{field}"] = getattr(message, field) + metadata[f"contrib.pipecat.{field}"] = getattr(message, field) if strategy: - metadata["pipecat.turn.stop_strategy"] = strategy + metadata["contrib.pipecat.turn.stop_strategy"] = strategy state["span"].log(metadata=metadata) self.log_message(state, message.content) state["span"].end() diff --git a/py/src/braintrust/integrations/pipecat/voice/user_capture.py b/py/src/braintrust/integrations/pipecat/voice/user_capture.py index 05da5d590..d09cf01de 100644 --- a/py/src/braintrust/integrations/pipecat/voice/user_capture.py +++ b/py/src/braintrust/integrations/pipecat/voice/user_capture.py @@ -70,8 +70,8 @@ async def process_audio(frame, direction): async def speech_start(frame): self.boundaries = [ { - "pipecat.frame.type": type(frame).__name__, - "pipecat.frame": native_value(frame), + "contrib.pipecat.frame.type": type(frame).__name__, + "contrib.pipecat.frame": native_value(frame), } ] return await original_start(frame) @@ -84,8 +84,8 @@ async def speech_stop(frame): "events": [ *self.boundaries, { - "pipecat.frame.type": type(frame).__name__, - "pipecat.frame": native_value(frame), + "contrib.pipecat.frame.type": type(frame).__name__, + "contrib.pipecat.frame": native_value(frame), }, ], } @@ -182,8 +182,8 @@ async def commit(*args, **kwargs): self.create_stt(segment, owner, turn) owner.log( metadata={ - "pipecat.transcriptions": [frame for frame, _ in accepted], - "pipecat.speech_events": [event for s in segments for event in s["boundaries"]], + "contrib.pipecat.transcriptions": [frame for frame, _ in accepted], + "contrib.pipecat.speech_events": [event for s in segments for event in s["boundaries"]], "braintrust.user_capture.association": "aggregator_consumed_frames", } ) @@ -198,8 +198,10 @@ async def commit(*args, **kwargs): batch["segments"].append(segment) owner.log( metadata={ - "pipecat.transcriptions": batch["frames"], - "pipecat.speech_events": [event for s in batch["segments"] for event in s["boundaries"]], + "contrib.pipecat.transcriptions": batch["frames"], + "contrib.pipecat.speech_events": [ + event for s in batch["segments"] for event in s["boundaries"] + ], } ) if turn: @@ -225,8 +227,8 @@ def create_stt(self, segment, owner, turn=None): if segment["span"] is not None: return metadata = { - "pipecat.transcriptions": segment["frames"], - "pipecat.function": "run_stt", + "contrib.pipecat.transcriptions": segment["frames"], + "contrib.pipecat.function": "run_stt", **segment.get("service_metadata", {}), **segment.get("ttfb_metadata", {}), } From ae0a630da4b3a22f297ecfbc6779c3d11b6d00db Mon Sep 17 00:00:00 2001 From: David Elner Date: Tue, 6 Oct 2026 13:13:46 -0400 Subject: [PATCH 13/18] Fixed: TTS early arrival, buffer limits --- .../integrations/pipecat/test_pipecat.py | 34 ++++++ .../pipecat/test_voice_pipeline.py | 42 ++++++- .../braintrust/integrations/pipecat/ttfb.py | 59 +++++++++- .../integrations/pipecat/voice/README.md | 6 +- .../integrations/pipecat/voice/discovery.py | 1 + .../pipecat/voice/instrumentation.py | 19 +++- .../integrations/pipecat/voice/realtime.py | 106 ++++++++++++------ .../integrations/pipecat/voice/test_hooks.py | 52 +++++++++ .../pipecat/voice/test_realtime.py | 37 ++++++ .../integrations/pipecat/voice/test_turns.py | 7 +- .../pipecat/voice/test_user_capture.py | 43 ++++++- .../integrations/pipecat/voice/tts_metrics.py | 44 ++++++++ .../pipecat/voice/user_capture.py | 25 ++++- 13 files changed, 419 insertions(+), 56 deletions(-) create mode 100644 py/src/braintrust/integrations/pipecat/voice/tts_metrics.py diff --git a/py/src/braintrust/integrations/pipecat/test_pipecat.py b/py/src/braintrust/integrations/pipecat/test_pipecat.py index 040c7a189..768e5bde5 100644 --- a/py/src/braintrust/integrations/pipecat/test_pipecat.py +++ b/py/src/braintrust/integrations/pipecat/test_pipecat.py @@ -387,3 +387,37 @@ def test_ttfb_router_bounds_and_rejects_mismatched_sources(): assert router.capture(metric, source) is None router.clear() assert not router.active + + +def test_request_metrics_keep_identity_across_overlap_shutdown_and_limits(): + from braintrust.integrations.pipecat.ttfb import TTFBRouter + + usage = _import("pipecat.metrics.metrics.TTSUsageMetricsData") + root, first, second = [], [], [] + source = SimpleNamespace(name="tts") + router = TTFBRouter(lambda **row: root.append(row)) + a, b = ("tts", "a"), ("tts", "b") + router.start(b, source, lambda **row: second.append(row)) + router.capture_request(usage(processor="tts", value=11), source, a) + assert not root and not second # An active sibling is not the request's owner. + router.start(a, source, lambda **row: first.append(row)) + router.capture_request(usage(processor="tts", value=22), source, b) + router.end(a) + router.capture_request(usage(processor="tts", value=33), source, a) + assert [m["value"] for m in first[-1]["metadata"]["contrib.pipecat.measurements"]] == [11, 33] + assert [m["value"] for m in second[-1]["metadata"]["contrib.pipecat.measurements"]] == [22] + router.capture_request(usage(processor="tts", value=44), SimpleNamespace(name="tts"), b) + assert root[-1]["metadata"]["contrib.pipecat.measurements"][0]["value"] == 44 + # Requests that never produce audio, including cancellation, cannot grow + # retention without bound or be silently dropped at shutdown. + for index in range(300): + router.capture_request(usage(processor="tts", value=index), source, ("tts", str(index))) + assert len(router.pending) == 256 + for index in range(100): + key = ("tts", f"completed-{index}") + router.start(key, source, lambda **row: None) + router.end(key) + assert len(router.completed) == 64 + router.clear() + assert not router.pending and not router.completed and not router.active + assert root[-1]["metadata"]["braintrust.measurements.omitted"] == 269 diff --git a/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py b/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py index ffb6cd8be..f48e9a4b9 100644 --- a/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py +++ b/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py @@ -175,7 +175,8 @@ def _assert_shutdown_audio(output, root, recordings): @pytest.mark.vcr(match_on=["method", "uri", "voice_payload"]) @pytest.mark.asyncio -async def test_cascade_voice_conversation(memory_logger, request): +@pytest.mark.parametrize("early_metrics", [False, True]) +async def test_cascade_voice_conversation(memory_logger, request, monkeypatch, early_metrics): setup_pipecat(capture_audio_attachments=True, audio_format="wav", recording_options=RecordingOptions()) init_test_logger("test-project-pipecat-py-tracing") tool_calls = [] @@ -214,6 +215,24 @@ async def lookup_order(params): api_key=key, settings=OpenAITTSService.Settings(model="gpt-4o-mini-tts", voice="alloy"), ) + if early_metrics: + # Exercise native audio-queue scheduling without replacing the service, + # metric emission, or provider responses. + usage_emitted = asyncio.Event() + original_push = tts.push_frame + + async def delayed_start(frame, *args, **kwargs): + if type(frame).__name__ == "TTSStartedFrame": + await usage_emitted.wait() # Covered by the conversation's overall deadline. + await asyncio.sleep(0) + result = await original_push(frame, *args, **kwargs) + if type(frame).__name__ == "MetricsFrame" and any( + type(metric).__name__ == "TTSUsageMetricsData" for metric in frame.data + ): + usage_emitted.set() + return result + + monkeypatch.setattr(tts, "push_frame", delayed_start) worker = _make_worker( Pipeline([source, stt, pair.user(), llm, tts, output, pair.assistant()]), params=PipelineParams( @@ -251,10 +270,23 @@ async def feed(worker, frame): recognition = _single_span(rows, "stt") tool = _single_span(rows, "lookup_order") synthesis = _single_span(rows, "tts") - measurements = synthesis["metadata"]["contrib.pipecat.measurements"] - assert any(m["type"] == "TTSUsageMetricsData" for m in measurements) - assert any(m["type"] == "ProcessingMetricsData" for m in measurements) - assert all(m["processor"].startswith("OpenAITTSService") for m in measurements) + # Request identity must survive early metrics, without root duplicates. + tts_measurements = [ + (row, measurement) + for row in rows + for measurement in row.get("metadata", {}).get("contrib.pipecat.measurements", []) + if measurement["processor"].startswith("OpenAITTSService") + ] + for metric_type in ("TTSUsageMetricsData", "ProcessingMetricsData"): + matching = [(row, metric) for row, metric in tts_measurements if metric["type"] == metric_type] + assert len(matching) == 1 + owner, metric = matching[0] + assert owner["span_id"] == synthesis["span_id"] + assert metric["value"] > 0 + assert all( + metric["processor"].startswith("OpenAITTSService") + for metric in synthesis.get("metadata", {}).get("contrib.pipecat.measurements", []) + ) for row in rows: assert not any(key.startswith("pipecat.") for key in row.get("metadata", {})) events = row.get("metadata", {}).get("contrib.pipecat.events", []) diff --git a/py/src/braintrust/integrations/pipecat/ttfb.py b/py/src/braintrust/integrations/pipecat/ttfb.py index 6bea0b33f..a2a8bc18d 100644 --- a/py/src/braintrust/integrations/pipecat/ttfb.py +++ b/py/src/braintrust/integrations/pipecat/ttfb.py @@ -1,4 +1,6 @@ -"""Associate reported TTFB only with an unambiguous live processor operation.""" +"""Route native measurements by request identity or an unambiguous live operation.""" + +from collections import OrderedDict, deque class TTFBRouter: @@ -6,18 +8,58 @@ def __init__(self, fallback): self.fallback = fallback self.active = {} self.unmatched = {} + self.pending = deque() + self.completed = OrderedDict() def start(self, key, processor, log): + self.completed.pop(key, None) # Repeated starts without a matching end cannot identify one operation. if key in self.active: processor = None self.active[key] = (processor, log, {}) + pending, self.pending = self.pending, deque() + for metric, source, request in pending: + if request == key: + self.capture_request(metric, source, request) + else: + self.pending.append((metric, source, request)) def end(self, key): - self.active.pop(key, None) + operation = self.active.pop(key, None) + if operation and isinstance(key, tuple) and key[0] == "tts": + self.completed[key] = operation + if len(self.completed) > 64: + self.completed.popitem(last=False) def clear(self): + while self.pending: + metric, _, _ = self.pending.popleft() + self._capture_request(metric, (None, self.fallback, self.unmatched)) self.active.clear() + self.completed.clear() + + def capture_request(self, metric, source, key): + """Use emission-time identity, including before start or after stop.""" + operation = self.active.get(key) or self.completed.get(key) + if operation: + processor, log, state = operation + owner = ( + (key, log, state) + if processor is source and metric.processor == source.name + else (None, self.fallback, self.unmatched) + ) + self._capture_request(metric, owner) + else: + if len(self.pending) >= 256: + oldest, _, _ = self.pending.popleft() + self._capture_request(oldest, (None, self.fallback, self.unmatched)) + self.pending.append((metric, source, key)) + + def _capture_request(self, metric, owner): + if type(metric).__name__ == "TTFBMetricsData": + self._capture_ttfb(metric, owner) + else: + self._capture_measurement(metric, owner) def owner(self, metric, source, *, operation=None): name = getattr(metric, "processor", None) @@ -34,8 +76,10 @@ def owner(self, metric, source, *, operation=None): return None, self.fallback, self.unmatched def capture(self, metric, source): - key, log, state = self.owner(metric, source) - name = getattr(metric, "processor", None) + return self._capture_ttfb(metric, self.owner(metric, source)) + + def _capture_ttfb(self, metric, owner): + key, log, state = owner values = state.setdefault("values", []) if len(values) >= 32: state["omitted"] = state.get("omitted", 0) + 1 @@ -44,7 +88,7 @@ def capture(self, metric, source): payload = ( metric.model_dump(mode="json") if hasattr(metric, "model_dump") - else {"processor": name, "value": metric.value} + else {"processor": getattr(metric, "processor", None), "value": metric.value} ) values.append(payload) log(metadata={"contrib.pipecat.ttfb": list(values)}) @@ -52,7 +96,10 @@ def capture(self, metric, source): def capture_measurement(self, metric, source): """Retain native measurements without a frame/routing envelope.""" - _, log, state = self.owner(metric, source) + self._capture_measurement(metric, self.owner(metric, source)) + + def _capture_measurement(self, metric, owner): + _, log, state = owner values = state.setdefault("measurements", []) if len(values) >= 32: state["measurements_omitted"] = state.get("measurements_omitted", 0) + 1 diff --git a/py/src/braintrust/integrations/pipecat/voice/README.md b/py/src/braintrust/integrations/pipecat/voice/README.md index 3512c93b1..4ad5309c7 100644 --- a/py/src/braintrust/integrations/pipecat/voice/README.md +++ b/py/src/braintrust/integrations/pipecat/voice/README.md @@ -20,14 +20,16 @@ Install `braintrust[audio]` for Ogg/Opus or WAV encoding. Audio defaults off. Ex | Public interfaces | `setup_pipecat`, automatic integration setup, `wrap_pipeline_worker`, and an explicitly supplied `BraintrustPipecatObserver` share the same injection path. `trace_turns=False` retains the existing observer behavior. | | Trace | `user_turn`, `assistant_turn`, `stt`, `llm_response`, `tts`, and actual tool names retain native boundaries and metadata. Realtime audio uses `pipecat.audio_output`; tool-only model responses can be pipeline children. `turn.id` / `turn.reply_to` and `continuation.tool_call_ids` preserve observed associations. | | Audio | Combined stereo call; segmented-STT caller clips on user turns; generated clips on TTS spans. Selections reference successfully captured samples. | -| TTFB | Native `contrib.pipecat.ttfb` measurements follow the emitting processor and a unique active operation. STT measurements follow `run_stt` into its eventual span. Ambiguous/unmatched metrics remain on the pipeline; arrays are bounded to 32 with omission counts. | +| TTFB | Native `contrib.pipecat.ttfb` measurements follow TTS request identity when available, otherwise the emitting processor's unique active operation. STT measurements follow `run_stt` into its eventual span. Ambiguous/unmatched metrics remain on the pipeline; arrays are bounded to 32 with omission counts. | | Turn detection | Native analyzer predictions in `contrib.pipecat.turn_metrics` on the user turn; preserve processor, metric class, confidence, completion and reported milliseconds. Retain 32 predictions and count omissions. Unassociated observations remain on the pipeline. | -| Measurements | `contrib.pipecat.measurements` retains native metric types and payloads on the emitting processor's unique active operation. Unattributable measurements remain on the pipeline. Each list retains at most 32 entries with `braintrust.measurements.omitted` counting excess observations. Metrics are not duplicated as frame events. | +| Measurements | `contrib.pipecat.measurements` retains native metric types and payloads on their operation. TTS metrics emitted inside synthesis follow the native request/context ID, including before queued audio starts or after it stops. Other measurements require a unique active operation; unattributable measurements remain on the pipeline. Each list retains at most 32 entries with `braintrust.measurements.omitted` counting excess observations. Metrics are not duplicated as frame events. | +| Request routing | At most 256 early metrics wait for their matching TTS span; 64 recently closed TTS targets accept late delivery. Overflow or requests that never open a span fall back to pipeline metadata. Request tags are private, consumed before export, and carry no audio. Hooks are removed at shutdown. | | Events and configuration | Initial service/configuration observations are stored once in `contrib.pipecat.configuration` (up to 32 source/type pairs). Changes, speech boundaries, interruptions, cancellation, and errors retain compact timestamped events; routine start/end/connection notifications and startup zero metrics are excluded. Event payloads omit transport bookkeeping and empty fields. The pipeline-wide event budget remains 256 entries / 64 KiB. | | Input alignment | Segmented STT input provenance or successful OpenAI input writes plus server-VAD item offsets. Clears/reconnects invalidate unsupported mappings. | | Output alignment | Equal-rate mono PCM without a mixer. Other paths retain recordings without asserting unsupported selections. | | Memory | Configurable 32 MiB retained call PCM by default; separate 8 MiB caller/generated source limits; 64 MiB shared source budget; 32 MiB encoded-attachment budget. These are retained-data bounds, not total RSS bounds. | | Recording limits | Rotate call segments every 60 seconds or at half the call buffer limit; stop capture at 30 minutes by default. Configure these with `RecordingOptions`. Completed segments and valid prefixes survive limits. | +| Pending capture | Completed caller clips release their batch and source bytes. Realtime turn associations retire when published; adjacent input sample ranges compact, and queued output frames carry their own context. Buffer capacity is reusable throughout the call. | | Admission | One encoder running, one queued; bounded packet/clip metadata. Overload produces an omission reason. | | Cleanup | Seal capture, restore installed hooks, end operation spans, encode off-loop, update attachments, then flush. Repeated finalization shares one task; cancelling a waiter does not cancel its work. | | Privacy | Standard span customizers apply to native metadata, messages, tool arguments/results, and attachment updates. Audio bytes are excluded from event metadata. | diff --git a/py/src/braintrust/integrations/pipecat/voice/discovery.py b/py/src/braintrust/integrations/pipecat/voice/discovery.py index 1ebe0eb96..2b8f5db70 100644 --- a/py/src/braintrust/integrations/pipecat/voice/discovery.py +++ b/py/src/braintrust/integrations/pipecat/voice/discovery.py @@ -27,4 +27,5 @@ def discover(pipeline): assistant_aggregator=assistant, stt=stts[0] if stts else None, realtime_service=realtime[0] if realtime else None, + tts_services=[processor for processor in processors if _is(processor, "TTSService")], ) diff --git a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py index 0745e0ba1..f3b98fd7e 100644 --- a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py +++ b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py @@ -156,7 +156,6 @@ def __init__( ) self.call_descriptors = {} self.clip_tasks = set() - self.frame_contexts = {} self.realtime = None self.input_processor = None self.capture_transport = False @@ -164,6 +163,7 @@ def __init__( root if root is not None else logger.start_span(name="pipecat.pipeline", type="task", set_current=False) ) self.ttfb = TTFBRouter(self.root.log) + self.tts_requests = None self.turns = Turns(self.root, self.hooks) self.turns.on_completed = self._turn_completed self.alignment = Alignment(self.root, self.call_recording) @@ -208,7 +208,9 @@ def __del__(self): source_budget.release(retained) self.audio_bytes = 0 - def bind(self, *, transport, user_aggregator, assistant_aggregator, stt=None, realtime_service=None): + def bind( + self, *, transport, user_aggregator, assistant_aggregator, stt=None, realtime_service=None, tts_services=() + ): """Bind one supported pipeline before it starts; does not alter routing.""" import importlib.metadata @@ -219,6 +221,9 @@ def bind(self, *, transport, user_aggregator, assistant_aggregator, stt=None, re if (stt is None) == (realtime_service is None): raise ValueError("Bind either segmented STT or an OpenAI realtime service") self.turns.install(user_aggregator, assistant_aggregator) + from .tts_metrics import TTSRequests + + self.tts_requests = TTSRequests(self.hooks, tts_services) self.user_aggregator, self.assistant_aggregator = user_aggregator, assistant_aggregator self.capture_transport = True if stt is not None: @@ -238,7 +243,10 @@ def bind(self, *, transport, user_aggregator, assistant_aggregator, stt=None, re return self def frame_context(self, frame): - return self.frame_contexts.get(frame.id, getattr(frame, "context_id", None)) + identity = getattr(frame, "_braintrust_realtime_context", None) + if identity and self.realtime and identity[0] is self.realtime.frame_token: + return identity[1] + return getattr(frame, "context_id", None) async def on_push_frame(self, data): if not data.first_push or self.finished: @@ -326,6 +334,11 @@ async def on_push_frame(self, data): fields = native_value(frame) if kind != "LLMContextFrame" else {} event_owner = self.root if kind == "MetricsFrame": + request = self.tts_requests.take(frame, data.source) if self.tts_requests else None + if request is not None: + for metric in frame.data: + self.ttfb.capture_request(metric, data.source, request) + return # Pipeline startup broadcasts zero placeholders for every service. if frame.data and all( getattr(metric, "value", None) == 0 diff --git a/py/src/braintrust/integrations/pipecat/voice/realtime.py b/py/src/braintrust/integrations/pipecat/voice/realtime.py index b4688b12f..259c7616b 100644 --- a/py/src/braintrust/integrations/pipecat/voice/realtime.py +++ b/py/src/braintrust/integrations/pipecat/voice/realtime.py @@ -17,8 +17,10 @@ def __init__(self, observer, service, aggregator): self.consumed = [] self.associations = [] self.sent_samples = 0 - self.sent_ranges = [] + self.sent_ranges: list[tuple[int, int, dict[str, int]]] = [] self.invalid = False + self.omitted = 0 + self.frame_token = object() self.active = ContextVar("realtime_response", default=None) self.service = service self.user_item = None @@ -37,7 +39,6 @@ async def user_started(aggregator, strategy): self.user_turns[self.user_item] = observer.turns.user if len(self.user_turns) > 256: self.user_turns.pop(next(iter(self.user_turns))) - self.invalid = True observer.turns.user["span"].log(metadata={"openai.item_id": self.user_item}) observer.hooks.event(aggregator, "on_user_turn_started", user_started) @@ -76,10 +77,8 @@ async def push(frame, *args, **kwargs): "TTSAudioRawFrame", "TTSTextFrame", }: - if len(observer.frame_contexts) < 16000: - observer.frame_contexts[frame.id] = context - else: - self.invalid = True + # Keep provenance on the queued frame, not in a call-long ID map. + frame._braintrust_realtime_context = (self.frame_token, context) return await original_push(frame, *args, **kwargs) observer.hooks.set(service, "push_frame", push) @@ -147,9 +146,13 @@ async def response(evt, original=original): async def speech(evt, original=original, field=field): if field == "audio_start_ms": self.user_item = evt.item_id - if len(self.items) < 256 or evt.item_id in self.items: - self.items.setdefault(evt.item_id, {})[field] = getattr(evt, field) - return await original(evt) + if evt.item_id not in self.items and len(self.items) >= 256: + self.items.pop(next(iter(self.items))) + self.omitted += 1 + self.items.setdefault(evt.item_id, {})[field] = getattr(evt, field) + result = await original(evt) + self.publish() + return result observer.hooks.set(service, name, speech) @@ -182,10 +185,11 @@ async def commit(*args, **kwargs): "braintrust.user_capture.association": "aggregator_consumed_frames", } ) - if len(self.associations) < 256: - self.associations.append((turn["span"], ids)) - else: - self.invalid = True + if len(self.associations) >= 256: + self.associations.pop(0) + self.omitted += 1 + self.associations.append((turn["span"], ids)) + self.publish() observer.context_reply_to = turn["span"].span_id return result @@ -233,11 +237,25 @@ def record_sent(frame): observer.call_recording.omit("capture_error") interval = None size = len(frame.audio) // 2 - if interval and len(self.sent_ranges) < 16000: - self.sent_ranges.append((self.sent_samples, self.sent_samples + size, interval)) - else: - self.invalid = True + if interval and not self.invalid: + merged = False + if self.sent_ranges: + start, end, previous = self.sent_ranges[-1] + if end == self.sent_samples and previous["end"] == interval["start"]: + self.sent_ranges[-1] = ( + start, + self.sent_samples + size, + {"start": previous["start"], "end": interval["end"]}, + ) + merged = True + if not merged: + if len(self.sent_ranges) >= 16000: + self.sent_ranges.pop(0) + self.omitted += 1 + self.sent_ranges.append((self.sent_samples, self.sent_samples + size, interval)) self.sent_samples += size + if self.associations: + self.publish() async def send_audio(frame): token = audio_frame.set(frame) @@ -250,28 +268,52 @@ async def send_audio(frame): async def send_event(evt): if evt.type == "input_audio_buffer.clear": + self.publish() self.invalid = True return await original_event(evt) observer.hooks.set(service, "_send_user_audio", send_audio) observer.hooks.set(service, "send_client_event", send_event) - def finish(self): + def publish(self, final=False): + pending = [] + completed = set() for span, ids in self.associations: events = [{"item_id": item, **self.items.get(item, {})} for item in ids] - span.log(metadata={"openai.input_audio_segments": events}) - if not self.observer.capture_user_audio or self.invalid: + ready = all("audio_start_ms" in event and "audio_end_ms" in event for event in events) + if ready and self.observer.capture_user_audio and not self.invalid: + ready = all(round(event["audio_end_ms"] * 24) <= self.sent_samples for event in events) + if not ready and not final: + pending.append((span, ids)) continue - for event in events: - if "audio_start_ms" not in event or "audio_end_ms" not in event: - continue - start, end = round(event["audio_start_ms"] * 24), round(event["audio_end_ms"] * 24) - ranges = [] - for a, b, interval in self.sent_ranges: - left, right = max(start, a), min(end, b) - if right > left: - ranges.append([interval["start"] + left - a, interval["start"] + right - a]) - if sum(b - a for a, b in ranges) == end - start: - self.observer.alignment.add(span, ranges, 0) - self.observer.root.log(metadata={"braintrust.realtime.alignment_invalidated": self.invalid}) + span.log(metadata={"openai.input_audio_segments": events}) + if self.observer.capture_user_audio and not self.invalid: + for event in events: + if "audio_start_ms" not in event or "audio_end_ms" not in event: + continue + start, end = round(event["audio_start_ms"] * 24), round(event["audio_end_ms"] * 24) + ranges = [] + for a, b, interval in self.sent_ranges: + left, right = max(start, a), min(end, b) + if right > left: + ranges.append([interval["start"] + left - a, interval["start"] + right - a]) + if sum(b - a for a, b in ranges) == end - start: + self.observer.alignment.add(span, ranges, 0) + else: + self.omitted += 1 + completed.update(ids) + for item in completed.difference(item for _, ids in pending for item in ids): + self.items.pop(item, None) + self.associations = pending + + def finish(self): + self.publish(final=True) + self.observer.root.log( + metadata={ + "braintrust.realtime.alignment_invalidated": self.invalid, + "braintrust.realtime.associations_omitted": self.omitted, + } + ) self.sent_ranges.clear() + self.items.clear() + self.user_turns.clear() diff --git a/py/src/braintrust/integrations/pipecat/voice/test_hooks.py b/py/src/braintrust/integrations/pipecat/voice/test_hooks.py index 69ef094e3..4c1fd6637 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_hooks.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_hooks.py @@ -34,3 +34,55 @@ async def observed(value): assert target.calls == 1 hooks.close() assert "run" not in target.__dict__ + + +@pytest.mark.asyncio +async def test_tts_request_tags_are_task_local_and_hooks_restore_on_cancellation(): + import asyncio + from types import SimpleNamespace + + from pipecat.frames.frames import MetricsFrame # pylint: disable=import-error + from pipecat.metrics.metrics import TTSUsageMetricsData # pylint: disable=import-error + + from .tts_metrics import TTSRequests + + both = asyncio.Event() + entered = [] + tagged = [] + + class Service: + async def _push_tts_frames(self, frame): + entered.append(frame.context_id) + if len(entered) == 2: + both.set() + await both.wait() + metric = MetricsFrame(data=[TTSUsageMetricsData(processor="tts", value=1)]) + await self.push_frame(metric) + assert requests.take(metric, self) == ("tts", frame.context_id) + assert requests.take(metric, self) is None # Consumed once, never exported. + detached = MetricsFrame(data=[]) + await asyncio.create_task(self.push_frame(detached)) + assert requests.take(detached, self) is None + tagged.append(frame.context_id) + if frame.context_id == "cancelled": + raise asyncio.CancelledError() + + async def push_frame(self, frame): + await asyncio.sleep(0) + + service, hooks = Service(), Hooks() + requests = TTSRequests(hooks, [service]) + try: + outcomes = await asyncio.gather( + service._push_tts_frames(SimpleNamespace(context_id="first")), + service._push_tts_frames(SimpleNamespace(context_id="cancelled")), + return_exceptions=True, + ) + assert sorted(tagged) == ["cancelled", "first"] + assert outcomes[0] is None and isinstance(outcomes[1], asyncio.CancelledError) + unassociated = MetricsFrame(data=[]) + await service.push_frame(unassociated) + assert requests.take(unassociated, service) is None + finally: + hooks.close() + assert "push_frame" not in service.__dict__ and "_push_tts_frames" not in service.__dict__ diff --git a/py/src/braintrust/integrations/pipecat/voice/test_realtime.py b/py/src/braintrust/integrations/pipecat/voice/test_realtime.py index ca1cd9cf8..25c152855 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_realtime.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_realtime.py @@ -76,3 +76,40 @@ async def test_disabled_does_not_wrap_connect_or_audio_send(self): await service._send_user_audio(InputAudioRawFrame(b"\0\0" * 480, 24000, 1)) self.assertEqual(capture.sent_ranges, []) self.assertEqual(observer.call_recording.bytes, 0) + + async def test_continuous_input_outlives_frame_limit_and_preserves_earlier_alignment(self): + from braintrust.audio import RecordingOptions + + observer = NativeObserver( + Span(), retain_audio=True, recording_options=RecordingOptions(segment_duration_seconds=600) + ) + service, aggregator = self.service() + capture = RealtimeCapture(observer, service, aggregator) + observer.realtime = capture + + async def send(message): + pass + + service._websocket = SimpleNamespace(send=send) + await service._connect() + frame = InputAudioRawFrame(b"\0\0" * 480, 24000, 1) + try: + for _ in range(16001): + await service._send_user_audio(frame) + self.assertFalse(capture.invalid) + self.assertEqual(len(capture.sent_ranges), 1) + for index in range(270): + turn = Span() + item_id = str(index) + capture.items[item_id] = {"audio_start_ms": index * 20, "audio_end_ms": (index + 1) * 20} + capture.associations.append((turn, [item_id])) + capture.publish() + self.assertIn(turn.span_id, observer.alignment.owners) + self.assertEqual(capture.items, {}) + self.assertEqual(capture.associations, []) + await service.send_client_event(SimpleNamespace(type="input_audio_buffer.clear")) + capture.finish() + self.assertIn(turn.span_id, observer.alignment.owners) + finally: + observer.hooks.close() + observer.call_recording.clear() diff --git a/py/src/braintrust/integrations/pipecat/voice/test_turns.py b/py/src/braintrust/integrations/pipecat/voice/test_turns.py index bb7831369..140f0369f 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_turns.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_turns.py @@ -221,12 +221,17 @@ async def test_native_turn_metrics_ownership_and_limits(self): async def test_ttfb_routes_only_to_unambiguous_processor_operation(self): from pipecat.frames.frames import MetricsFrame, TTSStoppedFrame # pylint: disable=import-error - from pipecat.metrics.metrics import TTFBMetricsData # pylint: disable=import-error + from pipecat.metrics.metrics import TTFBMetricsData, TTSUsageMetricsData # pylint: disable=import-error llm, tts = SimpleNamespace(name="llm"), SimpleNamespace(name="tts") await self.push(LLMFullResponseStartFrame(), llm) + # The usage frame can beat the queued TTSStartedFrame to the observer. + await self.push(MetricsFrame(data=[TTSUsageMetricsData(processor="tts", value=25)]), tts) + early_usage = self.observer.root.rows[-1]["metadata"]["contrib.pipecat.measurements"] + assert early_usage == [{"type": "TTSUsageMetricsData", "processor": "tts", "model": None, "value": 25}] await self.push(TTSStartedFrame(context_id="a"), tts) span = self.observer.tts["a"]["span"] + assert not any("contrib.pipecat.measurements" in row.get("metadata", {}) for row in span.rows) await self.push(MetricsFrame(data=[TTFBMetricsData(processor="tts", value=0.09)]), tts) assert span.rows[-1]["metadata"]["contrib.pipecat.ttfb"][0]["value"] == 0.09 await self.push(TTSStartedFrame(context_id="b"), tts) diff --git a/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py b/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py index 354cd36d5..ff28726a8 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py @@ -78,17 +78,17 @@ async def measured_run(audio): turn = observer.turns.start("user", "UserStartedSpeakingFrame") observer.turns.confirm("user") await aggregator._push_aggregation() - observer.turns.stop( - "user", - SimpleNamespace(content="Where is my order?", timestamp="t1", user_id="user"), - ) - await asyncio.sleep(0) self.assertEqual(context.get_messages()[-1]["content"], "Where is my order?") self.assertEqual(capture.batches[turn["span"].span_id]["frames"][0]["id"], frame.id) self.assertEqual(len(capture.batches[turn["span"].span_id]["segments"][0]["boundaries"]), 2) stt_span = capture.batches[turn["span"].span_id]["segments"][0]["span"] metadata = {key: value for row in stt_span.rows for key, value in row.get("metadata", {}).items()} self.assertEqual(metadata["contrib.pipecat.ttfb"][0]["value"], 0.12) + observer.turns.stop( + "user", + SimpleNamespace(content="Where is my order?", timestamp="t1", user_id="user"), + ) + await asyncio.sleep(0) return observer, capture, turn async def test_opt_out_keeps_metadata_without_retaining_encoding_or_attaching_audio( @@ -157,3 +157,36 @@ async def test_observation_failure_does_not_drop_native_transcription(self): self.assertEqual(frames[0].text, "Where is my order?") self.assertEqual(capture.omitted, 1) await observer.finish() + + async def test_completed_turns_release_capacity_for_later_clips(self): + observer = NativeObserver(Span(), retain_audio=True, audio_format="wav") + aggregator = LLMContextAggregatorPair(LLMContext()).user() + + async def push_context(): + pass + + aggregator.push_context_frame = push_context + stt = STT() + capture = UserCapture(observer, stt, aggregator, native_value) + observer.user_capture = capture + audio = encode_wav([b"\x10\x10" * 160], 16000, 1) + try: + for index in range(270): + async for frame in stt.run_stt(audio): + await aggregator._handle_transcription(frame) + turn = observer.turns.start("user", "UserStartedSpeakingFrame") + observer.turns.confirm("user") + await aggregator._push_aggregation() + observer.turns.stop("user", SimpleNamespace(content="order?", timestamp=str(index), user_id="user")) + await asyncio.gather(*capture.tasks) + descriptors = [ + r["metadata"]["audio.recordings"] + for r in turn["span"].rows + if "audio.recordings" in r.get("metadata", {}) + ] + self.assertEqual(descriptors[-1][0]["state"], "ready") + self.assertFalse(capture.batches) + self.assertFalse(capture.segments) + self.assertEqual(capture.bytes, 0) + finally: + await observer.finish() diff --git a/py/src/braintrust/integrations/pipecat/voice/tts_metrics.py b/py/src/braintrust/integrations/pipecat/voice/tts_metrics.py new file mode 100644 index 000000000..7526967cc --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/voice/tts_metrics.py @@ -0,0 +1,44 @@ +"""Carry native synthesis identity across Pipecat's asynchronous frame delivery.""" + +import asyncio +from contextvars import ContextVar + + +class TTSRequests: + def __init__(self, hooks, services): + self.token = object() + for service in services: + self.install(hooks, service) + + def install(self, hooks, service): + scope = ContextVar("braintrust_tts_request", default=None) + synthesize = hooks.original(service, "_push_tts_frames") + push = hooks.original(service, "push_frame") + + async def request(frame, *args, **kwargs): + token = scope.set((frame, asyncio.current_task())) + try: + return await synthesize(frame, *args, **kwargs) + finally: + scope.reset(token) + + async def emit(frame, *args, **kwargs): + active = scope.get() + if type(frame).__name__ == "MetricsFrame" and active and active[1] is asyncio.current_task(): + context = getattr(active[0], "context_id", None) + if context: + # Private bookkeeping travels with the existing queued frame; + # no side table retains frames or audio. Native serialization + # ignores private attributes, and the observer consumes it. + frame._braintrust_tts_request = (self.token, id(service), context) + return await push(frame, *args, **kwargs) + + hooks.set(service, "_push_tts_frames", request) + hooks.set(service, "push_frame", emit) + + def take(self, frame, source): + identity = getattr(frame, "_braintrust_tts_request", None) + if identity and identity[:2] == (self.token, id(source)): + del frame._braintrust_tts_request + return ("tts", identity[2]) + return None diff --git a/py/src/braintrust/integrations/pipecat/voice/user_capture.py b/py/src/braintrust/integrations/pipecat/voice/user_capture.py index d09cf01de..240d13425 100644 --- a/py/src/braintrust/integrations/pipecat/voice/user_capture.py +++ b/py/src/braintrust/integrations/pipecat/voice/user_capture.py @@ -113,6 +113,7 @@ async def run(audio): self.segments.append(segment) else: self.omitted += 1 + segment["reason"] = "capture_backlog_limit" if observer.capture_user_audio and tracked: if self.bytes + len(audio) <= observer.max_audio_bytes and source_budget.reserve(len(audio)): segment.update(audio=audio, reason=None) @@ -204,6 +205,21 @@ async def commit(*args, **kwargs): ], } ) + else: + if turn: + self.omitted += 1 + owner.log( + metadata={ + "audio.recordings": [ + { + "id": "user-clip", + "state": "omitted", + "reason": "capture_backlog_limit", + } + ] + } + ) + self.release_segments(segments) if turn: self.queue_completed(turn) return result @@ -248,13 +264,14 @@ def create_stt(self, segment, owner, turn=None): span.log(error=segment["error"]) span.end(end_time=segment["end"] or time.time()) segment["span"] = span + self.segments = [pending for pending in self.segments if pending is not segment] self.observer.alignment.add(span, segment["ranges"], 0) if turn: self.observer.alignment.add(owner, segment["ranges"], 0) span.log(input={"recording_span_id": owner.span_id, "recording_id": "user-clip"}) async def finish(self): - for segment in self.segments: + for segment in list(self.segments): self.create_stt(segment, self.observer.root) for batch in list(self.batches.values()): self.queue_completed(batch["turn"], force=True) @@ -321,13 +338,17 @@ async def _publish_batch(self, batch): ) turn["span"].log(metadata={"audio.recordings": [descriptor]}) + self.release_segments(segments) + self.batches.pop(turn["span"].span_id, None) + await asyncio.to_thread(self.observer.logger.flush) + + def release_segments(self, segments): for segment in segments: if segment["audio"] is not None: size = len(segment["audio"]) segment["audio"] = None self.bytes -= size source_budget.release(size) - await asyncio.to_thread(self.observer.logger.flush) def release(self): self.batches.clear() From b9257d8fbed667501f6a460ee97a7aa3098823c1 Mon Sep 17 00:00:00 2001 From: David Elner Date: Tue, 6 Oct 2026 18:03:25 -0400 Subject: [PATCH 14/18] Added: Pipecat state and timeline mappings --- py/src/braintrust/audio/alignment.py | 73 ++++++++++++++++--- py/src/braintrust/audio/segments.py | 11 ++- py/src/braintrust/audio/test_segments.py | 29 ++++++++ py/src/braintrust/audio/timeline.py | 34 +++++++++ .../pipecat/test_voice_pipeline.py | 14 ++++ .../integrations/pipecat/voice/README.md | 4 +- .../integrations/pipecat/voice/alignment.py | 48 +++++++----- .../integrations/pipecat/voice/hooks.py | 14 ++++ .../pipecat/voice/instrumentation.py | 48 +++++++++++- .../pipecat/voice/test_alignment.py | 18 +++++ .../pipecat/voice/test_instrumentation.py | 57 ++++++++++++++- .../pipecat/voice/test_user_capture.py | 15 ++++ .../pipecat/voice/user_capture.py | 42 +++++++++-- 13 files changed, 366 insertions(+), 41 deletions(-) create mode 100644 py/src/braintrust/audio/timeline.py diff --git a/py/src/braintrust/audio/alignment.py b/py/src/braintrust/audio/alignment.py index adced8d81..07abdcb3a 100644 --- a/py/src/braintrust/audio/alignment.py +++ b/py/src/braintrust/audio/alignment.py @@ -21,8 +21,12 @@ def __init__(self, root, recording): self.recording = recording self.owners = {} self.contexts = {} + self.clips = {} + self.clip_descriptors = {} self.count = 0 self.omitted = 0 + self.published = {} + self.pending_owners = set() def add(self, owner, ranges, channel): if self.recording.reason: @@ -38,7 +42,27 @@ def add(self, owner, ranges, channel): item[2].append(list(interval)) self.count += 1 + if item[2]: + self.pending_owners.add(owner.span_id) + + def publish_clip(self, owner, descriptor): + self.clip_descriptors[owner.span_id] = (owner, descriptor) + self.publish_clips() + + def publish_clips(self): + origin = getattr(self.recording, "origin_unix_ms", None) + if origin is None: + return + for span_id, (owner, descriptor) in self.clip_descriptors.items(): + clip = self.clips.get(span_id) + if clip is not None: + timeline = clip.descriptor(origin) + if timeline != descriptor.get("timeline"): + descriptor["timeline"] = timeline + owner.log(metadata={"audio.recordings": [dict(descriptor)]}) + def publish(self): + self.publish_clips() segments = getattr(self.recording, "completed", None) if segments is None: segments = ( @@ -46,8 +70,10 @@ def publish(self): if not self.recording.reason and self.recording.packets else [] ) - for owner, channel, ranges in self.owners.values(): - selections = [] + for span_id in tuple(self.pending_owners): + owner, channel, ranges = self.owners[span_id] + selections = list(self.published.get(owner.span_id, [])) + pending = [] for segment in segments: if segment["state"] != "ready": continue @@ -64,7 +90,29 @@ def publish(self): "channel_index": channel, } ) - if selections: + resolved = merge_ranges( + [ + [round(s["start_ms"] * 24), round(s["end_ms"] * 24)] + for s in segments + if s["state"] in {"ready", "omitted"} + ] + ) + for start, end in ranges: + for lower, upper in resolved: + if lower > start: + pending.append([start, min(lower, end)]) + start = max(start, upper) if lower < end else end + if start >= end: + break + if start < end: + pending.append([start, end]) + self.count -= len(ranges) - len(pending) + ranges[:] = pending + selections = [dict(items) for items in dict.fromkeys(tuple(s.items()) for s in selections)] + if not pending: + self.pending_owners.discard(span_id) + if selections and selections != self.published.get(span_id): + self.published[span_id] = selections owner.log( metadata={ "audio.selections": selections, @@ -93,17 +141,24 @@ def trim(self, retained): if not run[0]: self.runs.popleft() - def drain(self): - ranges = [] + def drain_mapped(self): + pieces = [] + position = 0 for count, interval, original, offset in self.runs: if interval: length = interval["end"] - interval["start"] - ranges.append( - [ + pieces.append( + ( + position, + position + count, interval["start"] + round(offset * length / original), interval["start"] + round((offset + count) * length / original), - ] + ) ) + position += count self.runs.clear() self.size = 0 - return merge_ranges(ranges) + return pieces + + def drain(self): + return merge_ranges([[a, b] for _, _, a, b in self.drain_mapped()]) diff --git a/py/src/braintrust/audio/segments.py b/py/src/braintrust/audio/segments.py index e476f60c0..6feae9018 100644 --- a/py/src/braintrust/audio/segments.py +++ b/py/src/braintrust/audio/segments.py @@ -1,6 +1,7 @@ """Progressively export independently decodable segments on one sample timeline.""" import asyncio +import logging from .attachments import prepare_recording from .budget import source_budget @@ -16,7 +17,7 @@ class SegmentedRecording(CallRecording): exported. One second of headroom permits in-flight output and packet skew. """ - def __init__(self, *, options=None, on_segment=None, audio_format="ogg", enabled=True): + def __init__(self, *, options=None, on_segment=None, on_pending=None, audio_format="ogg", enabled=True): self.options = options or RecordingOptions() super().__init__( enabled=enabled, @@ -25,6 +26,7 @@ def __init__(self, *, options=None, on_segment=None, audio_format="ogg", enabled max_packets=100000, ) self.on_segment = on_segment + self.on_pending = on_pending self.audio_format = audio_format self.start_ms = 0.0 self.sequence = 0 @@ -108,6 +110,8 @@ def _rotate(self, cut): self.start_ms = cut self.sequence += 1 self.inflight.append(segment) + if self.on_pending: + self.on_pending(segment) task = asyncio.create_task(self._export(segment)) self.pending.add(task) @@ -145,6 +149,11 @@ def encode(): if descriptor not in self.completed: self.completed.append(descriptor) self.omit("segment_export_failed") + if self.on_segment: + try: + await self.on_segment(segment, None) + except Exception: # noqa: BLE001 - failure reporting must not break the call + logging.getLogger(__name__).warning("Failed to publish recording omission", exc_info=True) finally: # encode_in_worker drains native work before propagating cancellation. segment.clear() diff --git a/py/src/braintrust/audio/test_segments.py b/py/src/braintrust/audio/test_segments.py index 148921839..0abbf9b56 100644 --- a/py/src/braintrust/audio/test_segments.py +++ b/py/src/braintrust/audio/test_segments.py @@ -13,13 +13,17 @@ class SegmentTests(unittest.IsolatedAsyncioTestCase): async def test_rotates_during_capture_and_preserves_cross_boundary_samples(self): files = [] + pending = set() async def publish(segment, encoded): + self.assertIn(segment.segment_id, pending) + pending.remove(segment.segment_id) files.append((segment.start_ms, encoded)) recorder = SegmentedRecording( options=RecordingOptions(segment_duration_seconds=0.1), on_segment=publish, + on_pending=lambda segment: pending.add(segment.segment_id), audio_format="wav", ) pcm = np.full(480, 1000, dtype=" Date: Tue, 6 Oct 2026 18:25:01 -0400 Subject: [PATCH 15/18] Refactored: Recording-job cleanup, synthesis recording, alignment --- py/src/braintrust/audio/alignment.py | 107 +++++--- py/src/braintrust/audio/jobs.py | 59 +++++ py/src/braintrust/audio/segments.py | 41 ++- py/src/braintrust/audio/test_segments.py | 8 +- py/src/braintrust/audio/test_worker.py | 34 +++ .../integrations/pipecat/voice/alignment.py | 14 +- .../pipecat/voice/instrumentation.py | 236 +++--------------- .../integrations/pipecat/voice/synthesis.py | 181 ++++++++++++++ .../pipecat/voice/test_alignment.py | 24 +- .../pipecat/voice/test_instrumentation.py | 60 ++--- .../pipecat/voice/test_realtime.py | 10 +- .../integrations/pipecat/voice/test_turns.py | 7 +- .../pipecat/voice/test_user_capture.py | 2 +- .../pipecat/voice/user_capture.py | 29 ++- 14 files changed, 488 insertions(+), 324 deletions(-) create mode 100644 py/src/braintrust/audio/jobs.py create mode 100644 py/src/braintrust/integrations/pipecat/voice/synthesis.py diff --git a/py/src/braintrust/audio/alignment.py b/py/src/braintrust/audio/alignment.py index 07abdcb3a..655475df7 100644 --- a/py/src/braintrust/audio/alignment.py +++ b/py/src/braintrust/audio/alignment.py @@ -1,6 +1,11 @@ """Bounded sample ranges and recording selections shared by voice integrations.""" from collections import deque +from dataclasses import dataclass, field + +from braintrust.logger import Span + +from .timeline import ClipTimeline def merge_ranges(ranges): @@ -15,51 +20,89 @@ def merge_ranges(ranges): return result +@dataclass +class _SelectionOwner: + span: Span + channel: int + ranges: list[list[int]] = field(default_factory=list) + published: list[dict] = field(default_factory=list) + + +@dataclass +class _Clip: + timeline: ClipTimeline = field(default_factory=ClipTimeline) + owner: Span | None = None + descriptor: dict | None = None + + class Alignment: def __init__(self, root, recording): self.root = root self.recording = recording - self.owners = {} - self.contexts = {} - self.clips = {} - self.clip_descriptors = {} + self._owners: dict[str, _SelectionOwner] = {} + self._contexts = {} + self._clips: dict[str, _Clip] = {} + self._dirty_clips: set[str] = set() self.count = 0 self.omitted = 0 - self.published = {} self.pending_owners = set() def add(self, owner, ranges, channel): if self.recording.reason: return - item = self.owners.setdefault(owner.span_id, (owner, channel, [])) + item = self._owners.setdefault(owner.span_id, _SelectionOwner(owner, channel)) for interval in ranges: - if item[2] and item[2][-1][0] <= interval[0] <= item[2][-1][1]: - item[2][-1][1] = max(item[2][-1][1], interval[1]) + if item.ranges and item.ranges[-1][0] <= interval[0] <= item.ranges[-1][1]: + item.ranges[-1][1] = max(item.ranges[-1][1], interval[1]) continue if self.count >= 32000: self.omitted += 1 continue - item[2].append(list(interval)) + item.ranges.append(list(interval)) self.count += 1 - if item[2]: + if item.ranges: self.pending_owners.add(owner.span_id) + def begin_output(self, context, owners): + if len(self._contexts) >= 16000: + self.omitted += 1 + return + self._contexts[context] = owners + self._clips[owners[0].span_id] = _Clip() + + def output_owners(self, context): + return self._contexts.get(context, []) + + def end_output(self, context): + self._contexts.pop(context, None) + + def add_clip_range(self, owner, start_ms, end_ms, call_start_ms, call_end_ms): + clip = self._clips.get(owner.span_id) + if clip is not None: + clip.timeline.add(start_ms, end_ms, call_start_ms, call_end_ms) + self._dirty_clips.add(owner.span_id) + def publish_clip(self, owner, descriptor): - self.clip_descriptors[owner.span_id] = (owner, descriptor) - self.publish_clips() + clip = self._clips.get(owner.span_id) + if clip is not None: + clip.owner, clip.descriptor = owner, descriptor + self._dirty_clips.add(owner.span_id) + self.publish_clips() def publish_clips(self): origin = getattr(self.recording, "origin_unix_ms", None) if origin is None: return - for span_id, (owner, descriptor) in self.clip_descriptors.items(): - clip = self.clips.get(span_id) - if clip is not None: - timeline = clip.descriptor(origin) - if timeline != descriptor.get("timeline"): - descriptor["timeline"] = timeline - owner.log(metadata={"audio.recordings": [dict(descriptor)]}) + for span_id in tuple(self._dirty_clips): + clip = self._clips[span_id] + if clip.descriptor is None: + continue + timeline = clip.timeline.descriptor(origin) + if timeline != clip.descriptor.get("timeline"): + clip.descriptor["timeline"] = timeline + clip.owner.log(metadata={"audio.recordings": [dict(clip.descriptor)]}) + self._dirty_clips.discard(span_id) def publish(self): self.publish_clips() @@ -70,15 +113,24 @@ def publish(self): if not self.recording.reason and self.recording.packets else [] ) + resolved = merge_ranges( + [ + [round(s["start_ms"] * 24), round(s["end_ms"] * 24)] + for s in segments + if s["state"] in {"ready", "omitted"} + ] + ) for span_id in tuple(self.pending_owners): - owner, channel, ranges = self.owners[span_id] - selections = list(self.published.get(owner.span_id, [])) + state = self._owners[span_id] + owner, channel, ranges = state.span, state.channel, state.ranges + selections = list(state.published) + merged = merge_ranges(ranges) pending = [] for segment in segments: if segment["state"] != "ready": continue lower, upper = round(segment["start_ms"] * 24), round(segment["end_ms"] * 24) - for start, end in merge_ranges(ranges): + for start, end in merged: start, end = max(start, lower), min(end, upper) if end > start: selections.append( @@ -90,13 +142,6 @@ def publish(self): "channel_index": channel, } ) - resolved = merge_ranges( - [ - [round(s["start_ms"] * 24), round(s["end_ms"] * 24)] - for s in segments - if s["state"] in {"ready", "omitted"} - ] - ) for start, end in ranges: for lower, upper in resolved: if lower > start: @@ -111,8 +156,8 @@ def publish(self): selections = [dict(items) for items in dict.fromkeys(tuple(s.items()) for s in selections)] if not pending: self.pending_owners.discard(span_id) - if selections and selections != self.published.get(span_id): - self.published[span_id] = selections + if selections and selections != state.published: + state.published = selections owner.log( metadata={ "audio.selections": selections, diff --git a/py/src/braintrust/audio/jobs.py b/py/src/braintrust/audio/jobs.py new file mode 100644 index 000000000..291b0b656 --- /dev/null +++ b/py/src/braintrust/audio/jobs.py @@ -0,0 +1,59 @@ +"""Own background recording tasks and release their input after work stops.""" + +import asyncio +import logging +from collections.abc import Awaitable, Callable + + +class RecordingJobs: + def __init__(self): + self._tasks: set[asyncio.Task] = set() + + def __len__(self): + return sum(not task.done() for task in self._tasks) + + def submit( + self, + operation: Callable[[], Awaitable[None]], + release: Callable[[], None], + *, + after_release: Callable[[], Awaitable[None]] | None = None, + ) -> asyncio.Task: + released = False + + def release_once(): + nonlocal released + if not released: + released = True + release() + + async def run(): + try: + await operation() + finally: + release_once() + if after_release: + await after_release() + + task = asyncio.create_task(run()) + self._tasks.add(task) + + def completed(task): + # Also runs if cancelled before run() starts. The encoding worker + # does not complete cancellation until native work has stopped. + try: + release_once() + finally: + self._tasks.discard(task) + if not task.cancelled() and (error := task.exception()) is not None: + logging.getLogger(__name__).warning( + "Recording publication failed", exc_info=(type(error), error, error.__traceback__) + ) + + task.add_done_callback(completed) + return task + + async def drain(self) -> None: + if self._tasks: + # A cancelled waiter must not release another task's encoder input. + await asyncio.shield(asyncio.gather(*tuple(self._tasks), return_exceptions=True)) diff --git a/py/src/braintrust/audio/segments.py b/py/src/braintrust/audio/segments.py index 6feae9018..7393b8021 100644 --- a/py/src/braintrust/audio/segments.py +++ b/py/src/braintrust/audio/segments.py @@ -5,6 +5,7 @@ from .attachments import prepare_recording from .budget import source_budget +from .jobs import RecordingJobs from .options import RecordingOptions from .recording import CallRecording, Packet from .worker import encode_in_worker @@ -31,7 +32,7 @@ def __init__(self, *, options=None, on_segment=None, on_pending=None, audio_form self.start_ms = 0.0 self.sequence = 0 self.completed = [] - self.pending = set() + self.jobs = RecordingJobs() self.inflight = [] self._finish_task = None @@ -74,8 +75,7 @@ def capture(self, channel, pcm, sample_rate, channels, *, observed_ns=None, obse def _rotate(self, cut): if not self.packets: return - self.pending = {task for task in self.pending if not task.done()} - if len(self.pending) >= 2: + if len(self.jobs) >= 2: self.omit("recording_export_capacity") return segment = CallRecording() @@ -110,18 +110,14 @@ def _rotate(self, cut): self.start_ms = cut self.sequence += 1 self.inflight.append(segment) - if self.on_pending: - self.on_pending(segment) - task = asyncio.create_task(self._export(segment)) - self.pending.add(task) - def completed(task): - self.pending.discard(task) - if segment in self.inflight: - segment.clear() - self.inflight.remove(segment) + def release(): + segment.clear() + self.inflight.remove(segment) - task.add_done_callback(completed) + self.jobs.submit(lambda: self._export(segment), release) + if self.on_pending: + self.on_pending(segment) async def _export(self, segment): descriptor = { @@ -154,10 +150,11 @@ def encode(): await self.on_segment(segment, None) except Exception: # noqa: BLE001 - failure reporting must not break the call logging.getLogger(__name__).warning("Failed to publish recording omission", exc_info=True) - finally: - # encode_in_worker drains native work before propagating cancellation. - segment.clear() - self.inflight.remove(segment) + + def release_idle_buffer(self): + """Release unsubmitted audio unless a final drain still owns it.""" + if self._finish_task is None or self._finish_task.done(): + self.clear() async def finish(self): if self._finish_task is None: @@ -166,9 +163,11 @@ async def finish(self): async def _finish(self): self.seal() - if self.pending: - await asyncio.gather(*tuple(self.pending)) + await self.drain_exports() if self.packets: self._rotate(max(self.ends)) - if self.pending: - await asyncio.gather(*tuple(self.pending)) + await self.drain_exports() + + async def drain_exports(self): + """Wait for detached segments without sealing the active recording.""" + await self.jobs.drain() diff --git a/py/src/braintrust/audio/test_segments.py b/py/src/braintrust/audio/test_segments.py index 0abbf9b56..4e26d1a91 100644 --- a/py/src/braintrust/audio/test_segments.py +++ b/py/src/braintrust/audio/test_segments.py @@ -57,8 +57,7 @@ async def publish(segment, encoded): recorder.capture(1, audio.tobytes(), 24000, 1, observed_ns=0) for index in range(4): recorder.capture(1, b"\0\0" * 24000, 24000, 1, observed_ns=(3 + index) * 1_000_000_000) - if recorder.pending: - await asyncio.gather(*tuple(recorder.pending)) + await recorder.drain_exports() self.assertTrue(files, "rotation must export before shutdown") await recorder.finish() pieces = [] @@ -117,8 +116,7 @@ async def publish(segment, encoded): recorder.capture(1, agent, 24000, 1, observed_ns=index * 500_000_000) peak = max(peak, recorder.retained_bytes) # Accelerated replay: permit the real encoder/exporter to drain. - if recorder.pending: - await asyncio.gather(*tuple(recorder.pending)) + await recorder.drain_exports() await recorder.finish() self.assertEqual(durations, [30000] * 20) self.assertIsNone(recorder.reason) @@ -215,7 +213,7 @@ def test_out_of_order_export_releases_only_resolved_ranges(self): alignment = Alignment(SimpleNamespace(span_id="root", log=lambda **_: None), recording) alignment.add(owner, [[0, 48000]], 0) alignment.publish() - self.assertEqual(alignment.owners["turn"][2], [[0, 24000]]) + self.assertEqual(rows[-1]["metadata"]["audio.selection"]["recording_id"], "later") recording.completed.append({"id": "earlier", "start_ms": 0, "end_ms": 1000, "state": "ready"}) alignment.publish() self.assertEqual(alignment.count, 0) diff --git a/py/src/braintrust/audio/test_worker.py b/py/src/braintrust/audio/test_worker.py index 82d9ccad0..8accdbd8a 100644 --- a/py/src/braintrust/audio/test_worker.py +++ b/py/src/braintrust/audio/test_worker.py @@ -35,6 +35,40 @@ def block(): release.set() self.assertEqual(await encode_in_worker(lambda: 4), 4) + async def test_recording_jobs_release_once_on_success_failure_and_early_cancellation(self): + from .jobs import RecordingJobs + + jobs = RecordingJobs() + released = [] + executed = [] + flushed = [] + + async def flush(): + self.assertEqual(released, ["success"]) + flushed.append("success") + + async def operation(outcome): + executed.append(outcome) + if outcome == "failure": + raise ValueError("publication failed") + + for outcome in ("success", "failure", "cancelled"): + task = jobs.submit( + lambda: operation(outcome), + lambda: released.append(outcome), + after_release=flush if outcome == "success" else None, + ) + if outcome == "cancelled": + task.cancel() # No operation body or finally block has run yet. + if outcome == "failure": + with self.assertLogs("braintrust.audio.jobs", level="WARNING"): + await jobs.drain() + else: + await jobs.drain() + self.assertEqual(flushed, ["success"]) + self.assertEqual(executed, ["success", "failure"]) + self.assertEqual(released, ["success", "failure", "cancelled"]) + async def test_process_capture_budget_rejects_without_retaining_and_releases(self): from unittest.mock import patch diff --git a/py/src/braintrust/integrations/pipecat/voice/alignment.py b/py/src/braintrust/integrations/pipecat/voice/alignment.py index 28081499e..bdb941f93 100644 --- a/py/src/braintrust/integrations/pipecat/voice/alignment.py +++ b/py/src/braintrust/integrations/pipecat/voice/alignment.py @@ -40,7 +40,7 @@ async def handle(frame): start = source_offsets.get(context, 0) if context is not None: source_offsets[context] = start + len(frame.audio) - owners = alignment.contexts.get(context, []) + owners = alignment.output_owners(context) current = active.set((owners, start)) try: return await handle_audio(frame) @@ -55,7 +55,7 @@ async def stop(frame): finally: active.reset(current) source_offsets.pop(context, None) - alignment.contexts.pop(context, None) + alignment.end_output(context) def buffer(audio, *, uninterruptible): result = buffer_audio(audio, uninterruptible=uninterruptible) @@ -144,9 +144,13 @@ async def write(frame): ] ] if owners: - clip = alignment.clips.get(owners[0].span_id) - if clip is not None: - clip.add(start / 48, (start + size) / 48, ranges[0][0] / 24, ranges[0][1] / 24) + alignment.add_clip_range( + owners[0], + start / 48, + (start + size) / 48, + ranges[0][0] / 24, + ranges[0][1] / 24, + ) for owner in owners: alignment.add(owner, ranges, 1) return result diff --git a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py index 7fe0b9792..8c673803a 100644 --- a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py +++ b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py @@ -9,16 +9,13 @@ from collections import Counter, deque from braintrust.audio.alignment import Alignment -from braintrust.audio.attachments import prepare_recording -from braintrust.audio.budget import source_budget -from braintrust.audio.recording import encode_audio from braintrust.audio.segments import SegmentedRecording -from braintrust.audio.worker import RecordingBusy, encode_in_worker from pipecat.observers.base_observer import BaseObserver # pylint: disable=import-error from ..llm_metrics import _llm_usage_metrics, _metadata_from_metric, _metadata_from_processor from ..ttfb import TTFBRouter from ..turn_metrics import TURN_METRIC_TYPES, log_turn_metric +from .synthesis import Synthesis, SynthesisRecordings from .turns import Turns @@ -156,7 +153,6 @@ def __init__( on_pending=self._pending_call_segment, ) self.call_descriptors = {} - self.clip_tasks = set() self.realtime = None self.input_processor = None self.capture_transport = False @@ -168,6 +164,13 @@ def __init__( self.turns = Turns(self.root, self.hooks) self.turns.on_completed = self._turn_completed self.alignment = Alignment(self.root, self.call_recording) + self.synthesis = SynthesisRecordings( + logger, + self.alignment, + enabled=self.capture_agent_audio, + max_bytes=max_audio_bytes, + audio_format=audio_format, + ) self.user_capture = None self.llm_turn = None self.user_aggregator = None @@ -192,9 +195,6 @@ def __init__( self.event_bytes = 0 self.retain_audio = retain_audio self.max_audio_bytes = max_audio_bytes - self.audio_bytes = 0 - self.recordings = [] - self.recordings_omitted = 0 self.finished = False self._finish_task = None self.tool_names = [] @@ -203,12 +203,6 @@ def __init__( self.seen = set() self.seen_order = deque() - def __del__(self): - retained = getattr(self, "audio_bytes", 0) - if retained: - source_budget.release(retained) - self.audio_bytes = 0 - def bind( self, *, transport, user_aggregator, assistant_aggregator, stt=None, realtime_service=None, tts_services=() ): @@ -281,44 +275,7 @@ async def on_push_frame(self, data): context = self.frame_context(frame) state = self.tts.get(context) if state: - audio_format = (frame.sample_rate, frame.num_channels) - state["last_pts"] = frame.pts - if state.get("observed_format") != audio_format: - state["observed_format"] = audio_format - state["span"].log( - metadata={ - "contrib.pipecat.sample_rate": frame.sample_rate, - "contrib.pipecat.num_channels": frame.num_channels, - "contrib.pipecat.frame.pts": frame.pts, - } - ) - if state["chunks"] and (state["rate"], state["channels"]) != audio_format: - state["omitted"] = True - state["reason"] = "audio_format_changed" - size = sum(map(len, state["chunks"])) - source_budget.release(size) - self.audio_bytes -= size - state["chunks"].clear() - if self.capture_agent_audio and not state["omitted"]: - if self.audio_bytes + len(frame.audio) <= self.max_audio_bytes and source_budget.reserve( - len(frame.audio) - ): - state["chunks"].append(frame.audio) - self.audio_bytes += len(frame.audio) - state["rate"], state["channels"] = ( - frame.sample_rate, - frame.num_channels, - ) - else: - state["omitted"] = True - state["reason"] = ( - "capture_byte_limit" - if self.audio_bytes + len(frame.audio) > self.max_audio_bytes - else "process_capture_byte_limit" - ) - source_budget.release(sum(map(len, state["chunks"]))) - self.audio_bytes -= sum(map(len, state["chunks"])) - state["chunks"].clear() + self.synthesis.capture(state, frame) return if "AudioRawFrame" in kind: return @@ -552,36 +509,26 @@ async def on_push_frame(self, data): internal={"instrumentation": "pipecat-auto"}, ) self.ttfb.start(("tts", context), data.source, span.log) - self.tts[context] = { - "span": span, - "chunks": [], - "omitted": False, - "text": [], - } - if self.capture_agent_audio and len(self.alignment.contexts) < 16000: - from braintrust.audio.timeline import ClipTimeline - - self.alignment.clips[span.span_id] = ClipTimeline() - self.alignment.contexts[context] = [span, turn["span"]] - elif self.capture_agent_audio: - self.alignment.omitted += 1 + self.tts[context] = Synthesis(span) + if self.capture_agent_audio: + self.alignment.begin_output(context, [span, turn["span"]]) elif kind == "TTSTextFrame": state = self.tts.get(self.frame_context(frame)) if state: - state["text"].append(frame.text) - text = "".join(state["text"]) - state["span"].log(input={"text": text}, metadata={"contrib.pipecat.text": text}) + state.text.append(frame.text) + text = "".join(state.text) + state.span.log(input={"text": text}, metadata={"contrib.pipecat.text": text}) elif kind == "TTSStoppedFrame": self.ttfb.end(("tts", self.frame_context(frame))) state = self.tts.pop(self.frame_context(frame), None) if state: - state["span"].end() - self.complete_recording(state) + state.span.end() + self.synthesis.complete(state) elif kind == "InterruptionFrame": for state in self.tts.values(): - state["span"].log(metadata={"contrib.pipecat.end_frame": kind}) - state["span"].end() - self.complete_recording(state) + state.span.log(metadata={"contrib.pipecat.end_frame": kind}) + state.span.end() + self.synthesis.complete(state) for context in self.tts: self.ttfb.end(("tts", context)) self.tts.clear() @@ -653,51 +600,6 @@ def capture_event(self, data, fields, owner): else: self.omitted_events += 1 - def complete_recording(self, state): - if "last_pts" in state: - state["span"].log(metadata={"contrib.pipecat.frame.pts": state["last_pts"]}) - if len(self.recordings) < 256: - self.recordings.append(state) - if state["chunks"] and not state["omitted"]: - state["span"].log( - metadata={ - "audio.recordings": [ - {"id": "tts-clip", "state": "pending", "sources": [{"boundary": "tts_output"}]} - ] - } - ) - task = asyncio.create_task(self._publish_tts_clip(state)) - self.clip_tasks.add(task) - - def completed(task): - # A failed exporter must release its source lease too. - size = sum(map(len, state["chunks"])) - state["chunks"].clear() - source_budget.release(size) - self.audio_bytes -= size - self.clip_tasks.discard(task) - self.recordings = [pending for pending in self.recordings if pending is not state] - - task.add_done_callback(completed) - return - self.recordings_omitted += 1 - state["span"].log( - metadata={ - "audio.recordings": [ - { - "id": "tts-clip", - "state": "omitted", - "reason": "recording_count_limit", - "sources": [{"boundary": "tts_output"}], - } - ] - } - ) - size = sum(map(len, state["chunks"])) - state["chunks"].clear() - source_budget.release(size) - self.audio_bytes -= size - async def cleanup(self): await self.finish() await super().cleanup() @@ -717,15 +619,12 @@ async def _finish(self): self.hooks.close() # A shielded segment drain can outlive cancellation of this finalizer. # Its active tail still belongs to that drain until it seals/exports it. - drain = self.call_recording._finish_task - if drain is None or drain.done(): - self.call_recording.clear() - for state in self.recordings: - state["chunks"].clear() - source_budget.release(self.audio_bytes) - self.audio_bytes = 0 + self.call_recording.release_idle_buffer() + await self.synthesis.drain() + for state in self.tts.values(): + self.synthesis.release(state) if self.user_capture: - self.user_capture.release() + await self.user_capture.close() async def _finalize(self): self.ttfb.clear() @@ -734,16 +633,16 @@ async def _finalize(self): if span: span.end() for state in self.tts.values(): - state["span"].end() - state["omitted"] = True - state["reason"] = "pipeline_closed_before_tts_stop" - self.complete_recording(state) + state.span.end() + state.omitted = True + state.reason = "pipeline_closed_before_tts_stop" + self.synthesis.complete(state) for owner, events in self.event_groups.values(): owner.log(metadata={"contrib.pipecat.events": events}) self.root.log( metadata={ "braintrust.capture.events_omitted": self.omitted_events, - "braintrust.capture.recordings_omitted": self.recordings_omitted, + "braintrust.capture.recordings_omitted": self.synthesis.omitted, "braintrust.capture.filtered_event_counts": dict(self.filtered_events), "braintrust.capture.duplicate_events": self.duplicate_events, "braintrust.capture.retained_events": len(self.events), @@ -755,86 +654,13 @@ async def _finalize(self): if self.realtime: self.realtime.finish() # Trace lifetime ends before encoding; flush only after attachment updates. - if self.clip_tasks: - await asyncio.gather(*tuple(self.clip_tasks)) + await self.synthesis.drain() if self.capture_transport: await self.call_recording.finish() self._publish_call_manifest() self.alignment.publish() await asyncio.to_thread(self.logger.flush) - async def _publish_tts_clip(self, state): - span = state["span"] - recording = {"id": "tts-clip", "sources": [{"boundary": "tts_output"}]} - if state["chunks"] and not state["omitted"]: - try: - encoded = await encode_in_worker( - prepare_recording, - "tts-clip", - encode_audio, - state["chunks"], - state["rate"], - state["channels"], - self.audio_format, - ) - except Exception as error: # noqa: BLE001 - capture/export failures must not break the call - span.log( - metadata={ - "audio.recordings": [ - { - **recording, - "state": "omitted", - "reason": str(error) if isinstance(error, RecordingBusy) else "encoding_failed", - } - ] - } - ) - source_budget.release(sum(map(len, state["chunks"]))) - self.audio_bytes -= sum(map(len, state["chunks"])) - state["chunks"].clear() - return - recording.update( - state="ready", - attachment={ - "span_id": span.span_id, - "ref": "/output/0/content/1/file/file_data", - }, - mime_type=encoded["mime_type"], - duration_ms=encoded["duration_ms"], - channel_count=encoded["channel_count"], - ) - span.log( - output=[ - { - "role": "assistant", - "content": [ - {"type": "text", "text": "".join(state["text"])}, - { - "type": "file", - "file": { - "file_data": encoded["attachment"], - "filename": f"tts-clip.{encoded['extension']}", - }, - }, - ], - } - ], - metadata={"braintrust.recording.processing": encoded["processing"]}, - ) - else: - recording.update( - state="omitted", - reason=state.get("reason", "no_audio_observed") if self.capture_agent_audio else "disabled", - ) - span.log(metadata={"audio.recordings": [recording]}) - if recording["state"] == "ready": - self.alignment.publish_clip(span, recording) - source_budget.release(sum(map(len, state["chunks"]))) - self.audio_bytes -= sum(map(len, state["chunks"])) - state["chunks"].clear() - - await asyncio.to_thread(self.logger.flush) - def _call_sources(self): return [ *( diff --git a/py/src/braintrust/integrations/pipecat/voice/synthesis.py b/py/src/braintrust/integrations/pipecat/voice/synthesis.py new file mode 100644 index 000000000..8b879e9b4 --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/voice/synthesis.py @@ -0,0 +1,181 @@ +"""Generated speech capture and publication, independent of frame dispatch.""" + +import asyncio +from dataclasses import dataclass, field + +from braintrust.audio.attachments import prepare_recording +from braintrust.audio.budget import source_budget +from braintrust.audio.jobs import RecordingJobs +from braintrust.audio.recording import encode_audio +from braintrust.audio.worker import RecordingBusy, encode_in_worker +from braintrust.logger import Span + + +@dataclass +class Synthesis: + span: Span + chunks: list[bytes] = field(default_factory=list) + text: list[str] = field(default_factory=list) + omitted: bool = False + reason: str | None = None + rate: int | None = None + channels: int | None = None + observed_format: tuple[int, int] | None = None + last_pts: int | None = None + + +class SynthesisRecordings: + def __init__(self, logger, alignment, *, enabled, max_bytes, audio_format): + self.logger = logger + self.alignment = alignment + self.enabled = enabled + self.max_bytes = max_bytes + self.audio_format = audio_format + self.bytes = 0 + self.omitted = 0 + self.jobs = RecordingJobs() + + def __del__(self): + retained = getattr(self, "bytes", 0) + if retained: + source_budget.release(retained) + self.bytes = 0 + + async def drain(self): + await self.jobs.drain() + + def capture(self, state, frame): + audio_format = (frame.sample_rate, frame.num_channels) + state.last_pts = frame.pts + if state.observed_format != audio_format: + state.observed_format = audio_format + state.span.log( + metadata={ + "contrib.pipecat.sample_rate": frame.sample_rate, + "contrib.pipecat.num_channels": frame.num_channels, + "contrib.pipecat.frame.pts": frame.pts, + } + ) + if state.chunks and (state.rate, state.channels) != audio_format: + state.omitted = True + state.reason = "audio_format_changed" + self.release(state) + if self.enabled and not state.omitted: + if self.bytes + len(frame.audio) <= self.max_bytes and source_budget.reserve(len(frame.audio)): + state.chunks.append(frame.audio) + self.bytes += len(frame.audio) + state.rate, state.channels = ( + frame.sample_rate, + frame.num_channels, + ) + else: + state.omitted = True + state.reason = ( + "capture_byte_limit" + if self.bytes + len(frame.audio) > self.max_bytes + else "process_capture_byte_limit" + ) + self.release(state) + + def complete(self, state): + if state.observed_format is not None: + state.span.log(metadata={"contrib.pipecat.frame.pts": state.last_pts}) + if len(self.jobs) < 256: + self.jobs.submit( + lambda: self._publish(state), + lambda: self.release(state), + after_release=lambda: asyncio.to_thread(self.logger.flush), + ) + if state.chunks and not state.omitted: + state.span.log( + metadata={ + "audio.recordings": [ + {"id": "tts-clip", "state": "pending", "sources": [{"boundary": "tts_output"}]} + ] + } + ) + return + self.omitted += 1 + state.span.log( + metadata={ + "audio.recordings": [ + { + "id": "tts-clip", + "state": "omitted", + "reason": "recording_count_limit", + "sources": [{"boundary": "tts_output"}], + } + ] + } + ) + self.release(state) + + def release(self, state): + size = sum(map(len, state.chunks)) + state.chunks.clear() + source_budget.release(size) + self.bytes -= size + + async def _publish(self, state): + span = state.span + recording = {"id": "tts-clip", "sources": [{"boundary": "tts_output"}]} + if state.chunks and not state.omitted: + try: + encoded = await encode_in_worker( + prepare_recording, + "tts-clip", + encode_audio, + state.chunks, + state.rate, + state.channels, + self.audio_format, + ) + except Exception as error: # noqa: BLE001 - capture/export failures must not break the call + span.log( + metadata={ + "audio.recordings": [ + { + **recording, + "state": "omitted", + "reason": str(error) if isinstance(error, RecordingBusy) else "encoding_failed", + } + ] + } + ) + return + recording.update( + state="ready", + attachment={ + "span_id": span.span_id, + "ref": "/output/0/content/1/file/file_data", + }, + mime_type=encoded["mime_type"], + duration_ms=encoded["duration_ms"], + channel_count=encoded["channel_count"], + ) + span.log( + output=[ + { + "role": "assistant", + "content": [ + {"type": "text", "text": "".join(state.text)}, + { + "type": "file", + "file": { + "file_data": encoded["attachment"], + "filename": f"tts-clip.{encoded['extension']}", + }, + }, + ], + } + ], + metadata={"braintrust.recording.processing": encoded["processing"]}, + ) + else: + recording.update( + state="omitted", + reason=(state.reason or "no_audio_observed") if self.enabled else "disabled", + ) + span.log(metadata={"audio.recordings": [recording]}) + if recording["state"] == "ready": + self.alignment.publish_clip(span, recording) diff --git a/py/src/braintrust/integrations/pipecat/voice/test_alignment.py b/py/src/braintrust/integrations/pipecat/voice/test_alignment.py index 61809eb58..5fea496c5 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_alignment.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_alignment.py @@ -73,7 +73,8 @@ async def write_audio_frame(self, frame): async def test_native_chunk_crossing_contexts_and_flush_padding(self): output, sender, alignment = await self.make_output() first, second = Span(), Span() - alignment.contexts = {"a": [first], "b": [second]} + alignment.begin_output("a", [first]) + alignment.begin_output("b", [second]) def audio(context, samples): return TTSAudioRawFrame( @@ -91,8 +92,9 @@ def audio(context, samples): frame = sender._audio_queue.get_nowait() if hasattr(frame, "audio"): await output.write_audio_frame(frame) - self.assertEqual(alignment.owners[first.span_id][2], [[0, 3]]) - self.assertEqual(alignment.owners[second.span_id][2], [[3, 6]]) + alignment.publish() + self.assertEqual(first.rows[-1]["metadata"]["audio.selection"]["end_offset_ms"], 3 / 24) + self.assertEqual(second.rows[-1]["metadata"]["audio.selection"]["start_offset_ms"], 3 / 24) decoded, _ = sf.read( io.BytesIO(alignment.recording.encode("wav")["data"]), dtype="int16", @@ -104,11 +106,9 @@ def audio(context, samples): async def test_discarded_output_and_failed_write_do_not_get_selections(self): output, sender, alignment = await self.make_output() - from braintrust.audio.timeline import ClipTimeline owner = Span() - alignment.clips[owner.span_id] = ClipTimeline() - alignment.contexts["a"] = [owner] + alignment.begin_output("a", [owner]) def audio(samples): return TTSAudioRawFrame( @@ -124,12 +124,14 @@ def audio(samples): frame = sender._audio_queue.get_nowait() output.success = False await output.write_audio_frame(frame) - self.assertEqual(alignment.owners, {}) + alignment.publish() + self.assertEqual(owner.rows, [{}]) self.assertEqual(alignment.recording.bytes, 0) await sender.handle_audio_frame(audio([7, 8, 9, 10])) output.success = True await output.write_audio_frame(sender._audio_queue.get_nowait()) - mapping = alignment.clips[owner.span_id].ranges + alignment.publish_clip(owner, {"id": "tts-clip", "state": "ready"}) + mapping = owner.rows[-1]["metadata"]["audio.recordings"][0]["timeline"]["ranges"] self.assertEqual(len(mapping), 1) self.assertEqual(mapping[0]["recording_start_ms"], 6 / 24) self.assertEqual(mapping[0]["recording_end_ms"], 10 / 24) @@ -138,7 +140,7 @@ def audio(samples): async def test_queue_recreation_keeps_context_mapping(self): output, sender, alignment = await self.make_output() owner = Span() - alignment.contexts["after-interruption"] = [owner] + alignment.begin_output("after-interruption", [owner]) import gc import weakref @@ -155,7 +157,9 @@ async def test_queue_recreation_keeps_context_mapping(self): ) await sender.handle_audio_frame(frame) await output.write_audio_frame(sender._audio_queue.get_nowait()) - self.assertEqual(alignment.owners[owner.span_id][2], [[0, 4]]) + alignment.publish() + selection = owner.rows[-1]["metadata"]["audio.selection"] + self.assertEqual((selection["start_offset_ms"], selection["end_offset_ms"]), (0, 4 / 24)) def test_disabled_or_omitted_recording_never_publishes_selection(self): root, owner = Span(), Span() diff --git a/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py b/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py index ff04dbd56..d6f9ea8f6 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py @@ -68,14 +68,13 @@ async def push(frame, first_push=True): await push(TTSStartedFrame(context_id="c1")) frame = TTSAudioRawFrame(audio=b"\x01\x00" * 480, sample_rate=24000, num_channels=1, context_id="c1") await push(frame) - span = observer.tts["c1"]["span"] + span = observer.root.children[-1].children[-1] await push(TTSStoppedFrame(context_id="c1")) await observer.finish() self.assertEqual(span.rows[0]["name"], "tts") recording = span.rows[-1]["metadata"]["audio.recordings"][0] self.assertEqual(recording["state"], "omitted") - self.assertEqual(observer.audio_bytes, 0) - self.assertEqual(observer.recordings, []) + self.assertEqual(observer.synthesis.bytes, 0) self.assertNotIn("audio", native_value(frame)) self.assertEqual(len(observer.events), 2) # Changed metadata and an explicit settings update. event = observer.events[0] @@ -204,14 +203,14 @@ async def push(frame): frame = TTSAudioRawFrame(b"\0\0" * 480, 24000, 1, context_id="format-test") frame.pts = index * 20_000_000 await push(frame) - assert len(state["span"].rows) <= 3 + assert len(state.span.rows) <= 3 await push(TTSAudioRawFrame(b"\0\0" * 320, 16000, 1, context_id="format-test")) await push(TTSStoppedFrame(context_id="format-test")) await observer.finish() - descriptor = state["span"].rows[-1]["metadata"]["audio.recordings"][0] + descriptor = state.span.rows[-1]["metadata"]["audio.recordings"][0] assert descriptor["state"] == "omitted" assert descriptor["reason"] == "audio_format_changed" - assert observer.audio_bytes == 0 + assert observer.synthesis.bytes == 0 async def test_cancelled_finalizer_preserves_pending_segment_and_active_tail(self): import asyncio @@ -251,34 +250,37 @@ def encode(segment, audio_format): self.assertEqual(len(observer.call_descriptors), 2) async def test_generated_clip_capacity_is_reused_after_publication(self): - import asyncio + from braintrust.audio.budget import source_budget observer = NativeObserver(Span(), retain_audio=True, audio_format="wav") - for _ in range(270): - span = Span() - state = { - "span": span, - "chunks": [b"\1\0" * 24], - "rate": 24000, - "channels": 1, - "text": ["hello"], - "omitted": False, - } - from braintrust.audio.budget import source_budget - - self.assertTrue(source_budget.reserve(48)) - observer.audio_bytes += 48 - observer.complete_recording(state) + before = source_budget.used + for index in range(270): + context = str(index) + for frame in ( + TTSStartedFrame(context_id=context), + TTSAudioRawFrame(b"\1\0" * 24, 24000, 1, context_id=context), + TTSStoppedFrame(context_id=context), + ): + await observer.on_push_frame( + SimpleNamespace( + frame=frame, + first_push=True, + source=SimpleNamespace(name="tts"), + destination=SimpleNamespace(name="output"), + direction=FrameDirection.DOWNSTREAM, + timestamp=0, + ) + ) + span = observer.root.children[-1].children[-1] self.assertEqual(span.rows[-1]["metadata"]["audio.recordings"][0]["state"], "pending") - await asyncio.gather(*tuple(observer.clip_tasks)) - self.assertEqual(span.rows[-1]["metadata"]["audio.recordings"][0]["state"], "ready") - self.assertFalse(observer.recordings) - self.assertEqual(observer.audio_bytes, 0) - self.assertEqual(observer.recordings_omitted, 0) + await observer.synthesis.drain() + descriptor = span.rows[-1]["metadata"]["audio.recordings"][0] + self.assertEqual(descriptor["state"], "ready") + self.assertEqual(descriptor["duration_ms"], 1) + self.assertEqual(source_budget.used, before) await observer.finish() async def test_failed_segment_publishes_omission_before_shutdown(self): - import asyncio from braintrust.audio import RecordingOptions, worker @@ -292,7 +294,7 @@ async def test_failed_segment_publishes_omission_before_shutdown(self): for index in range(60): observer.call_recording.capture(0, b"\0\0" * 480, 24000, 1, observed_ns=index * 20_000_000) self.assertEqual(observer.call_descriptors["call-0000"]["state"], "pending") - await asyncio.gather(*tuple(observer.call_recording.pending)) + await observer.call_recording.drain_exports() descriptor = observer.call_descriptors["call-0000"] self.assertEqual(descriptor["state"], "omitted") self.assertEqual(descriptor["reason"], "RecordingBusy") diff --git a/py/src/braintrust/integrations/pipecat/voice/test_realtime.py b/py/src/braintrust/integrations/pipecat/voice/test_realtime.py index 25c152855..8467eacb1 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_realtime.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_realtime.py @@ -98,18 +98,24 @@ async def send(message): await service._send_user_audio(frame) self.assertFalse(capture.invalid) self.assertEqual(len(capture.sent_ranges), 1) + turns = [] for index in range(270): turn = Span() item_id = str(index) capture.items[item_id] = {"audio_start_ms": index * 20, "audio_end_ms": (index + 1) * 20} capture.associations.append((turn, [item_id])) capture.publish() - self.assertIn(turn.span_id, observer.alignment.owners) + turns.append(turn) self.assertEqual(capture.items, {}) self.assertEqual(capture.associations, []) await service.send_client_event(SimpleNamespace(type="input_audio_buffer.clear")) capture.finish() - self.assertIn(turn.span_id, observer.alignment.owners) + await observer.call_recording.finish() + observer.alignment.publish() + for index, turn in enumerate(turns): + selection = turn.rows[-1]["metadata"]["audio.selection"] + self.assertEqual(selection["start_offset_ms"], index * 20) + self.assertEqual(selection["end_offset_ms"], (index + 1) * 20) finally: observer.hooks.close() observer.call_recording.clear() diff --git a/py/src/braintrust/integrations/pipecat/voice/test_turns.py b/py/src/braintrust/integrations/pipecat/voice/test_turns.py index 140f0369f..cebaf9cde 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_turns.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_turns.py @@ -120,7 +120,7 @@ async def test_tool_continuation_preserves_origin_after_next_user_starts(self): ["call-1"], ) await self.push(TTSStartedFrame(context_id="tts-1")) - self.assertIn(observer.tts["tts-1"]["span"], continuation["span"].children) + self.assertIn(observer.tts["tts-1"].span, continuation["span"].children) # A historical tool result does not relink a later context push. await self.push(LLMContextFrame(context), self.assistant) self.assertIsNone(observer.context_reply_to) @@ -175,8 +175,7 @@ async def test_interruption_ends_tts_and_transcription_does_not_guess_a_turn(sel state = self.observer.tts["interrupted"] await self.push(InterruptionFrame()) self.assertEqual(self.observer.tts, {}) - self.assertIn(state, self.observer.recordings) - self.assertEqual(state["span"].rows[-1]["metadata"]["contrib.pipecat.end_frame"], "InterruptionFrame") + self.assertEqual(state.span.rows[-1]["metadata"]["contrib.pipecat.end_frame"], "InterruptionFrame") await self.observer.finish() async def test_native_turn_metrics_ownership_and_limits(self): @@ -230,7 +229,7 @@ async def test_ttfb_routes_only_to_unambiguous_processor_operation(self): early_usage = self.observer.root.rows[-1]["metadata"]["contrib.pipecat.measurements"] assert early_usage == [{"type": "TTSUsageMetricsData", "processor": "tts", "model": None, "value": 25}] await self.push(TTSStartedFrame(context_id="a"), tts) - span = self.observer.tts["a"]["span"] + span = self.observer.tts["a"].span assert not any("contrib.pipecat.measurements" in row.get("metadata", {}) for row in span.rows) await self.push(MetricsFrame(data=[TTFBMetricsData(processor="tts", value=0.09)]), tts) assert span.rows[-1]["metadata"]["contrib.pipecat.ttfb"][0]["value"] == 0.09 diff --git a/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py b/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py index 6b8abc4b4..e5fd95839 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py @@ -178,7 +178,7 @@ async def push_context(): observer.turns.confirm("user") await aggregator._push_aggregation() observer.turns.stop("user", SimpleNamespace(content="order?", timestamp=str(index), user_id="user")) - await asyncio.gather(*capture.tasks) + await capture.jobs.drain() descriptors = [ r["metadata"]["audio.recordings"] for r in turn["span"].rows diff --git a/py/src/braintrust/integrations/pipecat/voice/user_capture.py b/py/src/braintrust/integrations/pipecat/voice/user_capture.py index 8f042f72a..64bceb8fe 100644 --- a/py/src/braintrust/integrations/pipecat/voice/user_capture.py +++ b/py/src/braintrust/integrations/pipecat/voice/user_capture.py @@ -12,6 +12,7 @@ from braintrust.audio.alignment import InputRanges from braintrust.audio.attachments import prepare_recording from braintrust.audio.budget import source_budget +from braintrust.audio.jobs import RecordingJobs from braintrust.audio.recording import encode_audio from braintrust.audio.timeline import ClipTimeline from braintrust.audio.worker import RecordingBusy, encode_in_worker @@ -59,6 +60,7 @@ def __init__(self, observer, stt, aggregator, native_value): self.omitted = 0 self.input_ranges = InputRanges() if observer.capture_user_audio else None self.segments = [] + self.jobs = RecordingJobs() original_start = observer.hooks.original(stt, "_handle_user_started_speaking") original_stop = observer.hooks.original(stt, "_handle_user_stopped_speaking") @@ -291,16 +293,30 @@ async def finish(self): self.create_stt(segment, self.observer.root) for batch in list(self.batches.values()): self.queue_completed(batch["turn"], force=True) - if getattr(self, "tasks", None): - await asyncio.gather(*tuple(self.tasks)) + await self.jobs.drain() self.observer.root.log(metadata={"braintrust.user_capture.events_omitted": self.omitted}) self.release() + async def close(self): + await self.jobs.drain() + self.release() + def queue_completed(self, turn, force=False): batch = self.batches.get(turn["span"].span_id) if not batch or batch.get("queued") or (not force and not turn.get("ended")): return batch["queued"] = True + + def release(): + self.release_segments(batch["segments"]) + self.batches.pop(turn["span"].span_id, None) + + self.jobs.submit( + lambda: self._publish_batch(batch), + release, + after_release=lambda: asyncio.to_thread(self.observer.logger.flush), + ) + if any(s["audio"] is not None for s in batch["segments"]): turn["span"].log( metadata={ @@ -309,11 +325,6 @@ def queue_completed(self, turn, force=False): ] } ) - if not hasattr(self, "tasks"): - self.tasks = set() - task = asyncio.create_task(self._publish_batch(batch)) - self.tasks.add(task) - task.add_done_callback(self.tasks.discard) async def _publish_batch(self, batch): turn, segments = batch["turn"], batch["segments"] @@ -370,10 +381,6 @@ async def _publish_batch(self, batch): ) turn["span"].log(metadata={"audio.recordings": [descriptor]}) - self.release_segments(segments) - self.batches.pop(turn["span"].span_id, None) - await asyncio.to_thread(self.observer.logger.flush) - def release_segments(self, segments): for segment in segments: if segment["audio"] is not None: From bd7df8d16d1ec56cb529b5c5cc0997d78b437940 Mon Sep 17 00:00:00 2001 From: David Elner Date: Wed, 7 Oct 2026 22:51:12 -0400 Subject: [PATCH 16/18] Refactored: Simplify audio recording lifecycle and consolidate Pipecat tests --- py/src/braintrust/audio/README.md | 20 -- py/src/braintrust/audio/__init__.py | 2 +- py/src/braintrust/audio/alignment.py | 177 +++--------------- py/src/braintrust/audio/attachments.py | 30 +-- py/src/braintrust/audio/export.py | 159 ++++++++++++++++ py/src/braintrust/audio/recording.py | 7 +- py/src/braintrust/audio/segments.py | 86 ++++++--- py/src/braintrust/audio/test_attachments.py | 25 --- py/src/braintrust/audio/test_dependencies.py | 6 +- py/src/braintrust/audio/test_recording.py | 40 +--- py/src/braintrust/audio/test_segments.py | 37 ++-- py/src/braintrust/audio/test_worker.py | 25 ++- py/src/braintrust/audio/timeline.py | 16 ++ .../pipecat/test_audio_cassettes.py | 37 +--- .../integrations/pipecat/test_pipecat.py | 27 --- .../pipecat/test_websocket_cassette.py | 22 +-- .../integrations/pipecat/voice/README.md | 37 ---- .../integrations/pipecat/voice/alignment.py | 10 +- .../pipecat/voice/instrumentation.py | 62 +----- .../integrations/pipecat/voice/realtime.py | 6 +- .../pipecat/voice/test_alignment.py | 32 +--- .../pipecat/voice/test_instrumentation.py | 73 +++----- .../integrations/pipecat/voice/test_setup.py | 20 +- .../integrations/pipecat/voice/test_turns.py | 14 ++ .../pipecat/voice/test_user_capture.py | 67 +++---- .../pipecat/voice/user_capture.py | 10 +- 26 files changed, 427 insertions(+), 620 deletions(-) delete mode 100644 py/src/braintrust/audio/README.md create mode 100644 py/src/braintrust/audio/export.py delete mode 100644 py/src/braintrust/audio/test_attachments.py delete mode 100644 py/src/braintrust/integrations/pipecat/voice/README.md diff --git a/py/src/braintrust/audio/README.md b/py/src/braintrust/audio/README.md deleted file mode 100644 index 02d9fdf47..000000000 --- a/py/src/braintrust/audio/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Shared audio recording components - -Internal SDK components, alongside the shared logger and other `braintrust` packages. Framework integrations depend on this package; it imports no framework. No new public setup API is introduced. - -```text -Framework integration audio/ logger.py - native lifecycle hooks recording: PCM/encoding - transport/sample association → alignment: sample ranges - turn/provider metadata budget: retained bytes - worker: bounded jobs - span + recording descriptors ─────────────────────────→ Attachment / flush -``` - -`CallRecording.capture(channel, pcm, sample_rate, channels, ...)` accepts signed 16-bit little-endian PCM and local observation times. The current two-channel recorder assumes continuous input on channel 0 and intermittent output on channel 1, produces 24 kHz stereo, and returns sample ranges on that clock. Integrations must verify those assumptions and preserve sample provenance. It is not a universal clock-reconciliation or media-playback layer. - -NumPy and SoundFile are imported only during encoding. Both WAV and Ogg currently use them; `braintrust[audio]` installs the optional dependencies. SoundFile uses CFFI and native libsndfile; Ogg requires a libsndfile build with Opus support. No FFmpeg subprocess is used. Simple existing PCM-to-WAV attachment helpers elsewhere in the SDK remain dependency-free. - -Capture budgets and worker admission are shared across integrations in the process. Source PCM and encoded attachments have separate process-wide budgets. Encoded bytes remain budgeted for the lifetime of the exporter-owned attachment. Neither budget alone bounds total RSS or encoder scratch space. Frame hooks, capture opt-in, metadata filtering, span parentage, and framework shutdown remain the integration's responsibility. Attachment transport stays in the existing SDK logger. - -Run `nox -s test_audio` for encoding, resource, and dependency-boundary tests. Dependency-free tests also run in `test_core`; that session excludes the codec-dependent recording tests. diff --git a/py/src/braintrust/audio/__init__.py b/py/src/braintrust/audio/__init__.py index 16a6c1c0c..420bb1577 100644 --- a/py/src/braintrust/audio/__init__.py +++ b/py/src/braintrust/audio/__init__.py @@ -1,4 +1,4 @@ -"""Shared audio configuration; codecs load only when recording is enabled.""" +"""Public audio configuration. Recording and encoding modules are SDK internals.""" from .options import RecordingOptions diff --git a/py/src/braintrust/audio/alignment.py b/py/src/braintrust/audio/alignment.py index 655475df7..e99a5427c 100644 --- a/py/src/braintrust/audio/alignment.py +++ b/py/src/braintrust/audio/alignment.py @@ -1,11 +1,8 @@ """Bounded sample ranges and recording selections shared by voice integrations.""" from collections import deque -from dataclasses import dataclass, field -from braintrust.logger import Span - -from .timeline import ClipTimeline +from .timeline import ms_to_samples, samples_to_ms def merge_ranges(ranges): @@ -20,151 +17,37 @@ def merge_ranges(ranges): return result -@dataclass -class _SelectionOwner: - span: Span - channel: int - ranges: list[list[int]] = field(default_factory=list) - published: list[dict] = field(default_factory=list) - - -@dataclass -class _Clip: - timeline: ClipTimeline = field(default_factory=ClipTimeline) - owner: Span | None = None - descriptor: dict | None = None - - -class Alignment: - def __init__(self, root, recording): - self.root = root - self.recording = recording - self._owners: dict[str, _SelectionOwner] = {} - self._contexts = {} - self._clips: dict[str, _Clip] = {} - self._dirty_clips: set[str] = set() - self.count = 0 - self.omitted = 0 - self.pending_owners = set() - - def add(self, owner, ranges, channel): - if self.recording.reason: - return - item = self._owners.setdefault(owner.span_id, _SelectionOwner(owner, channel)) - for interval in ranges: - if item.ranges and item.ranges[-1][0] <= interval[0] <= item.ranges[-1][1]: - item.ranges[-1][1] = max(item.ranges[-1][1], interval[1]) - continue - if self.count >= 32000: - self.omitted += 1 - continue - item.ranges.append(list(interval)) - self.count += 1 - - if item.ranges: - self.pending_owners.add(owner.span_id) - - def begin_output(self, context, owners): - if len(self._contexts) >= 16000: - self.omitted += 1 - return - self._contexts[context] = owners - self._clips[owners[0].span_id] = _Clip() - - def output_owners(self, context): - return self._contexts.get(context, []) - - def end_output(self, context): - self._contexts.pop(context, None) - - def add_clip_range(self, owner, start_ms, end_ms, call_start_ms, call_end_ms): - clip = self._clips.get(owner.span_id) - if clip is not None: - clip.timeline.add(start_ms, end_ms, call_start_ms, call_end_ms) - self._dirty_clips.add(owner.span_id) - - def publish_clip(self, owner, descriptor): - clip = self._clips.get(owner.span_id) - if clip is not None: - clip.owner, clip.descriptor = owner, descriptor - self._dirty_clips.add(owner.span_id) - self.publish_clips() - - def publish_clips(self): - origin = getattr(self.recording, "origin_unix_ms", None) - if origin is None: - return - for span_id in tuple(self._dirty_clips): - clip = self._clips[span_id] - if clip.descriptor is None: - continue - timeline = clip.timeline.descriptor(origin) - if timeline != clip.descriptor.get("timeline"): - clip.descriptor["timeline"] = timeline - clip.owner.log(metadata={"audio.recordings": [dict(clip.descriptor)]}) - self._dirty_clips.discard(span_id) - - def publish(self): - self.publish_clips() - segments = getattr(self.recording, "completed", None) - if segments is None: - segments = ( - [{"id": "call", "start_ms": 0, "end_ms": max(self.recording.ends), "state": "ready"}] - if not self.recording.reason and self.recording.packets - else [] - ) - resolved = merge_ranges( - [ - [round(s["start_ms"] * 24), round(s["end_ms"] * 24)] - for s in segments - if s["state"] in {"ready", "omitted"} - ] - ) - for span_id in tuple(self.pending_owners): - state = self._owners[span_id] - owner, channel, ranges = state.span, state.channel, state.ranges - selections = list(state.published) - merged = merge_ranges(ranges) - pending = [] - for segment in segments: - if segment["state"] != "ready": - continue - lower, upper = round(segment["start_ms"] * 24), round(segment["end_ms"] * 24) - for start, end in merged: - start, end = max(start, lower), min(end, upper) - if end > start: - selections.append( - { - "recording_span_id": self.root.span_id, - "recording_id": segment["id"], - "start_offset_ms": (start - lower) / 24, - "end_offset_ms": (end - lower) / 24, - "channel_index": channel, - } - ) - for start, end in ranges: - for lower, upper in resolved: - if lower > start: - pending.append([start, min(lower, end)]) - start = max(start, upper) if lower < end else end - if start >= end: - break - if start < end: - pending.append([start, end]) - self.count -= len(ranges) - len(pending) - ranges[:] = pending - selections = [dict(items) for items in dict.fromkeys(tuple(s.items()) for s in selections)] - if not pending: - self.pending_owners.discard(span_id) - if selections and selections != state.published: - state.published = selections - owner.log( - metadata={ - "audio.selections": selections, - "audio.selection": selections[0] if len(selections) == 1 else None, +def resolve_selections(ranges, segments, resolved, recording_span_id, channel): + """Map call samples into ready files; retain ranges whose files are pending.""" + selections = [] + merged = merge_ranges(ranges) + pending = [] + for segment in segments: + if segment["state"] != "ready": + continue + lower, upper = ms_to_samples(segment["start_ms"]), ms_to_samples(segment["end_ms"]) + for start, end in merged: + start, end = max(start, lower), min(end, upper) + if end > start: + selections.append( + { + "recording_span_id": recording_span_id, + "recording_id": segment["id"], + "start_offset_ms": samples_to_ms(start - lower), + "end_offset_ms": samples_to_ms(end - lower), + "channel_index": channel, } ) - self.root.log(metadata={"braintrust.alignment.ranges_omitted": self.omitted}) + for start, end in ranges: + for lower, upper in resolved: + if lower > start: + pending.append([start, min(lower, end)]) + start = max(start, upper) if lower < end else end + if start >= end: + break + if start < end: + pending.append([start, end]) + return selections, pending class InputRanges: diff --git a/py/src/braintrust/audio/attachments.py b/py/src/braintrust/audio/attachments.py index 237c3a7a6..7255cc565 100644 --- a/py/src/braintrust/audio/attachments.py +++ b/py/src/braintrust/audio/attachments.py @@ -1,38 +1,12 @@ -"""Bound encoded recordings while queued or retained by the exporter.""" - -import weakref +"""Prepare encoded recordings for the standard attachment exporter.""" from braintrust.logger import Attachment -from .budget import ByteBudget -from .worker import RecordingBusy - - -encoded_budget = ByteBudget(32 * 1024 * 1024) - - -class RecordingAttachment(Attachment): - def __init__(self, *, data, filename, content_type): - self._retained_bytes = 0 - if not encoded_budget.reserve(len(data)): - raise RecordingBusy("encoded_audio_capacity") - self._retained_bytes = len(data) - super().__init__(data=data, filename=filename, content_type=content_type) - - def _init_uploader(self): - # The base uploader closes over its owner. Avoid keeping uploaded bytes - # and their budget reservation alive until cyclic GC happens to run. - return Attachment._init_uploader(weakref.proxy(self)) - - def __del__(self): - encoded_budget.release(getattr(self, "_retained_bytes", 0)) - self._retained_bytes = 0 - def prepare_recording(name, function, *args): encoded = function(*args) if encoded is not None: - encoded["attachment"] = RecordingAttachment( + encoded["attachment"] = Attachment( data=encoded["data"], filename=f"{name}.{encoded['extension']}", content_type=encoded["mime_type"], diff --git a/py/src/braintrust/audio/export.py b/py/src/braintrust/audio/export.py new file mode 100644 index 000000000..36f2f50b1 --- /dev/null +++ b/py/src/braintrust/audio/export.py @@ -0,0 +1,159 @@ +"""Serialize recording manifests without retaining their audio payloads.""" + +from dataclasses import dataclass, field + +from braintrust.logger import Span + +from .alignment import merge_ranges, resolve_selections +from .timeline import ClipTimeline, ms_to_samples + + +def segment_descriptor(segment, span_id, sources): + descriptor = { + "id": segment.segment_id, + "recording_group_id": "call", + "state": segment.state, + "sources": sources, + "timeline": { + "origin_unix_ms": segment.origin_unix_ms, + "recording_start_offset_ms": segment.start_ms, + "basis": "input_sample_clock_and_output_write_observation", + }, + } + if segment.state == "ready": + descriptor.update(segment.metadata) + descriptor["attachment"] = {"span_id": span_id, "ref": f"/input/audio/{segment.segment_id}"} + elif segment.state == "omitted": + descriptor["reason"] = segment.reason + descriptor["gaps"] = [ + { + "start_offset_ms": segment.start_ms, + "end_offset_ms": segment.end_ms, + "reason": segment.reason, + } + ] + return descriptor + + +@dataclass +class _SelectionOwner: + span: Span + channel: int + ranges: list[list[int]] = field(default_factory=list) + published: list[dict] = field(default_factory=list) + + +@dataclass +class _Clip: + timeline: ClipTimeline = field(default_factory=ClipTimeline) + owner: Span | None = None + descriptor: dict | None = None + + +class Alignment: + """Publish resolved selections and clip timelines to their owning spans.""" + + def __init__(self, root, recording): + self.root = root + self.recording = recording + self._owners: dict[str, _SelectionOwner] = {} + self._contexts = {} + self._clips: dict[str, _Clip] = {} + self._dirty_clips: set[str] = set() + self.count = 0 + self.omitted = 0 + self.pending_owners = set() + + def add(self, owner, ranges, channel): + if self.recording.reason: + return + item = self._owners.setdefault(owner.span_id, _SelectionOwner(owner, channel)) + for interval in ranges: + if item.ranges and item.ranges[-1][0] <= interval[0] <= item.ranges[-1][1]: + item.ranges[-1][1] = max(item.ranges[-1][1], interval[1]) + continue + if self.count >= 32000: + self.omitted += 1 + continue + item.ranges.append(list(interval)) + self.count += 1 + + if item.ranges: + self.pending_owners.add(owner.span_id) + + def begin_output(self, context, owners): + if len(self._contexts) >= 16000: + self.omitted += 1 + return + self._contexts[context] = owners + self._clips[owners[0].span_id] = _Clip() + + def output_owners(self, context): + return self._contexts.get(context, []) + + def end_output(self, context): + self._contexts.pop(context, None) + + def add_clip_range(self, owner, start_ms, end_ms, call_start_ms, call_end_ms): + clip = self._clips.get(owner.span_id) + if clip is not None: + clip.timeline.add(start_ms, end_ms, call_start_ms, call_end_ms) + self._dirty_clips.add(owner.span_id) + + def publish_clip(self, owner, descriptor): + clip = self._clips.get(owner.span_id) + if clip is not None: + clip.owner, clip.descriptor = owner, descriptor + self._dirty_clips.add(owner.span_id) + self.publish_clips() + + def publish_clips(self): + origin = getattr(self.recording, "origin_unix_ms", None) + if origin is None: + return + for span_id in tuple(self._dirty_clips): + clip = self._clips[span_id] + if clip.descriptor is None: + continue + timeline = clip.timeline.descriptor(origin) + if timeline != clip.descriptor.get("timeline"): + clip.descriptor["timeline"] = timeline + clip.owner.log(metadata={"audio.recordings": [dict(clip.descriptor)]}) + self._dirty_clips.discard(span_id) + + def publish(self): + self.publish_clips() + segments = getattr(self.recording, "completed", None) + if segments is None: + segments = ( + [{"id": "call", "start_ms": 0, "end_ms": max(self.recording.ends), "state": "ready"}] + if not self.recording.reason and self.recording.packets + else [] + ) + resolved = merge_ranges( + [ + [ms_to_samples(s["start_ms"]), ms_to_samples(s["end_ms"])] + for s in segments + if s["state"] in {"ready", "omitted"} + ] + ) + for span_id in tuple(self.pending_owners): + state = self._owners[span_id] + owner, channel, ranges = state.span, state.channel, state.ranges + selections = list(state.published) + additions, pending = resolve_selections(ranges, segments, resolved, self.root.span_id, channel) + selections.extend(additions) + self.count -= len(ranges) - len(pending) + ranges[:] = pending + selections = [dict(items) for items in dict.fromkeys(tuple(s.items()) for s in selections)] + if not pending: + self.pending_owners.discard(span_id) + if selections and selections != state.published: + state.published = selections + owner.log( + metadata={ + "audio.selections": selections, + "audio.selection": selections[0] if len(selections) == 1 else None, + } + ) + self.root.log(metadata={"braintrust.alignment.ranges_omitted": self.omitted}) diff --git a/py/src/braintrust/audio/recording.py b/py/src/braintrust/audio/recording.py index 361cffb28..fa0c0445f 100644 --- a/py/src/braintrust/audio/recording.py +++ b/py/src/braintrust/audio/recording.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from .budget import source_budget +from .timeline import ms_to_samples, pcm_bytes_to_ms @dataclass(frozen=True) @@ -117,7 +118,7 @@ def capture(self, channel, pcm, sample_rate, channels, *, observed_ns=None, obse if channel == 0 and self.started[channel] else max((observed_ns - self.origin_ns) / 1e6, self.ends[channel]) ) - duration = len(pcm) / 2 / channels / sample_rate * 1000 + duration = pcm_bytes_to_ms(len(pcm), sample_rate, channels) # Transport frames can contain seconds of audio (e.g. shutdown silence). # Bound retained bytes and timeline duration, not the transport's packetization. if start + duration > self.max_duration_ms: @@ -135,8 +136,8 @@ def capture(self, channel, pcm, sample_rate, channels, *, observed_ns=None, obse self.started[channel] = True # Coordinates match encode() placement, including resampling rounding. return { - "start": round(start * 24), - "end": round(start * 24) + round(duration * 24), + "start": ms_to_samples(start), + "end": ms_to_samples(start) + ms_to_samples(duration), } def encode(self, audio_format="ogg"): diff --git a/py/src/braintrust/audio/segments.py b/py/src/braintrust/audio/segments.py index 7393b8021..2d80d5a00 100644 --- a/py/src/braintrust/audio/segments.py +++ b/py/src/braintrust/audio/segments.py @@ -2,15 +2,56 @@ import asyncio import logging +from dataclasses import dataclass, field from .attachments import prepare_recording from .budget import source_budget from .jobs import RecordingJobs from .options import RecordingOptions from .recording import CallRecording, Packet +from .timeline import ms_to_samples from .worker import encode_in_worker +@dataclass +class AudioSegment: + """One manifest throughout encoding and export; PCM is sealed at rotation. + + Only the worker releases submitted PCM. Encoded bytes live through export; + afterwards only small metadata remains on the call's manifest. + """ + + segment_id: str + start_ms: float + end_ms: float + origin_unix_ms: float + source: CallRecording + state: str = "pending" + reason: str | None = None + encoded: dict | None = None + metadata: dict = field(default_factory=dict) + + @property + def bytes(self): + return self.source.bytes + + def clear(self): + self.source.clear() + + def ready(self, encoded): + self.encoded = encoded + self.metadata = {key: encoded[key] for key in ("mime_type", "duration_ms", "channel_count")} + self.state = "ready" + + def omit(self, reason): + self.state, self.reason = "omitted", reason + self.encoded = None + self.metadata.clear() + + def interval(self): + return {"id": self.segment_id, "start_ms": self.start_ms, "end_ms": self.end_ms, "state": self.state} + + class SegmentedRecording(CallRecording): """Capture remains synchronous; detached segments belong to encoder workers. @@ -31,11 +72,15 @@ def __init__(self, *, options=None, on_segment=None, on_pending=None, audio_form self.audio_format = audio_format self.start_ms = 0.0 self.sequence = 0 - self.completed = [] + self.segments: list[AudioSegment] = [] self.jobs = RecordingJobs() self.inflight = [] self._finish_task = None + @property + def completed(self): + return [segment.interval() for segment in self.segments if segment.state != "pending"] + @property def retained_bytes(self): return self.bytes + sum(segment.bytes for segment in self.inflight) @@ -56,7 +101,7 @@ def capture(self, channel, pcm, sample_rate, channels, *, observed_ns=None, obse if interval is None: return None # Never rewrite audio that was already sealed/exported. - if interval["start"] < round(self.start_ms * 24): + if interval["start"] < ms_to_samples(self.start_ms): self.omit("capture_clock_discontinuity") packet = self.packets.pop() self.bytes -= len(packet.pcm) @@ -78,20 +123,18 @@ def _rotate(self, cut): if len(self.jobs) >= 2: self.omit("recording_export_capacity") return - segment = CallRecording() - segment.start_ms = self.start_ms - segment.segment_id = f"call-{self.sequence:04d}" - segment.origin_unix_ms = self.origin_unix_ms + snapshot = CallRecording() + segment = AudioSegment(f"call-{self.sequence:04d}", self.start_ms, cut, self.origin_unix_ms, snapshot) later = [] for packet in self.packets: size = len(packet.pcm) // (2 * packet.channels) before = min(size, max(0, round((cut - packet.start_ms) * packet.sample_rate / 1000))) if before: data = packet.pcm if before == size else packet.pcm[: before * 2 * packet.channels] - segment.packets.append( + snapshot.packets.append( Packet(packet.channel, packet.start_ms - self.start_ms, data, packet.sample_rate, packet.channels) ) - segment.bytes += len(data) + snapshot.bytes += len(data) if before < size: data = packet.pcm if not before else packet.pcm[before * 2 * packet.channels :] later.append( @@ -103,13 +146,14 @@ def _rotate(self, cut): packet.channels, ) ) - segment.ends = [cut - self.start_ms, cut - self.start_ms] - segment.seal() + snapshot.ends = [cut - self.start_ms, cut - self.start_ms] + snapshot.seal() self.packets = later self.bytes -= segment.bytes self.start_ms = cut self.sequence += 1 self.inflight.append(segment) + self.segments.append(segment) def release(): segment.clear() @@ -120,36 +164,28 @@ def release(): self.on_pending(segment) async def _export(self, segment): - descriptor = { - "id": segment.segment_id, - "start_ms": segment.start_ms, - "end_ms": segment.start_ms + max(segment.ends), - "state": "pending", - } - def encode(): try: - return prepare_recording(segment.segment_id, segment.encode, self.audio_format) + return prepare_recording(segment.segment_id, segment.source.encode, self.audio_format) finally: # Only the encoder may release a segment handed to its thread. segment.clear() try: encoded = await encode_in_worker(encode) - descriptor["state"] = "ready" - self.completed.append(descriptor) + segment.ready(encoded) if self.on_segment: - await self.on_segment(segment, encoded) + await self.on_segment(segment) except Exception as error: # noqa: BLE001 - recording cannot stop speech - descriptor.update(state="omitted", reason=type(error).__name__) - if descriptor not in self.completed: - self.completed.append(descriptor) + segment.omit(type(error).__name__) self.omit("segment_export_failed") if self.on_segment: try: - await self.on_segment(segment, None) + await self.on_segment(segment) except Exception: # noqa: BLE001 - failure reporting must not break the call logging.getLogger(__name__).warning("Failed to publish recording omission", exc_info=True) + finally: + segment.encoded = None def release_idle_buffer(self): """Release unsubmitted audio unless a final drain still owns it.""" diff --git a/py/src/braintrust/audio/test_attachments.py b/py/src/braintrust/audio/test_attachments.py deleted file mode 100644 index 6bd1eee73..000000000 --- a/py/src/braintrust/audio/test_attachments.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Encoded audio stays budgeted for as long as the exporter retains it.""" - -import gc - -import pytest - -from .attachments import RecordingAttachment, encoded_budget, prepare_recording -from .worker import RecordingBusy - - -def test_encoded_budget_follows_attachment_lifetime(monkeypatch): - previous = encoded_budget.used - monkeypatch.setattr(encoded_budget, "maximum", previous + 4) - attachment = RecordingAttachment(data=b"1234", filename="clip.wav", content_type="audio/wav") - with pytest.raises(RecordingBusy): - RecordingAttachment(data=b"x", filename="clip.wav", content_type="audio/wav") - assert encoded_budget.used == previous + 4 - del attachment - # Completed uploads must not wait for cyclic GC to return capacity. - assert encoded_budget.used == previous - gc.collect() - - -def test_prepare_recording_preserves_disabled_result(): - assert prepare_recording("call", lambda: None) is None diff --git a/py/src/braintrust/audio/test_dependencies.py b/py/src/braintrust/audio/test_dependencies.py index ab5618bc2..f6fdc838f 100644 --- a/py/src/braintrust/audio/test_dependencies.py +++ b/py/src/braintrust/audio/test_dependencies.py @@ -21,7 +21,11 @@ def find_spec(self, fullname, path=None, target=None): from braintrust.audio import worker, RecordingOptions from braintrust.audio.segments import SegmentedRecording import asyncio -asyncio.run(SegmentedRecording(enabled=False, options=RecordingOptions()).finish()) +disabled = SegmentedRecording(enabled=False, options=RecordingOptions()) +assert disabled.capture(0, b"\\0\\0" * 480, 24000, 1) is None +asyncio.run(disabled.finish()) +assert disabled.retained_bytes == 0 +assert disabled.completed == [] assert worker._executor is None recording = CallRecording() interval = recording.capture(0, b"\\0\\0" * 480, 24000, 1, observed_ns=0) diff --git a/py/src/braintrust/audio/test_recording.py b/py/src/braintrust/audio/test_recording.py index 7d2224843..70ea6483b 100644 --- a/py/src/braintrust/audio/test_recording.py +++ b/py/src/braintrust/audio/test_recording.py @@ -32,43 +32,23 @@ def test_input_anchor_and_real_silence_preserve_output_pauses(self): recording.capture(0, **frame([1000] * 320), observed_ns=100_000_000) recording.capture(0, **frame([0] * 320), observed_ns=125_000_000) recording.capture(0, **frame([1000] * 320), observed_ns=126_000_000) + recording.capture(1, **frame([-2000] * 960, 24000), observed_ns=140_000_000) recording.capture(1, **frame([-2000] * 320), observed_ns=200_000_000) + recording.seal() + self.assertIsNone(recording.capture(0, **frame([123] * 320))) result = recording.encode("wav") - samples, _ = sf.read(io.BytesIO(result["data"]), dtype="int16", always_2d=True) - self.assertEqual([p.start_ms for p in recording.packets], [0, 100, 120, 140, 200]) + samples, rate = sf.read(io.BytesIO(result["data"]), dtype="int16", always_2d=True) + self.assertEqual(rate, 24000) + self.assertEqual(samples.shape, (5280, 2)) self.assertTrue(np.all(samples[:2400, 0] == 0)) self.assertTrue(np.all(samples[2880:3360, 0] == 0)) self.assertTrue(np.all(samples[3360:3840, 0] == 1000)) - self.assertTrue(np.all(samples[480:4800, 1] == 0)) + self.assertTrue(np.all(samples[480:3360, 1] == 0)) + self.assertTrue(np.all(samples[3360:3840] == [1000, -2000])) + self.assertTrue(np.all(samples[3840:, 0] == 0)) + self.assertTrue(np.all(samples[4320:4800, 1] == 0)) self.assertEqual(result["duration_ms"], 220) - def test_two_sources_keep_overlap_silence_and_duration(self): - recording = CallRecording() - recording.capture(0, **frame([1000] * 1600), observed_ns=0, observed_unix_ms=123456) - recording.capture(1, **frame([-2000] * 2400, 24000), observed_ns=50_000_000) - result = recording.encode("wav") - samples, rate = sf.read(io.BytesIO(result["data"]), dtype="int16", always_2d=True) - self.assertEqual(rate, 24000) - self.assertEqual(samples.shape, (3600, 2)) - self.assertEqual(result["duration_ms"], 150) - self.assertTrue(np.all(samples[:1200, 1] == 0)) - self.assertTrue(np.all(samples[1200:2400] == [1000, -2000])) - self.assertTrue(np.all(samples[2400:, 0] == 0)) - - def test_disabled_and_limits_release_all_audio(self): - recording = CallRecording(max_bytes=10) - recording.capture(0, **frame([1] * 6), observed_ns=0) - self.assertEqual(recording.reason, "capture_byte_limit") - self.assertEqual(recording.packets, []) - self.assertEqual(recording.bytes, 0) - self.assertIsNone(recording.encode()) - disabled = CallRecording(enabled=False) - disabled.capture(0, **frame([1] * 100), observed_ns=0) - self.assertEqual(disabled.bytes, 0) - duration = CallRecording(max_duration_ms=1) - duration.capture(0, **frame([1] * 160), observed_ns=0) - self.assertEqual(duration.reason, "duration_limit") - def test_ogg_decodes_to_original_length_and_is_smaller(self): samples = (np.sin(np.arange(24000) * 2 * np.pi * 440 / 24000) * 12000).astype("= llm_span["metrics"]["completion_tokens"] -def test_setup_and_wrap_pipeline_worker_are_idempotent(): - Pipeline = _import("pipecat.pipeline.pipeline.Pipeline") - PipelineWorker = _import("pipecat.pipeline.worker.PipelineWorker") - IdentityFilter = _import("pipecat.processors.filters.identity_filter.IdentityFilter") - - assert setup_pipecat(project_name="test-project-pipecat-py-tracing") - assert setup_pipecat(project_name="test-project-pipecat-py-tracing") - assert setup_pipecat(project_name="test-project-pipecat-py-tracing", capture_audio_attachments=True) - assert wrap_pipeline_worker(PipelineWorker) is PipelineWorker - - capturing_worker = _make_worker(Pipeline([IdentityFilter()])) - capturing_observers = getattr(getattr(capturing_worker, "_observer"), "_observers") - capturing_bt_observer = next( - observer for observer in capturing_observers if isinstance(observer, BraintrustPipecatObserver) - ) - assert capturing_bt_observer.capture_audio_attachments is True - - assert setup_pipecat(project_name="test-project-pipecat-py-tracing", capture_audio_attachments=False) - explicit_observer = BraintrustPipecatObserver() - worker = _make_worker(Pipeline([IdentityFilter()]), observers=[explicit_observer]) - worker_observer = getattr(worker, "_observer") - observers = getattr(worker_observer, "_observers") - braintrust_observers = [observer for observer in observers if isinstance(observer, BraintrustPipecatObserver)] - assert braintrust_observers == [explicit_observer] - - @pytest.mark.vcr @pytest.mark.skipif(__import__("sys").version_info < (3, 11), reason="Pipecat AI 1.x requires Python 3.11+") def test_auto_instrument_pipecat_subprocess(): diff --git a/py/src/braintrust/integrations/pipecat/test_websocket_cassette.py b/py/src/braintrust/integrations/pipecat/test_websocket_cassette.py index 166635e98..c900310f3 100644 --- a/py/src/braintrust/integrations/pipecat/test_websocket_cassette.py +++ b/py/src/braintrust/integrations/pipecat/test_websocket_cassette.py @@ -8,25 +8,13 @@ @pytest.mark.asyncio -async def test_websocket_cassette_preserves_ids_and_rejects_changed_payload(tmp_path): +async def test_websocket_cassette_rejects_changed_payload(tmp_path): path = tmp_path / "socket.json" path.write_text( json.dumps( { "endpoint": "wss://example.test", "events": [ - { - "direction": "send", - "message": { - "type": "conversation.item.create", - "event_id": "old-event", - "item": {"id": "old-item", "text": "hello"}, - }, - }, - { - "direction": "receive", - "message": {"type": "conversation.item.created", "item": {"id": "old-item"}}, - }, { "direction": "send", "message": {"type": "response.create", "response": {"modalities": ["audio"]}}, @@ -36,14 +24,6 @@ async def test_websocket_cassette_preserves_ids_and_rejects_changed_payload(tmp_ ) ) cassette = WebSocketCassette(path) - await cassette.send( - json.dumps( - {"type": "conversation.item.create", "event_id": "new-event", "item": {"id": "new-item", "text": "hello"}} - ) - ) - assert json.loads(await anext(cassette))["item"]["id"] == "new-item" - with pytest.raises(AssertionError, match="not fully consumed"): - cassette.verify() with pytest.raises(AssertionError, match="request mismatch"): await cassette.send(json.dumps({"type": "response.create", "response": {"modalities": ["text"]}})) with pytest.raises(AssertionError, match="replay failed"): diff --git a/py/src/braintrust/integrations/pipecat/voice/README.md b/py/src/braintrust/integrations/pipecat/voice/README.md deleted file mode 100644 index ff5523ed4..000000000 --- a/py/src/braintrust/integrations/pipecat/voice/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Pipecat voice instrumentation - -The existing public setup installs one observer and discovers a supported voice pipeline. Shared recording, sample ranges, worker admission, and byte budgets live in [`braintrust.audio`](../../../audio/README.md). - -```python -from braintrust import init_logger -from braintrust.integrations.pipecat import setup_pipecat - -init_logger(project="voice") -setup_pipecat(capture_audio_attachments=True, audio_format="ogg") -# Construct and run the existing PipelineWorker normally. -# Its observer cleanup finalizes recordings before flushing. -``` - -Install `braintrust[audio]` for Ogg/Opus or WAV encoding. Audio defaults off. Existing user/agent capture arguments and environment variables still apply independently. With audio disabled, no PCM retention, sample-tracking hooks, encoder jobs, or attachments are created. Transcripts and native metadata remain observable. - -| Contract | Behavior | -|---|---| -| Native voice path | Pipecat 1.12.0, one input/output transport, universal user/assistant aggregators, segmented STT or OpenAI Realtime. Other pipeline shapes/versions retain existing frame tracing. | -| Public interfaces | `setup_pipecat`, automatic integration setup, `wrap_pipeline_worker`, and an explicitly supplied `BraintrustPipecatObserver` share the same injection path. `trace_turns=False` retains the existing observer behavior. | -| Trace | `user_turn`, `assistant_turn`, `stt`, `llm_response`, `tts`, and actual tool names retain native boundaries and metadata. Realtime audio uses `pipecat.audio_output`; tool-only model responses can be pipeline children. `turn.id` / `turn.reply_to` and `continuation.tool_call_ids` preserve observed associations. | -| Audio | Combined stereo call; segmented-STT caller clips on user turns; generated clips on TTS spans. Selections reference successfully captured samples. Clip timelines map file positions to the call; padding and unwritten output remain unmapped. Accepted jobs publish pending descriptors before ready/omitted updates. | -| TTFB | Native `contrib.pipecat.ttfb` measurements follow TTS request identity when available, otherwise the emitting processor's unique active operation. STT measurements follow `run_stt` into its eventual span. Ambiguous/unmatched metrics remain on the pipeline; arrays are bounded to 32 with omission counts. | -| Turn detection | Native analyzer predictions in `contrib.pipecat.turn_metrics` on the user turn; preserve processor, metric class, confidence, completion and reported milliseconds. Retain 32 predictions and count omissions. Unassociated observations remain on the pipeline. | -| Measurements | `contrib.pipecat.measurements` retains native metric types and payloads on their operation. TTS metrics emitted inside synthesis follow the native request/context ID, including before queued audio starts or after it stops. Other measurements require a unique active operation; unattributable measurements remain on the pipeline. Each list retains at most 32 entries with `braintrust.measurements.omitted` counting excess observations. Metrics are not duplicated as frame events. | -| Request routing | At most 256 early metrics wait for their matching TTS span; 64 recently closed TTS targets accept late delivery. Overflow or requests that never open a span fall back to pipeline metadata. Request tags are private, consumed before export, and carry no audio. Hooks are removed at shutdown. | -| Events and configuration | Initial service/configuration observations are stored once in `contrib.pipecat.configuration` (up to 32 source/type pairs). Changes, speech boundaries, interruptions, cancellation, and errors retain compact timestamped events; routine start/end/connection notifications and startup zero metrics are excluded. Event payloads omit transport bookkeeping and empty fields. The pipeline-wide event budget remains 256 entries / 64 KiB. | -| Input alignment | Segmented STT input provenance or successful OpenAI input writes plus server-VAD item offsets. Clears/reconnects invalidate unsupported mappings. | -| Output alignment | Equal-rate mono PCM without a mixer. Other paths retain recordings without asserting unsupported selections. | -| Memory | Configurable 32 MiB retained call PCM by default; separate 8 MiB caller/generated source limits; 64 MiB shared source budget; 32 MiB encoded-attachment budget. These are retained-data bounds, not total RSS bounds. | -| Recording limits | Rotate call segments every 60 seconds or at half the call buffer limit; stop capture at 30 minutes by default. Configure these with `RecordingOptions`. Completed segments and valid prefixes survive limits. | -| Pending capture | Completed caller and generated clips release their job capacity and source bytes. Published selections release pending-range capacity. Realtime turn associations retire when published; adjacent input sample ranges compact, and queued output frames carry their own context. Buffer capacity is reusable throughout the call. | -| Admission | One encoder running, one queued; bounded packet/clip metadata. Overload produces an omission reason. | -| Cleanup | Seal capture, restore installed hooks, end operation spans, encode off-loop, update attachments, then flush. Repeated finalization shares one task; cancelling a waiter does not cancel its work. | -| Privacy | Standard span customizers apply to native metadata, messages, tool arguments/results, and attachment updates. Audio bytes are excluded from event metadata. | - -Run `nox -s test_audio 'test_pipecat(latest)' 'test_pipecat(1.3.0)'`. Recorded OpenAI response objects cover tool requests and continuations; transport/queue tests cover interruption, padding, failed writes, and recording opt-out. The `voice` modules are integration internals, not another customer setup API. diff --git a/py/src/braintrust/integrations/pipecat/voice/alignment.py b/py/src/braintrust/integrations/pipecat/voice/alignment.py index bdb941f93..8ca2299f9 100644 --- a/py/src/braintrust/integrations/pipecat/voice/alignment.py +++ b/py/src/braintrust/integrations/pipecat/voice/alignment.py @@ -3,6 +3,8 @@ from collections import deque from contextvars import ContextVar +from braintrust.audio.timeline import pcm_bytes_to_ms, samples_to_ms + def instrument_output(output, alignment, frame_context=None, hooks=None): """Track equal-rate mono PCM through native chunking without copying audio. @@ -146,10 +148,10 @@ async def write(frame): if owners: alignment.add_clip_range( owners[0], - start / 48, - (start + size) / 48, - ranges[0][0] / 24, - ranges[0][1] / 24, + pcm_bytes_to_ms(start, frame.sample_rate, frame.num_channels), + pcm_bytes_to_ms(start + size, frame.sample_rate, frame.num_channels), + samples_to_ms(ranges[0][0]), + samples_to_ms(ranges[0][1]), ) for owner in owners: alignment.add(owner, ranges, 1) diff --git a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py index 8c673803a..5c56cca41 100644 --- a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py +++ b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py @@ -8,7 +8,7 @@ import wave from collections import Counter, deque -from braintrust.audio.alignment import Alignment +from braintrust.audio.export import Alignment, segment_descriptor from braintrust.audio.segments import SegmentedRecording from pipecat.observers.base_observer import BaseObserver # pylint: disable=import-error @@ -152,7 +152,6 @@ def __init__( on_segment=self._publish_call_segment, on_pending=self._pending_call_segment, ) - self.call_descriptors = {} self.realtime = None self.input_processor = None self.capture_transport = False @@ -672,67 +671,22 @@ def _call_sources(self): ] def _pending_call_segment(self, segment): - self.call_descriptors[segment.segment_id] = { - "id": segment.segment_id, - "recording_group_id": "call", - "state": "pending", - "sources": self._call_sources(), - "timeline": { - "origin_unix_ms": segment.origin_unix_ms, - "recording_start_offset_ms": segment.start_ms, - "basis": "input_sample_clock_and_output_write_observation", - }, - } self._publish_call_manifest() - async def _publish_call_segment(self, segment, encoded): - if encoded is None: + async def _publish_call_segment(self, segment): + if segment.encoded is None: self._publish_call_manifest() return - recording_id = segment.segment_id - self.call_descriptors[recording_id] = { - "id": recording_id, - "recording_group_id": "call", - "state": "ready", - "attachment": {"span_id": self.root.span_id, "ref": f"/input/audio/{recording_id}"}, - "mime_type": encoded["mime_type"], - "duration_ms": encoded["duration_ms"], - "channel_count": 2, - "sources": self._call_sources(), - "timeline": { - "origin_unix_ms": self.call_recording.origin_unix_ms, - "recording_start_offset_ms": segment.start_ms, - "basis": "input_sample_clock_and_output_write_observation", - }, - } - self.root.log(input={"audio": {recording_id: encoded["attachment"]}}) + self.root.log(input={"audio": {segment.segment_id: segment.encoded["attachment"]}}) self._publish_call_manifest() self.alignment.publish() await asyncio.to_thread(self.logger.flush) def _publish_call_manifest(self): - for segment in self.call_recording.completed: - if segment["state"] == "omitted": - self.call_descriptors[segment["id"]] = { - "id": segment["id"], - "recording_group_id": "call", - "state": "omitted", - "reason": segment["reason"], - "sources": self._call_sources(), - "timeline": { - "origin_unix_ms": self.call_recording.origin_unix_ms, - "recording_start_offset_ms": segment["start_ms"], - "basis": "input_sample_clock_and_output_write_observation", - }, - "gaps": [ - { - "start_offset_ms": segment["start_ms"], - "end_offset_ms": segment["end_ms"], - "reason": segment["reason"], - } - ], - } - descriptors = list(self.call_descriptors.values()) + descriptors = [ + segment_descriptor(segment, self.root.span_id, self._call_sources()) + for segment in self.call_recording.segments + ] reason = self.call_recording.reason if reason or not descriptors: descriptors.append( diff --git a/py/src/braintrust/integrations/pipecat/voice/realtime.py b/py/src/braintrust/integrations/pipecat/voice/realtime.py index 259c7616b..52a74e276 100644 --- a/py/src/braintrust/integrations/pipecat/voice/realtime.py +++ b/py/src/braintrust/integrations/pipecat/voice/realtime.py @@ -6,6 +6,8 @@ from contextvars import ContextVar +from braintrust.audio.timeline import ms_to_samples + from ..llm_metrics import _metadata_from_processor from .instrumentation import native_value @@ -282,7 +284,7 @@ def publish(self, final=False): events = [{"item_id": item, **self.items.get(item, {})} for item in ids] ready = all("audio_start_ms" in event and "audio_end_ms" in event for event in events) if ready and self.observer.capture_user_audio and not self.invalid: - ready = all(round(event["audio_end_ms"] * 24) <= self.sent_samples for event in events) + ready = all(ms_to_samples(event["audio_end_ms"]) <= self.sent_samples for event in events) if not ready and not final: pending.append((span, ids)) continue @@ -291,7 +293,7 @@ def publish(self, final=False): for event in events: if "audio_start_ms" not in event or "audio_end_ms" not in event: continue - start, end = round(event["audio_start_ms"] * 24), round(event["audio_end_ms"] * 24) + start, end = ms_to_samples(event["audio_start_ms"]), ms_to_samples(event["audio_end_ms"]) ranges = [] for a, b, interval in self.sent_ranges: left, right = max(start, a), min(end, b) diff --git a/py/src/braintrust/integrations/pipecat/voice/test_alignment.py b/py/src/braintrust/integrations/pipecat/voice/test_alignment.py index 5fea496c5..9466bbba9 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_alignment.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_alignment.py @@ -1,12 +1,11 @@ import asyncio import io import unittest -from types import SimpleNamespace from unittest.mock import patch import numpy as np import soundfile as sf # pylint: disable=import-error -from braintrust.audio.alignment import Alignment, InputRanges +from braintrust.audio.export import Alignment from braintrust.audio.recording import CallRecording from pipecat.frames.frames import TTSAudioRawFrame, TTSStoppedFrame # pylint: disable=import-error from pipecat.transports.base_output import BaseOutputTransport # pylint: disable=import-error @@ -17,27 +16,6 @@ class AlignmentTests(unittest.IsolatedAsyncioTestCase): - def test_input_trim_uses_sample_positions_not_span_time(self): - recording = CallRecording() - ledger = InputRanges() - for index in range(3): - frame = SimpleNamespace( - audio=np.full(320, index + 1, dtype=" Date: Wed, 7 Oct 2026 23:55:23 -0400 Subject: [PATCH 17/18] Fixed: Audio segments stay pending until uploaded --- py/src/braintrust/audio/attachments.py | 20 +++ py/src/braintrust/audio/conftest.py | 10 ++ py/src/braintrust/audio/segments.py | 23 ++-- py/src/braintrust/audio/worker.py | 7 +- .../integrations/pipecat/conftest.py | 3 + .../integrations/pipecat/voice/synthesis.py | 7 +- .../pipecat/voice/test_instrumentation.py | 121 +++++++++++++++++- .../integrations/pipecat/voice/test_turns.py | 4 +- .../pipecat/voice/test_user_capture.py | 26 +++- .../pipecat/voice/user_capture.py | 14 +- 10 files changed, 207 insertions(+), 28 deletions(-) create mode 100644 py/src/braintrust/audio/conftest.py create mode 100644 py/src/braintrust/integrations/pipecat/conftest.py diff --git a/py/src/braintrust/audio/attachments.py b/py/src/braintrust/audio/attachments.py index 7255cc565..986c839d6 100644 --- a/py/src/braintrust/audio/attachments.py +++ b/py/src/braintrust/audio/attachments.py @@ -1,7 +1,11 @@ """Prepare encoded recordings for the standard attachment exporter.""" +import asyncio + from braintrust.logger import Attachment +from .worker import await_background + def prepare_recording(name, function, *args): encoded = function(*args) @@ -12,3 +16,19 @@ def prepare_recording(name, function, *args): content_type=encoded["mime_type"], ) return encoded + + +class UploadFailed(RuntimeError): + """The attachment did not reach confirmed upload completion.""" + + +async def upload_recording(encoded): + def upload(): + try: + status = encoded["attachment"].upload() + except Exception as error: + raise UploadFailed("upload_failed") from error + if status.get("upload_status") != "done": + raise UploadFailed("upload_failed") + + await await_background(asyncio.to_thread(upload)) diff --git a/py/src/braintrust/audio/conftest.py b/py/src/braintrust/audio/conftest.py new file mode 100644 index 000000000..3a0e539de --- /dev/null +++ b/py/src/braintrust/audio/conftest.py @@ -0,0 +1,10 @@ +"""Audio tests retain attachments locally instead of contacting Braintrust storage.""" + +import pytest +from braintrust.logger import Attachment + + +@pytest.fixture(autouse=True) +def local_attachment_uploads(monkeypatch): + # Override in lifecycle tests to hold/fail uploads. Keep real encoding and payloads. + monkeypatch.setattr(Attachment, "upload", lambda self: {"upload_status": "done"}) diff --git a/py/src/braintrust/audio/segments.py b/py/src/braintrust/audio/segments.py index 2d80d5a00..c550eeaf0 100644 --- a/py/src/braintrust/audio/segments.py +++ b/py/src/braintrust/audio/segments.py @@ -1,10 +1,9 @@ """Progressively export independently decodable segments on one sample timeline.""" import asyncio -import logging from dataclasses import dataclass, field -from .attachments import prepare_recording +from .attachments import UploadFailed, prepare_recording, upload_recording from .budget import source_budget from .jobs import RecordingJobs from .options import RecordingOptions @@ -172,18 +171,18 @@ def encode(): segment.clear() try: - encoded = await encode_in_worker(encode) - segment.ready(encoded) + try: + encoded = await encode_in_worker(encode) + segment.encoded = encoded + await upload_recording(encoded) + segment.ready(encoded) + except Exception as error: # noqa: BLE001 - recording cannot stop speech + segment.omit("upload_failed" if isinstance(error, UploadFailed) else type(error).__name__) + self.omit("segment_export_failed") + # Publication failures do not invalidate an already uploaded recording. + # The authoritative manifest remains available for subsequent upserts. if self.on_segment: await self.on_segment(segment) - except Exception as error: # noqa: BLE001 - recording cannot stop speech - segment.omit(type(error).__name__) - self.omit("segment_export_failed") - if self.on_segment: - try: - await self.on_segment(segment) - except Exception: # noqa: BLE001 - failure reporting must not break the call - logging.getLogger(__name__).warning("Failed to publish recording omission", exc_info=True) finally: segment.encoded = None diff --git a/py/src/braintrust/audio/worker.py b/py/src/braintrust/audio/worker.py index 7c4ee1e7c..ad433490d 100644 --- a/py/src/braintrust/audio/worker.py +++ b/py/src/braintrust/audio/worker.py @@ -37,7 +37,12 @@ def work(): except BaseException: _slots.release() raise - wrapped = asyncio.wrap_future(future) + return await await_background(asyncio.wrap_future(future)) + + +async def await_background(work): + """Drain non-cancellable thread work before releasing its owning job.""" + wrapped = asyncio.ensure_future(work) try: return await asyncio.shield(wrapped) except asyncio.CancelledError: diff --git a/py/src/braintrust/integrations/pipecat/conftest.py b/py/src/braintrust/integrations/pipecat/conftest.py new file mode 100644 index 000000000..e97445f51 --- /dev/null +++ b/py/src/braintrust/integrations/pipecat/conftest.py @@ -0,0 +1,3 @@ +"""Pipecat tests use in-memory trace and attachment export; provider calls use cassettes.""" + +from braintrust.audio.conftest import local_attachment_uploads as local_attachment_uploads diff --git a/py/src/braintrust/integrations/pipecat/voice/synthesis.py b/py/src/braintrust/integrations/pipecat/voice/synthesis.py index 8b879e9b4..e26897ed5 100644 --- a/py/src/braintrust/integrations/pipecat/voice/synthesis.py +++ b/py/src/braintrust/integrations/pipecat/voice/synthesis.py @@ -3,7 +3,7 @@ import asyncio from dataclasses import dataclass, field -from braintrust.audio.attachments import prepare_recording +from braintrust.audio.attachments import UploadFailed, prepare_recording, upload_recording from braintrust.audio.budget import source_budget from braintrust.audio.jobs import RecordingJobs from braintrust.audio.recording import encode_audio @@ -130,6 +130,7 @@ async def _publish(self, state): state.channels, self.audio_format, ) + await upload_recording(encoded) except Exception as error: # noqa: BLE001 - capture/export failures must not break the call span.log( metadata={ @@ -137,7 +138,9 @@ async def _publish(self, state): { **recording, "state": "omitted", - "reason": str(error) if isinstance(error, RecordingBusy) else "encoding_failed", + "reason": str(error) + if isinstance(error, (RecordingBusy, UploadFailed)) + else "encoding_failed", } ] } diff --git a/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py b/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py index 3a83d0d4c..3774af5c5 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py @@ -223,6 +223,23 @@ def encode(segment, audio_format): self.assertTrue(all(recording["state"] == "ready" for recording in recordings)) async def test_generated_clip_capacity_is_reused_after_publication(self): + from unittest.mock import patch + + from braintrust.logger import Attachment + + uploaded = [] + + def upload(attachment): + uploaded.append(attachment.reference) + if len(uploaded) == 2: + return {"upload_status": "error", "error_message": "failed"} + if len(uploaded) == 3: + raise OSError("upload failed") + return {"upload_status": "done"} + + uploader = patch.object(Attachment, "upload", upload) + uploader.start() + self.addCleanup(uploader.stop) from braintrust.audio.budget import source_budget observer = NativeObserver(Span(), retain_audio=True, audio_format="wav") @@ -248,8 +265,18 @@ async def test_generated_clip_capacity_is_reused_after_publication(self): self.assertEqual(span.rows[-1]["metadata"]["audio.recordings"][0]["state"], "pending") await observer.synthesis.drain() descriptor = span.rows[-1]["metadata"]["audio.recordings"][0] - self.assertEqual(descriptor["state"], "ready") - self.assertEqual(descriptor["duration_ms"], 1) + self.assertEqual(descriptor["state"], "omitted" if index in (1, 2) else "ready") + if index in (1, 2): + self.assertEqual(descriptor["reason"], "upload_failed") + self.assertFalse( + any( + d["state"] == "ready" + for row in span.rows + for d in row.get("metadata", {}).get("audio.recordings", []) + ) + ) + else: + self.assertEqual(descriptor["duration_ms"], 1) self.assertEqual(source_budget.used, before) await observer.finish() @@ -275,3 +302,93 @@ async def test_failed_segment_publishes_omission_before_shutdown(self): worker._slots.release() worker._slots.release() await observer.finish() + + +# This tests local publication ordering, not the attachment HTTP protocol. +# Provider conversations are exercised separately through cassette replay. +import pytest + + +@pytest.mark.asyncio +@pytest.mark.parametrize("outcome", ["done", "error", "exception", "flush_error"]) +async def test_segment_readiness_waits_for_upload_and_preserves_earlier_audio(monkeypatch, outcome): + import asyncio + import threading + + from braintrust.audio import RecordingOptions + from braintrust.logger import Attachment + + entered, release = threading.Event(), threading.Event() + loop_thread = threading.get_ident() + + def upload(attachment): + assert threading.get_ident() != loop_thread + if attachment.reference["filename"] == "call-0001.wav": + entered.set() + if not release.wait(3): + raise TimeoutError("test upload deadline") + if outcome == "exception": + raise OSError("upload failed") + if outcome == "error": + return {"upload_status": "error", "error_message": "upload failed"} + return {"upload_status": "done"} + + monkeypatch.setattr(Attachment, "upload", upload) + observer = NativeObserver( + Span(), + retain_audio=True, + audio_format="wav", + recording_options=RecordingOptions(segment_duration_seconds=0.02), + ) + observer.capture_transport = True + turn = Span() + + def recordings(): + return [ + row["metadata"]["audio.recordings"] + for row in observer.root.rows + if "audio.recordings" in row.get("metadata", {}) + ] + + def selections(): + return [s for row in turn.rows for s in row.get("metadata", {}).get("audio.selections", [])] + + try: + for index in range(2): + observer.call_recording.capture(0, b"\1\0" * 26400, 24000, 1, observed_ns=index * 1_100_000_000) + observer.alignment.add(turn, [[index * 480, (index + 1) * 480]], 0) + observer.call_recording.capture(1, b"\2\0" * 480, 24000, 1, observed_ns=(index + 1) * 1_100_000_000) + if index == 0: + await observer.call_recording.drain_exports() + assert recordings()[-1][0]["state"] == "ready" + assert await asyncio.to_thread(entered.wait, 1), "export must upload before marking ready" + # An unrelated publication must not expose the encoded segment as ready. + observer._publish_call_manifest() + observer.alignment.publish() + assert recordings()[-1][1]["state"] == "pending" + assert all(s["recording_id"] != "call-0001" for s in selections()) + if outcome == "flush_error": + + def fail_flush(): + raise OSError("trace publication failed") + + monkeypatch.setattr(observer.logger, "flush", fail_flush) + release.set() + await observer.call_recording.drain_exports() + observer._publish_call_manifest() + expected = "omitted" if outcome in {"error", "exception"} else "ready" + assert recordings()[-1][0]["state"] == "ready" + assert recordings()[-1][1]["state"] == expected + if expected == "omitted": + assert recordings()[-1][1]["reason"] + assert not any(d["id"] == "call-0001" and d["state"] == "ready" for row in recordings() for d in row) + assert all(s["recording_id"] != "call-0001" for s in selections()) + else: + selected = [s for s in selections() if s["recording_id"] == "call-0001"] + assert selected and selected[-1]["start_offset_ms"] == 0 + assert selected[-1]["end_offset_ms"] == 20 + assert any(s["recording_id"] == "call-0000" for s in selections()) + finally: + release.set() + monkeypatch.setattr(observer.logger, "flush", lambda: None) + await observer.finish() diff --git a/py/src/braintrust/integrations/pipecat/voice/test_turns.py b/py/src/braintrust/integrations/pipecat/voice/test_turns.py index e2014c8c5..7f2a3875c 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_turns.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_turns.py @@ -145,8 +145,8 @@ async def test_low_value_events_skip_serialization_and_keep_later_interruption( self.assertEqual(self.observer.filtered_events["BotSpeakingFrame"], 500) self.assertEqual(self.observer.duplicate_events, 1) self.assertEqual(self.observer.omitted_events, 0) - from pipecat.audio.vad.vad_analyzer import VADParams - from pipecat.frames.frames import STTMetadataFrame, VADParamsUpdateFrame + from pipecat.audio.vad.vad_analyzer import VADParams # pylint: disable=import-error + from pipecat.frames.frames import STTMetadataFrame, VADParamsUpdateFrame # pylint: disable=import-error await self.push(STTMetadataFrame(service_name="stt", ttfs_p99_latency=0.5)) await self.push(STTMetadataFrame(service_name="stt", ttfs_p99_latency=0.5)) diff --git a/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py b/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py index 399e26efb..131a25ea5 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py @@ -130,6 +130,23 @@ async def test_observation_failure_does_not_drop_native_transcription(self): await observer.finish() async def test_completed_turns_release_capacity_for_later_clips(self): + from unittest.mock import patch + + from braintrust.logger import Attachment + + uploaded = [] + + def upload(attachment): + uploaded.append(attachment.reference) + if len(uploaded) == 2: + return {"upload_status": "error", "error_message": "failed"} + if len(uploaded) == 3: + raise OSError("upload failed") + return {"upload_status": "done"} + + uploader = patch.object(Attachment, "upload", upload) + uploader.start() + self.addCleanup(uploader.stop) observer = NativeObserver(Span(), retain_audio=True, audio_format="wav", max_audio_bytes=1024) aggregator = LLMContextAggregatorPair(LLMContext()).user() @@ -157,9 +174,12 @@ async def push_context(): for r in turn["span"].rows if "audio.recordings" in r.get("metadata", {}) ] - self.assertEqual(descriptors[-1][0]["state"], "omitted" if index == 135 else "ready") - if index == 135: - self.assertEqual(descriptors[-1][0]["reason"], "capture_byte_limit") + self.assertEqual(descriptors[-1][0]["state"], "omitted" if index in (1, 2, 135) else "ready") + if index in (1, 2, 135): + self.assertEqual( + descriptors[-1][0]["reason"], "capture_byte_limit" if index == 135 else "upload_failed" + ) + self.assertFalse(any(d["state"] == "ready" for row in descriptors for d in row)) self.assertFalse(capture.batches) self.assertFalse(capture.segments) self.assertEqual(capture.bytes, 0) diff --git a/py/src/braintrust/integrations/pipecat/voice/user_capture.py b/py/src/braintrust/integrations/pipecat/voice/user_capture.py index edf615a2c..ee0bd6f58 100644 --- a/py/src/braintrust/integrations/pipecat/voice/user_capture.py +++ b/py/src/braintrust/integrations/pipecat/voice/user_capture.py @@ -10,7 +10,7 @@ from collections import deque from braintrust.audio.alignment import InputRanges -from braintrust.audio.attachments import prepare_recording +from braintrust.audio.attachments import UploadFailed, prepare_recording, upload_recording from braintrust.audio.budget import source_budget from braintrust.audio.jobs import RecordingJobs from braintrust.audio.recording import encode_audio @@ -343,6 +343,13 @@ async def _publish_batch(self, batch): self.observer.audio_format, [s["clip_ranges"] for s in segments if s["audio"] is not None], ) + await upload_recording(encoded) + except Exception as error: # noqa: BLE001 - recording failure must not break the call + descriptor.update( + state="omitted", + reason=str(error) if isinstance(error, (RecordingBusy, UploadFailed)) else "encoding_failed", + ) + else: descriptor.update( state="ready", attachment={ @@ -374,11 +381,6 @@ async def _publish_batch(self, batch): } ] ) - except Exception as error: # noqa: BLE001 - attachment failure must not break flush - descriptor.update( - state="omitted", - reason=str(error) if isinstance(error, RecordingBusy) else "encoding_failed", - ) turn["span"].log(metadata={"audio.recordings": [descriptor]}) def release_segments(self, segments): From 6d5155737bacb830eb794e9db3ea7e8349af0365 Mon Sep 17 00:00:00 2001 From: David Elner Date: Thu, 8 Oct 2026 16:19:23 -0400 Subject: [PATCH 18/18] Refactored: Audio export and alignment ownership --- py/noxfile.py | 6 +- py/src/braintrust/_audio/__init__.py | 33 +++ py/src/braintrust/_audio/alignment.py | 204 ++++++++++++++++++ .../{audio => _audio}/attachments.py | 0 py/src/braintrust/{audio => _audio}/budget.py | 0 .../braintrust/{audio => _audio}/conftest.py | 0 py/src/braintrust/_audio/constants.py | 11 + py/src/braintrust/_audio/export.py | 95 ++++++++ py/src/braintrust/_audio/exporter.py | 26 +++ py/src/braintrust/{audio => _audio}/jobs.py | 0 .../braintrust/{audio => _audio}/options.py | 3 + .../braintrust/{audio => _audio}/recording.py | 35 ++- py/src/braintrust/_audio/segment.py | 45 ++++ .../braintrust/{audio => _audio}/segments.py | 92 +++----- .../{audio => _audio}/test_dependencies.py | 8 +- .../{audio => _audio}/test_recording.py | 0 .../{audio => _audio}/test_segments.py | 56 ++++- .../{audio => _audio}/test_worker.py | 4 +- .../braintrust/{audio => _audio}/timeline.py | 0 py/src/braintrust/_audio/types.py | 34 +++ py/src/braintrust/{audio => _audio}/worker.py | 0 py/src/braintrust/audio/__init__.py | 6 - py/src/braintrust/audio/alignment.py | 92 -------- py/src/braintrust/audio/export.py | 159 -------------- .../integrations/pipecat/__init__.py | 3 +- .../integrations/pipecat/conftest.py | 2 +- .../pipecat/test_voice_pipeline.py | 2 +- .../integrations/pipecat/tracing.py | 2 +- .../integrations/pipecat/voice/alignment.py | 2 +- .../pipecat/voice/instrumentation.py | 5 +- .../integrations/pipecat/voice/realtime.py | 2 +- .../integrations/pipecat/voice/synthesis.py | 15 +- .../pipecat/voice/test_alignment.py | 6 +- .../pipecat/voice/test_instrumentation.py | 12 +- .../pipecat/voice/test_realtime.py | 2 +- .../pipecat/voice/test_recording.py | 2 +- .../pipecat/voice/test_user_capture.py | 4 +- .../pipecat/voice/user_capture.py | 21 +- 38 files changed, 606 insertions(+), 383 deletions(-) create mode 100644 py/src/braintrust/_audio/__init__.py create mode 100644 py/src/braintrust/_audio/alignment.py rename py/src/braintrust/{audio => _audio}/attachments.py (100%) rename py/src/braintrust/{audio => _audio}/budget.py (100%) rename py/src/braintrust/{audio => _audio}/conftest.py (100%) create mode 100644 py/src/braintrust/_audio/constants.py create mode 100644 py/src/braintrust/_audio/export.py create mode 100644 py/src/braintrust/_audio/exporter.py rename py/src/braintrust/{audio => _audio}/jobs.py (100%) rename py/src/braintrust/{audio => _audio}/options.py (79%) rename py/src/braintrust/{audio => _audio}/recording.py (85%) create mode 100644 py/src/braintrust/_audio/segment.py rename py/src/braintrust/{audio => _audio}/segments.py (68%) rename py/src/braintrust/{audio => _audio}/test_dependencies.py (85%) rename py/src/braintrust/{audio => _audio}/test_recording.py (100%) rename py/src/braintrust/{audio => _audio}/test_segments.py (80%) rename py/src/braintrust/{audio => _audio}/test_worker.py (95%) rename py/src/braintrust/{audio => _audio}/timeline.py (100%) create mode 100644 py/src/braintrust/_audio/types.py rename py/src/braintrust/{audio => _audio}/worker.py (100%) delete mode 100644 py/src/braintrust/audio/__init__.py delete mode 100644 py/src/braintrust/audio/alignment.py delete mode 100644 py/src/braintrust/audio/export.py diff --git a/py/noxfile.py b/py/noxfile.py index 597e56559..a9eed96d3 100644 --- a/py/noxfile.py +++ b/py/noxfile.py @@ -790,7 +790,7 @@ def test_pytest_plugin(session, version): def test_audio(session): _install_test_deps(session) _install_group_locked(session, "test-audio") - _run_tests(session, "braintrust/audio") + _run_tests(session, "braintrust/_audio") @nox.session() @@ -963,8 +963,8 @@ def _run_core_tests(session): SRC_DIR, ignore_paths=[ WRAPPER_DIR, - "braintrust/audio/test_recording.py", - "braintrust/audio/test_segments.py", + "braintrust/_audio/test_recording.py", + "braintrust/_audio/test_segments.py", *_integration_subdirs_to_ignore(), CONTRIB_DIR, DEVSERVER_DIR, diff --git a/py/src/braintrust/_audio/__init__.py b/py/src/braintrust/_audio/__init__.py new file mode 100644 index 000000000..33cb43c93 --- /dev/null +++ b/py/src/braintrust/_audio/__init__.py @@ -0,0 +1,33 @@ +"""Internal audio API shared by integrations; codecs remain optional and lazy.""" + +from .alignment import InputRanges +from .attachments import UploadFailed, prepare_recording, upload_recording +from .budget import source_budget +from .export import AlignmentPublisher, segment_descriptor +from .jobs import RecordingJobs +from .options import RecordingOptions +from .recording import encode_audio +from .segments import SegmentedRecording +from .timeline import ClipTimeline, ms_to_samples, pcm_bytes_to_ms, samples_to_ms +from .worker import RecordingBusy, encode_in_worker + + +__all__ = [ + "AlignmentPublisher", + "ClipTimeline", + "InputRanges", + "RecordingBusy", + "RecordingJobs", + "RecordingOptions", + "SegmentedRecording", + "UploadFailed", + "encode_audio", + "encode_in_worker", + "ms_to_samples", + "pcm_bytes_to_ms", + "prepare_recording", + "samples_to_ms", + "segment_descriptor", + "source_budget", + "upload_recording", +] diff --git a/py/src/braintrust/_audio/alignment.py b/py/src/braintrust/_audio/alignment.py new file mode 100644 index 000000000..d7d6b5a5f --- /dev/null +++ b/py/src/braintrust/_audio/alignment.py @@ -0,0 +1,204 @@ +"""Bounded sample ranges and recording selections shared by voice integrations.""" + +from collections import deque +from collections.abc import Iterator +from dataclasses import dataclass, field + +from .constants import MAX_PENDING_RANGES +from .timeline import ClipTimeline, ms_to_samples, samples_to_ms +from .types import SegmentInterval, Selection + + +def merge_ranges(ranges: list[list[int]]) -> list[list[int]]: + result = [] + for start, end in sorted(ranges): + if end <= start: + continue + if result and start <= result[-1][1]: + result[-1][1] = max(result[-1][1], end) + else: + result.append([start, end]) + return result + + +def resolve_selections( + ranges: list[list[int]], + segments: list[SegmentInterval], + resolved: list[list[int]], + recording_span_id: str, + channel: int, +) -> tuple[list[Selection], list[list[int]]]: + """Map call samples into ready files; retain ranges whose files are pending.""" + selections = [] + merged = merge_ranges(ranges) + pending = [] + for segment in segments: + if segment["state"] != "ready": + continue + lower, upper = ms_to_samples(segment["start_ms"]), ms_to_samples(segment["end_ms"]) + for start, end in merged: + start, end = max(start, lower), min(end, upper) + if end > start: + selections.append( + { + "recording_span_id": recording_span_id, + "recording_id": segment["id"], + "start_offset_ms": samples_to_ms(start - lower), + "end_offset_ms": samples_to_ms(end - lower), + "channel_index": channel, + } + ) + for start, end in ranges: + for lower, upper in resolved: + if lower > start: + pending.append([start, min(lower, end)]) + start = max(start, upper) if lower < end else end + if start >= end: + break + if start < end: + pending.append([start, end]) + return selections, pending + + +class InputRanges: + def __init__(self): + self.runs = deque() + self.size = 0 + + def append(self, size, interval): + self.runs.append([size, interval, size, 0]) + self.size += size + + def trim(self, retained): + while self.size > retained and self.runs: + run = self.runs[0] + count = min(self.size - retained, run[0]) + run[0] -= count + run[3] += count + self.size -= count + if not run[0]: + self.runs.popleft() + + def drain_mapped(self): + pieces = [] + position = 0 + for count, interval, original, offset in self.runs: + if interval: + length = interval["end"] - interval["start"] + pieces.append( + ( + position, + position + count, + interval["start"] + round(offset * length / original), + interval["start"] + round((offset + count) * length / original), + ) + ) + position += count + self.runs.clear() + self.size = 0 + return pieces + + def drain(self): + return merge_ranges([[a, b] for _, _, a, b in self.drain_mapped()]) + + +@dataclass +class _SelectionOwner: + channel: int + ranges: list[list[int]] = field(default_factory=list) + published: list[Selection] = field(default_factory=list) + + +@dataclass +class _Clip: + timeline: ClipTimeline = field(default_factory=ClipTimeline) + descriptor: dict | None = None + + +class Alignment: + """Resolve recording positions into metadata updates, without SDK objects or I/O.""" + + def __init__(self, recording_span_id: str): + self.recording_span_id = recording_span_id + self._owners: dict[str, _SelectionOwner] = {} + self._clips: dict[str, _Clip] = {} + self._dirty_clips: set[str] = set() + self.count = 0 + self.omitted = 0 + self.pending_owners = set() + + def add(self, span_id: str, ranges: list[list[int]], channel: int) -> None: + item = self._owners.setdefault(span_id, _SelectionOwner(channel)) + for interval in ranges: + if item.ranges and item.ranges[-1][0] <= interval[0] <= item.ranges[-1][1]: + item.ranges[-1][1] = max(item.ranges[-1][1], interval[1]) + continue + if self.count >= MAX_PENDING_RANGES: + self.omitted += 1 + continue + item.ranges.append(list(interval)) + self.count += 1 + + if item.ranges: + self.pending_owners.add(span_id) + + def begin_clip(self, span_id: str) -> None: + self._clips[span_id] = _Clip() + + def add_clip_range( + self, span_id: str, start_ms: float, end_ms: float, call_start_ms: float, call_end_ms: float + ) -> None: + clip = self._clips.get(span_id) + if clip is not None: + clip.timeline.add(start_ms, end_ms, call_start_ms, call_end_ms) + self._dirty_clips.add(span_id) + + def set_clip(self, span_id: str, descriptor: dict) -> None: + clip = self._clips.get(span_id) + if clip is not None: + clip.descriptor = descriptor + self._dirty_clips.add(span_id) + + def clip_updates(self, origin: float | None) -> Iterator[tuple[str, dict]]: + if origin is None: + return + for span_id in tuple(self._dirty_clips): + clip = self._clips[span_id] + if clip.descriptor is None: + continue + timeline = clip.timeline.descriptor(origin) + if timeline != clip.descriptor.get("timeline"): + yield span_id, {"audio.recordings": [{**clip.descriptor, "timeline": timeline}]} + # Iteration resumes only after the publisher logs successfully. + clip.descriptor["timeline"] = timeline + self._dirty_clips.discard(span_id) + + def selection_updates(self, segments: list[SegmentInterval]) -> Iterator[tuple[str, dict]]: + resolved = merge_ranges( + [ + [ms_to_samples(s["start_ms"]), ms_to_samples(s["end_ms"])] + for s in segments + if s["state"] in {"ready", "omitted"} + ] + ) + for span_id in tuple(self.pending_owners): + state = self._owners[span_id] + channel, ranges = state.channel, state.ranges + selections = list(state.published) + additions, pending = resolve_selections(ranges, segments, resolved, self.recording_span_id, channel) + selections.extend(additions) + selections = [dict(items) for items in dict.fromkeys(tuple(s.items()) for s in selections)] + if selections and selections != state.published: + yield ( + span_id, + { + "audio.selections": selections, + "audio.selection": selections[0] if len(selections) == 1 else None, + }, + ) + state.published = selections + # Keep unresolved publication work intact if logging raises at yield. + self.count -= len(ranges) - len(pending) + ranges[:] = pending + if not pending: + self.pending_owners.discard(span_id) diff --git a/py/src/braintrust/audio/attachments.py b/py/src/braintrust/_audio/attachments.py similarity index 100% rename from py/src/braintrust/audio/attachments.py rename to py/src/braintrust/_audio/attachments.py diff --git a/py/src/braintrust/audio/budget.py b/py/src/braintrust/_audio/budget.py similarity index 100% rename from py/src/braintrust/audio/budget.py rename to py/src/braintrust/_audio/budget.py diff --git a/py/src/braintrust/audio/conftest.py b/py/src/braintrust/_audio/conftest.py similarity index 100% rename from py/src/braintrust/audio/conftest.py rename to py/src/braintrust/_audio/conftest.py diff --git a/py/src/braintrust/_audio/constants.py b/py/src/braintrust/_audio/constants.py new file mode 100644 index 000000000..8b4cc7413 --- /dev/null +++ b/py/src/braintrust/_audio/constants.py @@ -0,0 +1,11 @@ +"""Internal safety bounds; recording policy remains configurable in RecordingOptions.""" + +# Leave room for output writes that arrive behind the input sample clock. +SEGMENT_HEADROOM_MS = 1000 +# One encoding job and one queued segment; a slow exporter cannot grow a backlog. +MAX_SEGMENT_JOBS = 2 +# Independent object-count guard for unusually small PCM packets in an active buffer. +MAX_BUFFERED_PACKETS = 100000 +# Bound unresolved alignment work, not the lifetime number of turns or frames. +MAX_PENDING_RANGES = 32000 +MAX_OUTPUT_CONTEXTS = 16000 diff --git a/py/src/braintrust/_audio/export.py b/py/src/braintrust/_audio/export.py new file mode 100644 index 000000000..b65169f1d --- /dev/null +++ b/py/src/braintrust/_audio/export.py @@ -0,0 +1,95 @@ +"""Serialize manifests and publish alignment updates through the SDK.""" + +from collections.abc import Iterable + +from braintrust.logger import Span + +from .alignment import Alignment +from .constants import MAX_OUTPUT_CONTEXTS +from .recording import CallRecording +from .segment import AudioSegment + + +def segment_descriptor(segment: AudioSegment, span_id: str, sources: list[dict]) -> dict: + descriptor = { + "id": segment.segment_id, + "recording_group_id": "call", + "state": segment.state, + "sources": sources, + "timeline": { + "origin_unix_ms": segment.origin_unix_ms, + "recording_start_offset_ms": segment.start_ms, + "basis": "input_sample_clock_and_output_write_observation", + }, + } + if segment.state == "ready": + descriptor.update(segment.metadata) + descriptor["attachment"] = {"span_id": span_id, "ref": f"/input/audio/{segment.segment_id}"} + elif segment.state == "omitted": + descriptor["reason"] = segment.reason + descriptor["gaps"] = [ + { + "start_offset_ms": segment.start_ms, + "end_offset_ms": segment.end_ms, + "reason": segment.reason, + } + ] + return descriptor + + +class AlignmentPublisher: + """Adapt observed span ownership to the pure alignment model.""" + + def __init__(self, root: Span, recording: CallRecording): + self.root = root + self.recording = recording + self.alignment = Alignment(root.span_id) + self._spans: dict[str, Span] = {} + self._contexts: dict[str, list[Span]] = {} + + def add(self, owner: Span, ranges: list[list[int]], channel: int) -> None: + if not self.recording.reason: + self._spans[owner.span_id] = owner + self.alignment.add(owner.span_id, ranges, channel) + + def begin_output(self, context: str, owners: list[Span]) -> None: + if len(self._contexts) >= MAX_OUTPUT_CONTEXTS: + self.alignment.omitted += 1 + return + self._contexts[context] = owners + self.alignment.begin_clip(owners[0].span_id) + + def output_owners(self, context: str) -> list[Span]: + return self._contexts.get(context, []) + + def end_output(self, context: str) -> None: + self._contexts.pop(context, None) + + def add_clip_range( + self, owner: Span, start_ms: float, end_ms: float, call_start_ms: float, call_end_ms: float + ) -> None: + self.alignment.add_clip_range(owner.span_id, start_ms, end_ms, call_start_ms, call_end_ms) + + def publish_clip(self, owner: Span, descriptor: dict) -> None: + self._spans[owner.span_id] = owner + self.alignment.set_clip(owner.span_id, descriptor) + self.publish_clips() + + def _publish(self, updates: Iterable[tuple[str, dict]]) -> None: + for span_id, metadata in updates: + self._spans[span_id].log(metadata=metadata) + + def publish_clips(self) -> None: + self._publish(self.alignment.clip_updates(getattr(self.recording, "origin_unix_ms", None))) + + def publish(self) -> None: + self.publish_clips() + segments = getattr(self.recording, "completed", None) + if segments is None: + segments = ( + [{"id": "call", "start_ms": 0, "end_ms": max(self.recording.ends), "state": "ready"}] + if not self.recording.reason and self.recording.packets + else [] + ) + self._publish(self.alignment.selection_updates(segments)) + self.root.log(metadata={"braintrust.alignment.ranges_omitted": self.alignment.omitted}) diff --git a/py/src/braintrust/_audio/exporter.py b/py/src/braintrust/_audio/exporter.py new file mode 100644 index 000000000..2f415aa21 --- /dev/null +++ b/py/src/braintrust/_audio/exporter.py @@ -0,0 +1,26 @@ +"""Execute a sealed segment without deciding recording policy or publishing spans.""" + +from .attachments import UploadFailed, prepare_recording, upload_recording +from .segment import AudioSegment +from .worker import encode_in_worker + + +class SegmentExporter: + def __init__(self, audio_format: str = "ogg"): + self.audio_format = audio_format + + async def export(self, segment: AudioSegment) -> None: + def encode(): + try: + return prepare_recording(segment.segment_id, segment.source.encode, self.audio_format) + finally: + # The worker owns PCM until native encoding has finished. + segment.clear() + + try: + encoded = await encode_in_worker(encode) + segment.encoded = encoded + await upload_recording(encoded) + segment.ready(encoded) + except Exception as error: # noqa: BLE001 - recording cannot stop speech + segment.omit("upload_failed" if isinstance(error, UploadFailed) else type(error).__name__) diff --git a/py/src/braintrust/audio/jobs.py b/py/src/braintrust/_audio/jobs.py similarity index 100% rename from py/src/braintrust/audio/jobs.py rename to py/src/braintrust/_audio/jobs.py diff --git a/py/src/braintrust/audio/options.py b/py/src/braintrust/_audio/options.py similarity index 79% rename from py/src/braintrust/audio/options.py rename to py/src/braintrust/_audio/options.py index 1bbffa672..dc1967d23 100644 --- a/py/src/braintrust/audio/options.py +++ b/py/src/braintrust/_audio/options.py @@ -8,6 +8,9 @@ class RecordingOptions: """Per-call limits. A rotation threshold is not a total recording limit.""" + # A minute of dual-mono 24 kHz PCM is about 5.5 MiB; duration usually + # rotates first. Byte rotation leaves headroom while an export is in flight. + # These are conservative defaults, not throughput guarantees. segment_duration_seconds: float = 60 max_duration_seconds: float = 1800 max_buffer_bytes: int = 32 * 1024 * 1024 diff --git a/py/src/braintrust/audio/recording.py b/py/src/braintrust/_audio/recording.py similarity index 85% rename from py/src/braintrust/audio/recording.py rename to py/src/braintrust/_audio/recording.py index fa0c0445f..a98da2388 100644 --- a/py/src/braintrust/audio/recording.py +++ b/py/src/braintrust/_audio/recording.py @@ -4,9 +4,11 @@ import math import time from dataclasses import dataclass +from enum import Enum, auto from .budget import source_budget -from .timeline import ms_to_samples, pcm_bytes_to_ms +from .timeline import CALL_SAMPLE_RATE, ms_to_samples, pcm_bytes_to_ms +from .types import EncodedAudio @dataclass(frozen=True) @@ -18,7 +20,7 @@ class Packet: channels: int -def encode_audio(chunks, sample_rate, channels, audio_format="ogg"): +def encode_audio(chunks: list[bytes], sample_rate: int, channels: int, audio_format: str = "ogg") -> EncodedAudio: import numpy as np started = time.perf_counter() @@ -57,6 +59,13 @@ def _encode(samples, sample_rate, audio_format): } +class CaptureState(Enum): + OPEN = auto() + SEALED = auto() + STOPPED = auto() + DISABLED = auto() + + class CallRecording: def __init__( self, @@ -66,8 +75,7 @@ def __init__( max_duration_ms=120000, max_packets=16000, ): - self.enabled = enabled - self.sealed = False + self.state = CaptureState.OPEN if enabled else CaptureState.DISABLED self.max_bytes = max_bytes self.max_duration_ms = max_duration_ms self.max_packets = max_packets @@ -80,14 +88,21 @@ def __init__( self.reason = None if enabled else "disabled" def __del__(self): + # Explicit clear() is the normal owner-release path. This fallback + # returns reserved process bytes if an unsubmitted recorder is abandoned. if getattr(self, "bytes", 0): self.clear() - def seal(self): - self.sealed = True + def seal(self) -> None: + if self.state is CaptureState.OPEN: + self.state = CaptureState.SEALED - def omit(self, reason): + def stop(self, reason: str) -> None: + self.state = CaptureState.STOPPED self.reason = reason + + def omit(self, reason: str) -> None: + self.stop(reason) self.clear() def clear(self): @@ -96,7 +111,7 @@ def clear(self): self.packets.clear() def capture(self, channel, pcm, sample_rate, channels, *, observed_ns=None, observed_unix_ms=None): - if self.reason or self.sealed: + if self.state is not CaptureState.OPEN: return if channel not in (0, 1) or channels not in (1, 2) or not 8000 <= sample_rate <= 48000: self.omit("unsupported_audio_format") @@ -140,7 +155,7 @@ def capture(self, channel, pcm, sample_rate, channels, *, observed_ns=None, obse "end": ms_to_samples(start) + ms_to_samples(duration), } - def encode(self, audio_format="ogg"): + def encode(self, audio_format: str = "ogg") -> EncodedAudio | None: if self.reason or not self.packets: return None @@ -148,7 +163,7 @@ def encode(self, audio_format="ogg"): started = time.perf_counter() cpu_started = time.thread_time() - sample_rate = 24000 + sample_rate = CALL_SAMPLE_RATE sample_count = math.ceil(max(self.ends) * sample_rate / 1000) samples = np.zeros((sample_count, 2), dtype=np.int16) for packet in self.packets: diff --git a/py/src/braintrust/_audio/segment.py b/py/src/braintrust/_audio/segment.py new file mode 100644 index 000000000..fdf29bd0c --- /dev/null +++ b/py/src/braintrust/_audio/segment.py @@ -0,0 +1,45 @@ +"""Authoritative lifecycle manifest for one sealed recording segment.""" + +from dataclasses import dataclass, field + +from .recording import CallRecording +from .types import EncodedAudio, RecordingState, SegmentInterval + + +@dataclass +class AudioSegment: + """One manifest throughout encoding and export; PCM is sealed at rotation. + + Only the worker releases submitted PCM. Encoded bytes live through export; + afterwards only small metadata remains on the call's manifest. + """ + + segment_id: str + start_ms: float + end_ms: float + origin_unix_ms: float + source: CallRecording + state: RecordingState = "pending" + reason: str | None = None + encoded: EncodedAudio | None = None + metadata: dict = field(default_factory=dict) + + @property + def bytes(self) -> int: + return self.source.bytes + + def clear(self) -> None: + self.source.clear() + + def ready(self, encoded: EncodedAudio) -> None: + self.encoded = encoded + self.metadata = {key: encoded[key] for key in ("mime_type", "duration_ms", "channel_count")} + self.state = "ready" + + def omit(self, reason: str) -> None: + self.state, self.reason = "omitted", reason + self.encoded = None + self.metadata.clear() + + def interval(self) -> SegmentInterval: + return {"id": self.segment_id, "start_ms": self.start_ms, "end_ms": self.end_ms, "state": self.state} diff --git a/py/src/braintrust/audio/segments.py b/py/src/braintrust/_audio/segments.py similarity index 68% rename from py/src/braintrust/audio/segments.py rename to py/src/braintrust/_audio/segments.py index c550eeaf0..b3fd8cc76 100644 --- a/py/src/braintrust/audio/segments.py +++ b/py/src/braintrust/_audio/segments.py @@ -1,54 +1,16 @@ """Progressively export independently decodable segments on one sample timeline.""" import asyncio -from dataclasses import dataclass, field +from collections.abc import Awaitable, Callable -from .attachments import UploadFailed, prepare_recording, upload_recording from .budget import source_budget +from .constants import MAX_BUFFERED_PACKETS, MAX_SEGMENT_JOBS, SEGMENT_HEADROOM_MS +from .exporter import SegmentExporter from .jobs import RecordingJobs from .options import RecordingOptions -from .recording import CallRecording, Packet +from .recording import CallRecording, CaptureState, Packet +from .segment import AudioSegment from .timeline import ms_to_samples -from .worker import encode_in_worker - - -@dataclass -class AudioSegment: - """One manifest throughout encoding and export; PCM is sealed at rotation. - - Only the worker releases submitted PCM. Encoded bytes live through export; - afterwards only small metadata remains on the call's manifest. - """ - - segment_id: str - start_ms: float - end_ms: float - origin_unix_ms: float - source: CallRecording - state: str = "pending" - reason: str | None = None - encoded: dict | None = None - metadata: dict = field(default_factory=dict) - - @property - def bytes(self): - return self.source.bytes - - def clear(self): - self.source.clear() - - def ready(self, encoded): - self.encoded = encoded - self.metadata = {key: encoded[key] for key in ("mime_type", "duration_ms", "channel_count")} - self.state = "ready" - - def omit(self, reason): - self.state, self.reason = "omitted", reason - self.encoded = None - self.metadata.clear() - - def interval(self): - return {"id": self.segment_id, "start_ms": self.start_ms, "end_ms": self.end_ms, "state": self.state} class SegmentedRecording(CallRecording): @@ -58,17 +20,25 @@ class SegmentedRecording(CallRecording): exported. One second of headroom permits in-flight output and packet skew. """ - def __init__(self, *, options=None, on_segment=None, on_pending=None, audio_format="ogg", enabled=True): + def __init__( + self, + *, + options: RecordingOptions | None = None, + on_segment: Callable[[AudioSegment], Awaitable[None]] | None = None, + on_pending: Callable[[AudioSegment], None] | None = None, + audio_format: str = "ogg", + enabled: bool = True, + ): self.options = options or RecordingOptions() super().__init__( enabled=enabled, max_bytes=self.options.max_buffer_bytes, max_duration_ms=self.options.max_duration_seconds * 1000, - max_packets=100000, + max_packets=MAX_BUFFERED_PACKETS, ) self.on_segment = on_segment self.on_pending = on_pending - self.audio_format = audio_format + self.exporter = SegmentExporter(audio_format) self.start_ms = 0.0 self.sequence = 0 self.segments: list[AudioSegment] = [] @@ -86,10 +56,10 @@ def retained_bytes(self): def omit(self, reason): # Keep the valid prefix. Neither an error nor a limit erases prior audio. - self.reason = reason + self.stop(reason) def capture(self, channel, pcm, sample_rate, channels, *, observed_ns=None, observed_unix_ms=None): - if self.reason or self.sealed: + if self.state is not CaptureState.OPEN: return None if self.retained_bytes + len(pcm) > self.max_bytes: self.omit("capture_byte_limit") @@ -108,7 +78,7 @@ def capture(self, channel, pcm, sample_rate, channels, *, observed_ns=None, obse return None latest = self.packets[-1].start_ms watermark = min(self.ends[0], latest) if self.started[0] else latest - watermark = max(self.start_ms, watermark - 1000) + watermark = max(self.start_ms, watermark - SEGMENT_HEADROOM_MS) cut = self.start_ms + self.options.segment_duration_seconds * 1000 if watermark >= cut: self._rotate(cut) @@ -119,7 +89,7 @@ def capture(self, channel, pcm, sample_rate, channels, *, observed_ns=None, obse def _rotate(self, cut): if not self.packets: return - if len(self.jobs) >= 2: + if len(self.jobs) >= MAX_SEGMENT_JOBS: self.omit("recording_export_capacity") return snapshot = CallRecording() @@ -162,25 +132,13 @@ def release(): if self.on_pending: self.on_pending(segment) - async def _export(self, segment): - def encode(): - try: - return prepare_recording(segment.segment_id, segment.source.encode, self.audio_format) - finally: - # Only the encoder may release a segment handed to its thread. - segment.clear() - + async def _export(self, segment: AudioSegment) -> None: try: - try: - encoded = await encode_in_worker(encode) - segment.encoded = encoded - await upload_recording(encoded) - segment.ready(encoded) - except Exception as error: # noqa: BLE001 - recording cannot stop speech - segment.omit("upload_failed" if isinstance(error, UploadFailed) else type(error).__name__) + await self.exporter.export(segment) + if segment.state == "omitted": self.omit("segment_export_failed") - # Publication failures do not invalidate an already uploaded recording. - # The authoritative manifest remains available for subsequent upserts. + # Publishing is independent of upload success. A logging failure + # cannot invalidate an already uploaded segment. if self.on_segment: await self.on_segment(segment) finally: diff --git a/py/src/braintrust/audio/test_dependencies.py b/py/src/braintrust/_audio/test_dependencies.py similarity index 85% rename from py/src/braintrust/audio/test_dependencies.py rename to py/src/braintrust/_audio/test_dependencies.py index f6fdc838f..b2e134fb6 100644 --- a/py/src/braintrust/audio/test_dependencies.py +++ b/py/src/braintrust/_audio/test_dependencies.py @@ -16,10 +16,10 @@ def find_spec(self, fullname, path=None, target=None): raise AssertionError("Unexpected optional import: " + fullname) sys.meta_path.insert(0, BlockOptional()) -from braintrust.audio.recording import CallRecording -from braintrust.audio.alignment import InputRanges -from braintrust.audio import worker, RecordingOptions -from braintrust.audio.segments import SegmentedRecording +from braintrust._audio.recording import CallRecording +from braintrust._audio.alignment import InputRanges +from braintrust._audio import worker, RecordingOptions +from braintrust._audio.segments import SegmentedRecording import asyncio disabled = SegmentedRecording(enabled=False, options=RecordingOptions()) assert disabled.capture(0, b"\\0\\0" * 480, 24000, 1) is None diff --git a/py/src/braintrust/audio/test_recording.py b/py/src/braintrust/_audio/test_recording.py similarity index 100% rename from py/src/braintrust/audio/test_recording.py rename to py/src/braintrust/_audio/test_recording.py diff --git a/py/src/braintrust/audio/test_segments.py b/py/src/braintrust/_audio/test_segments.py similarity index 80% rename from py/src/braintrust/audio/test_segments.py rename to py/src/braintrust/_audio/test_segments.py index 5bb3614cd..558e03b5a 100644 --- a/py/src/braintrust/audio/test_segments.py +++ b/py/src/braintrust/_audio/test_segments.py @@ -155,7 +155,7 @@ async def publish(segment): def test_selection_crossing_segment_boundary(self): from types import SimpleNamespace - from .export import Alignment + from .export import AlignmentPublisher logs = [] root = SimpleNamespace(span_id="root", log=lambda **_: None) @@ -167,7 +167,7 @@ def test_selection_crossing_segment_boundary(self): {"id": "call-0001", "start_ms": 30000, "end_ms": 60000, "state": "ready"}, ], ) - alignment = Alignment(root, recording) + alignment = AlignmentPublisher(root, recording) alignment.add(owner, [[29000 * 24, 32000 * 24]], 0) alignment.publish() selections = logs[-1]["metadata"]["audio.selections"] @@ -210,24 +210,68 @@ def encode(segment, audio_format): def test_out_of_order_export_releases_only_resolved_ranges(self): from types import SimpleNamespace - from .export import Alignment + from .export import AlignmentPublisher rows = [] owner = SimpleNamespace(span_id="turn", log=lambda **row: rows.append(row)) recording = SimpleNamespace( reason=None, completed=[{"id": "later", "start_ms": 1000, "end_ms": 2000, "state": "ready"}] ) - alignment = Alignment(SimpleNamespace(span_id="root", log=lambda **_: None), recording) + root_rows = [] + alignment = AlignmentPublisher( + SimpleNamespace(span_id="root", log=lambda **row: root_rows.append(row)), recording + ) alignment.add(owner, [[0, 48000]], 0) alignment.publish() self.assertEqual(rows[-1]["metadata"]["audio.selection"]["recording_id"], "later") recording.completed.append({"id": "earlier", "start_ms": 0, "end_ms": 1000, "state": "ready"}) alignment.publish() - self.assertEqual(alignment.count, 0) self.assertEqual({s["recording_id"] for s in rows[-1]["metadata"]["audio.selections"]}, {"earlier", "later"}) # The admission budget is available again, even after many completed ranges. for _ in range(33000): alignment.add(owner, [[0, 1]], 0) alignment.publish() - self.assertEqual(alignment.omitted, 0) + self.assertEqual(root_rows[-1]["metadata"]["braintrust.alignment.ranges_omitted"], 0) self.assertEqual(len(rows), 3, "unchanged historical selections must not be exported again") + + def test_failed_alignment_publication_is_retried(self): + from types import SimpleNamespace + + from .export import AlignmentPublisher + + for kind in ("selection", "clip"): + with self.subTest(kind=kind): + rows = [] + failing = True + + def log(**row): + if failing: + raise RuntimeError("publication failed") + rows.append(row) + + root = SimpleNamespace(span_id="root", log=lambda **_: None) + owner = SimpleNamespace(span_id="turn", log=log) + recording = SimpleNamespace( + reason=None, + origin_unix_ms=1000, + completed=[{"id": "call-0000", "start_ms": 0, "end_ms": 1000, "state": "ready"}], + ) + alignment = AlignmentPublisher(root, recording) + with self.assertRaisesRegex(RuntimeError, "publication failed"): + if kind == "selection": + alignment.add(owner, [[0, 24000]], 0) + alignment.publish() + else: + alignment.begin_output("context", [owner]) + alignment.add_clip_range(owner, 0, 1000, 0, 1000) + alignment.publish_clip(owner, {"id": "clip", "state": "ready"}) + failing = False + alignment.publish() + alignment.publish() + self.assertEqual(len(rows), 1, "retry must publish exactly once") + if kind == "selection": + self.assertEqual(rows[0]["metadata"]["audio.selection"]["end_offset_ms"], 1000) + else: + timeline = rows[0]["metadata"]["audio.recordings"][0]["timeline"] + self.assertEqual(timeline["origin_unix_ms"], 1000) + self.assertEqual(timeline["ranges"][0]["timeline_end_ms"], 1000) diff --git a/py/src/braintrust/audio/test_worker.py b/py/src/braintrust/_audio/test_worker.py similarity index 95% rename from py/src/braintrust/audio/test_worker.py rename to py/src/braintrust/_audio/test_worker.py index 2c2ca2ee9..02c416e80 100644 --- a/py/src/braintrust/audio/test_worker.py +++ b/py/src/braintrust/_audio/test_worker.py @@ -61,7 +61,7 @@ async def operation(outcome): if outcome == "cancelled": task.cancel() # No operation body or finally block has run yet. if outcome == "failure": - with self.assertLogs("braintrust.audio.jobs", level="WARNING"): + with self.assertLogs("braintrust._audio.jobs", level="WARNING"): await jobs.drain() else: await jobs.drain() @@ -76,7 +76,7 @@ async def test_capture_limits_reject_and_release_retained_audio(self): from .recording import CallRecording budget = ByteBudget(1000) - with patch("braintrust.audio.recording.source_budget", budget): + with patch("braintrust._audio.recording.source_budget", budget): first, second = CallRecording(), CallRecording() pcm = b"\0\0" * 480 self.assertIsNotNone(first.capture(0, pcm, 24000, 1)) diff --git a/py/src/braintrust/audio/timeline.py b/py/src/braintrust/_audio/timeline.py similarity index 100% rename from py/src/braintrust/audio/timeline.py rename to py/src/braintrust/_audio/timeline.py diff --git a/py/src/braintrust/_audio/types.py b/py/src/braintrust/_audio/types.py new file mode 100644 index 000000000..0a6025524 --- /dev/null +++ b/py/src/braintrust/_audio/types.py @@ -0,0 +1,34 @@ +"""Internal data contracts shared by capture, export, and alignment.""" + +from typing import Literal, TypedDict + +from braintrust.logger import Attachment +from typing_extensions import NotRequired + + +RecordingState = Literal["pending", "ready", "omitted"] + + +class EncodedAudio(TypedDict): + data: bytes + duration_ms: float + channel_count: int + mime_type: str + extension: str + processing: NotRequired[dict[str, float | int]] + attachment: NotRequired[Attachment] + + +class SegmentInterval(TypedDict): + id: str + start_ms: float + end_ms: float + state: RecordingState + + +class Selection(TypedDict): + recording_span_id: str + recording_id: str + start_offset_ms: float + end_offset_ms: float + channel_index: int diff --git a/py/src/braintrust/audio/worker.py b/py/src/braintrust/_audio/worker.py similarity index 100% rename from py/src/braintrust/audio/worker.py rename to py/src/braintrust/_audio/worker.py diff --git a/py/src/braintrust/audio/__init__.py b/py/src/braintrust/audio/__init__.py deleted file mode 100644 index 420bb1577..000000000 --- a/py/src/braintrust/audio/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Public audio configuration. Recording and encoding modules are SDK internals.""" - -from .options import RecordingOptions - - -__all__ = ["RecordingOptions"] diff --git a/py/src/braintrust/audio/alignment.py b/py/src/braintrust/audio/alignment.py deleted file mode 100644 index e99a5427c..000000000 --- a/py/src/braintrust/audio/alignment.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Bounded sample ranges and recording selections shared by voice integrations.""" - -from collections import deque - -from .timeline import ms_to_samples, samples_to_ms - - -def merge_ranges(ranges): - result = [] - for start, end in sorted(ranges): - if end <= start: - continue - if result and start <= result[-1][1]: - result[-1][1] = max(result[-1][1], end) - else: - result.append([start, end]) - return result - - -def resolve_selections(ranges, segments, resolved, recording_span_id, channel): - """Map call samples into ready files; retain ranges whose files are pending.""" - selections = [] - merged = merge_ranges(ranges) - pending = [] - for segment in segments: - if segment["state"] != "ready": - continue - lower, upper = ms_to_samples(segment["start_ms"]), ms_to_samples(segment["end_ms"]) - for start, end in merged: - start, end = max(start, lower), min(end, upper) - if end > start: - selections.append( - { - "recording_span_id": recording_span_id, - "recording_id": segment["id"], - "start_offset_ms": samples_to_ms(start - lower), - "end_offset_ms": samples_to_ms(end - lower), - "channel_index": channel, - } - ) - for start, end in ranges: - for lower, upper in resolved: - if lower > start: - pending.append([start, min(lower, end)]) - start = max(start, upper) if lower < end else end - if start >= end: - break - if start < end: - pending.append([start, end]) - return selections, pending - - -class InputRanges: - def __init__(self): - self.runs = deque() - self.size = 0 - - def append(self, size, interval): - self.runs.append([size, interval, size, 0]) - self.size += size - - def trim(self, retained): - while self.size > retained and self.runs: - run = self.runs[0] - count = min(self.size - retained, run[0]) - run[0] -= count - run[3] += count - self.size -= count - if not run[0]: - self.runs.popleft() - - def drain_mapped(self): - pieces = [] - position = 0 - for count, interval, original, offset in self.runs: - if interval: - length = interval["end"] - interval["start"] - pieces.append( - ( - position, - position + count, - interval["start"] + round(offset * length / original), - interval["start"] + round((offset + count) * length / original), - ) - ) - position += count - self.runs.clear() - self.size = 0 - return pieces - - def drain(self): - return merge_ranges([[a, b] for _, _, a, b in self.drain_mapped()]) diff --git a/py/src/braintrust/audio/export.py b/py/src/braintrust/audio/export.py deleted file mode 100644 index 36f2f50b1..000000000 --- a/py/src/braintrust/audio/export.py +++ /dev/null @@ -1,159 +0,0 @@ -"""Serialize recording manifests without retaining their audio payloads.""" - -from dataclasses import dataclass, field - -from braintrust.logger import Span - -from .alignment import merge_ranges, resolve_selections -from .timeline import ClipTimeline, ms_to_samples - - -def segment_descriptor(segment, span_id, sources): - descriptor = { - "id": segment.segment_id, - "recording_group_id": "call", - "state": segment.state, - "sources": sources, - "timeline": { - "origin_unix_ms": segment.origin_unix_ms, - "recording_start_offset_ms": segment.start_ms, - "basis": "input_sample_clock_and_output_write_observation", - }, - } - if segment.state == "ready": - descriptor.update(segment.metadata) - descriptor["attachment"] = {"span_id": span_id, "ref": f"/input/audio/{segment.segment_id}"} - elif segment.state == "omitted": - descriptor["reason"] = segment.reason - descriptor["gaps"] = [ - { - "start_offset_ms": segment.start_ms, - "end_offset_ms": segment.end_ms, - "reason": segment.reason, - } - ] - return descriptor - - -@dataclass -class _SelectionOwner: - span: Span - channel: int - ranges: list[list[int]] = field(default_factory=list) - published: list[dict] = field(default_factory=list) - - -@dataclass -class _Clip: - timeline: ClipTimeline = field(default_factory=ClipTimeline) - owner: Span | None = None - descriptor: dict | None = None - - -class Alignment: - """Publish resolved selections and clip timelines to their owning spans.""" - - def __init__(self, root, recording): - self.root = root - self.recording = recording - self._owners: dict[str, _SelectionOwner] = {} - self._contexts = {} - self._clips: dict[str, _Clip] = {} - self._dirty_clips: set[str] = set() - self.count = 0 - self.omitted = 0 - self.pending_owners = set() - - def add(self, owner, ranges, channel): - if self.recording.reason: - return - item = self._owners.setdefault(owner.span_id, _SelectionOwner(owner, channel)) - for interval in ranges: - if item.ranges and item.ranges[-1][0] <= interval[0] <= item.ranges[-1][1]: - item.ranges[-1][1] = max(item.ranges[-1][1], interval[1]) - continue - if self.count >= 32000: - self.omitted += 1 - continue - item.ranges.append(list(interval)) - self.count += 1 - - if item.ranges: - self.pending_owners.add(owner.span_id) - - def begin_output(self, context, owners): - if len(self._contexts) >= 16000: - self.omitted += 1 - return - self._contexts[context] = owners - self._clips[owners[0].span_id] = _Clip() - - def output_owners(self, context): - return self._contexts.get(context, []) - - def end_output(self, context): - self._contexts.pop(context, None) - - def add_clip_range(self, owner, start_ms, end_ms, call_start_ms, call_end_ms): - clip = self._clips.get(owner.span_id) - if clip is not None: - clip.timeline.add(start_ms, end_ms, call_start_ms, call_end_ms) - self._dirty_clips.add(owner.span_id) - - def publish_clip(self, owner, descriptor): - clip = self._clips.get(owner.span_id) - if clip is not None: - clip.owner, clip.descriptor = owner, descriptor - self._dirty_clips.add(owner.span_id) - self.publish_clips() - - def publish_clips(self): - origin = getattr(self.recording, "origin_unix_ms", None) - if origin is None: - return - for span_id in tuple(self._dirty_clips): - clip = self._clips[span_id] - if clip.descriptor is None: - continue - timeline = clip.timeline.descriptor(origin) - if timeline != clip.descriptor.get("timeline"): - clip.descriptor["timeline"] = timeline - clip.owner.log(metadata={"audio.recordings": [dict(clip.descriptor)]}) - self._dirty_clips.discard(span_id) - - def publish(self): - self.publish_clips() - segments = getattr(self.recording, "completed", None) - if segments is None: - segments = ( - [{"id": "call", "start_ms": 0, "end_ms": max(self.recording.ends), "state": "ready"}] - if not self.recording.reason and self.recording.packets - else [] - ) - resolved = merge_ranges( - [ - [ms_to_samples(s["start_ms"]), ms_to_samples(s["end_ms"])] - for s in segments - if s["state"] in {"ready", "omitted"} - ] - ) - for span_id in tuple(self.pending_owners): - state = self._owners[span_id] - owner, channel, ranges = state.span, state.channel, state.ranges - selections = list(state.published) - additions, pending = resolve_selections(ranges, segments, resolved, self.root.span_id, channel) - selections.extend(additions) - self.count -= len(ranges) - len(pending) - ranges[:] = pending - selections = [dict(items) for items in dict.fromkeys(tuple(s.items()) for s in selections)] - if not pending: - self.pending_owners.discard(span_id) - if selections and selections != state.published: - state.published = selections - owner.log( - metadata={ - "audio.selections": selections, - "audio.selection": selections[0] if len(selections) == 1 else None, - } - ) - self.root.log(metadata={"braintrust.alignment.ranges_omitted": self.omitted}) diff --git a/py/src/braintrust/integrations/pipecat/__init__.py b/py/src/braintrust/integrations/pipecat/__init__.py index 81b7ca25f..630f0718d 100644 --- a/py/src/braintrust/integrations/pipecat/__init__.py +++ b/py/src/braintrust/integrations/pipecat/__init__.py @@ -1,6 +1,6 @@ """Braintrust integration for Pipecat AI.""" -from braintrust.audio import RecordingOptions +from braintrust._audio import RecordingOptions from braintrust.logger import NOOP_SPAN, current_span, init_logger from .integration import PipecatIntegration @@ -11,6 +11,7 @@ __all__ = [ "BraintrustPipecatObserver", "PipecatIntegration", + "RecordingOptions", "setup_pipecat", "wrap_pipeline_worker", ] diff --git a/py/src/braintrust/integrations/pipecat/conftest.py b/py/src/braintrust/integrations/pipecat/conftest.py index e97445f51..d564d65e2 100644 --- a/py/src/braintrust/integrations/pipecat/conftest.py +++ b/py/src/braintrust/integrations/pipecat/conftest.py @@ -1,3 +1,3 @@ """Pipecat tests use in-memory trace and attachment export; provider calls use cassettes.""" -from braintrust.audio.conftest import local_attachment_uploads as local_attachment_uploads +from braintrust._audio.conftest import local_attachment_uploads as local_attachment_uploads diff --git a/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py b/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py index 6e1782703..62c60645b 100644 --- a/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py +++ b/py/src/braintrust/integrations/pipecat/test_voice_pipeline.py @@ -13,7 +13,7 @@ import numpy as np import pytest -from braintrust.audio import RecordingOptions +from braintrust._audio import RecordingOptions from braintrust.integrations.pipecat import setup_pipecat from braintrust.integrations.pipecat.test_pipecat import ( _make_worker, diff --git a/py/src/braintrust/integrations/pipecat/tracing.py b/py/src/braintrust/integrations/pipecat/tracing.py index c1c682ae1..fbf697cde 100644 --- a/py/src/braintrust/integrations/pipecat/tracing.py +++ b/py/src/braintrust/integrations/pipecat/tracing.py @@ -4,7 +4,7 @@ import logging from typing import Any -from braintrust.audio import RecordingOptions +from braintrust._audio import RecordingOptions from braintrust.integrations.utils import ( _is_not_given, _normalize_chat_messages, diff --git a/py/src/braintrust/integrations/pipecat/voice/alignment.py b/py/src/braintrust/integrations/pipecat/voice/alignment.py index 8ca2299f9..c146fa08c 100644 --- a/py/src/braintrust/integrations/pipecat/voice/alignment.py +++ b/py/src/braintrust/integrations/pipecat/voice/alignment.py @@ -3,7 +3,7 @@ from collections import deque from contextvars import ContextVar -from braintrust.audio.timeline import pcm_bytes_to_ms, samples_to_ms +from braintrust._audio import pcm_bytes_to_ms, samples_to_ms def instrument_output(output, alignment, frame_context=None, hooks=None): diff --git a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py index 5c56cca41..e5bbcfd72 100644 --- a/py/src/braintrust/integrations/pipecat/voice/instrumentation.py +++ b/py/src/braintrust/integrations/pipecat/voice/instrumentation.py @@ -8,8 +8,7 @@ import wave from collections import Counter, deque -from braintrust.audio.export import Alignment, segment_descriptor -from braintrust.audio.segments import SegmentedRecording +from braintrust._audio import AlignmentPublisher, SegmentedRecording, segment_descriptor from pipecat.observers.base_observer import BaseObserver # pylint: disable=import-error from ..llm_metrics import _llm_usage_metrics, _metadata_from_metric, _metadata_from_processor @@ -162,7 +161,7 @@ def __init__( self.tts_requests = None self.turns = Turns(self.root, self.hooks) self.turns.on_completed = self._turn_completed - self.alignment = Alignment(self.root, self.call_recording) + self.alignment = AlignmentPublisher(self.root, self.call_recording) self.synthesis = SynthesisRecordings( logger, self.alignment, diff --git a/py/src/braintrust/integrations/pipecat/voice/realtime.py b/py/src/braintrust/integrations/pipecat/voice/realtime.py index 52a74e276..25488f3e7 100644 --- a/py/src/braintrust/integrations/pipecat/voice/realtime.py +++ b/py/src/braintrust/integrations/pipecat/voice/realtime.py @@ -6,7 +6,7 @@ from contextvars import ContextVar -from braintrust.audio.timeline import ms_to_samples +from braintrust._audio import ms_to_samples from ..llm_metrics import _metadata_from_processor from .instrumentation import native_value diff --git a/py/src/braintrust/integrations/pipecat/voice/synthesis.py b/py/src/braintrust/integrations/pipecat/voice/synthesis.py index e26897ed5..2534a58aa 100644 --- a/py/src/braintrust/integrations/pipecat/voice/synthesis.py +++ b/py/src/braintrust/integrations/pipecat/voice/synthesis.py @@ -3,11 +3,16 @@ import asyncio from dataclasses import dataclass, field -from braintrust.audio.attachments import UploadFailed, prepare_recording, upload_recording -from braintrust.audio.budget import source_budget -from braintrust.audio.jobs import RecordingJobs -from braintrust.audio.recording import encode_audio -from braintrust.audio.worker import RecordingBusy, encode_in_worker +from braintrust._audio import ( + RecordingBusy, + RecordingJobs, + UploadFailed, + encode_audio, + encode_in_worker, + prepare_recording, + source_budget, + upload_recording, +) from braintrust.logger import Span diff --git a/py/src/braintrust/integrations/pipecat/voice/test_alignment.py b/py/src/braintrust/integrations/pipecat/voice/test_alignment.py index 9466bbba9..419a6ecb8 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_alignment.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_alignment.py @@ -5,8 +5,8 @@ import numpy as np import soundfile as sf # pylint: disable=import-error -from braintrust.audio.export import Alignment -from braintrust.audio.recording import CallRecording +from braintrust._audio.export import AlignmentPublisher +from braintrust._audio.recording import CallRecording from pipecat.frames.frames import TTSAudioRawFrame, TTSStoppedFrame # pylint: disable=import-error from pipecat.transports.base_output import BaseOutputTransport # pylint: disable=import-error from pipecat.transports.base_transport import TransportParams # pylint: disable=import-error @@ -42,7 +42,7 @@ async def write_audio_frame(self, frame): sender._audio_queue = asyncio.Queue() output._media_senders = {None: sender} recording = CallRecording(enabled=enabled) - alignment = Alignment(Span(), recording) + alignment = AlignmentPublisher(Span(), recording) instrument_output(output, alignment) await output.start(None) self.addCleanup(sender._executor.shutdown) diff --git a/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py b/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py index 3774af5c5..da6b52400 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_instrumentation.py @@ -93,7 +93,7 @@ def encode(audio_format): observer.call_recording.capture(0, b"\0\0" * 480, 24000, 1) from unittest.mock import patch - from braintrust.audio.recording import CallRecording + from braintrust._audio.recording import CallRecording patcher = patch.object(CallRecording, "encode", lambda _self, audio_format: encode(audio_format)) patcher.start() @@ -115,7 +115,7 @@ async def test_encoding_failure_still_flushes_trace(self): observer.capture_transport = True from unittest.mock import patch - from braintrust.audio.recording import CallRecording + from braintrust._audio.recording import CallRecording observer.call_recording.capture(0, b"\0\0" * 480, 24000, 1) patcher = patch.object(CallRecording, "encode", side_effect=RuntimeError("encoder failed")) @@ -184,7 +184,7 @@ async def test_cancelled_finalizer_preserves_pending_segment_and_active_tail(sel import threading from unittest.mock import patch - from braintrust.audio.recording import CallRecording + from braintrust._audio.recording import CallRecording observer = NativeObserver(Span(), retain_audio=True) observer.capture_transport = True @@ -240,7 +240,7 @@ def upload(attachment): uploader = patch.object(Attachment, "upload", upload) uploader.start() self.addCleanup(uploader.stop) - from braintrust.audio.budget import source_budget + from braintrust._audio.budget import source_budget observer = NativeObserver(Span(), retain_audio=True, audio_format="wav") before = source_budget.used @@ -282,7 +282,7 @@ def upload(attachment): async def test_failed_segment_publishes_omission_before_shutdown(self): - from braintrust.audio import RecordingOptions, worker + from braintrust._audio import RecordingOptions, worker observer = NativeObserver( Span(), retain_audio=True, recording_options=RecordingOptions(segment_duration_seconds=0.1) @@ -315,7 +315,7 @@ async def test_segment_readiness_waits_for_upload_and_preserves_earlier_audio(mo import asyncio import threading - from braintrust.audio import RecordingOptions + from braintrust._audio import RecordingOptions from braintrust.logger import Attachment entered, release = threading.Event(), threading.Event() diff --git a/py/src/braintrust/integrations/pipecat/voice/test_realtime.py b/py/src/braintrust/integrations/pipecat/voice/test_realtime.py index 8467eacb1..5844d74ce 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_realtime.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_realtime.py @@ -78,7 +78,7 @@ async def test_disabled_does_not_wrap_connect_or_audio_send(self): self.assertEqual(observer.call_recording.bytes, 0) async def test_continuous_input_outlives_frame_limit_and_preserves_earlier_alignment(self): - from braintrust.audio import RecordingOptions + from braintrust._audio import RecordingOptions observer = NativeObserver( Span(), retain_audio=True, recording_options=RecordingOptions(segment_duration_seconds=600) diff --git a/py/src/braintrust/integrations/pipecat/voice/test_recording.py b/py/src/braintrust/integrations/pipecat/voice/test_recording.py index 88ba9f5b1..2561aede0 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_recording.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_recording.py @@ -2,7 +2,7 @@ import unittest from types import SimpleNamespace -from braintrust.audio.recording import CallRecording +from braintrust._audio.recording import CallRecording from .recording import capture_transport_output diff --git a/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py b/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py index 131a25ea5..4dbd522fb 100644 --- a/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py +++ b/py/src/braintrust/integrations/pipecat/voice/test_user_capture.py @@ -104,7 +104,7 @@ async def test_opt_out_keeps_metadata_without_retaining_encoding_or_attaching_au "braintrust.integrations.pipecat.voice.user_capture.encode_segments", side_effect=AssertionError("encoded"), ), - patch("braintrust.audio.attachments.Attachment", side_effect=AssertionError("attached")), + patch("braintrust._audio.attachments.Attachment", side_effect=AssertionError("attached")), ): observer, capture, turn = await self.exercise(False) self.assertEqual(capture.bytes, 0) @@ -187,7 +187,7 @@ async def push_context(): await observer.finish() def test_joined_clip_positions_include_padding_but_do_not_map_it(self): - from braintrust.audio.alignment import InputRanges + from braintrust._audio.alignment import InputRanges from .user_capture import encode_segments diff --git a/py/src/braintrust/integrations/pipecat/voice/user_capture.py b/py/src/braintrust/integrations/pipecat/voice/user_capture.py index ee0bd6f58..db3118c2a 100644 --- a/py/src/braintrust/integrations/pipecat/voice/user_capture.py +++ b/py/src/braintrust/integrations/pipecat/voice/user_capture.py @@ -9,13 +9,20 @@ import wave from collections import deque -from braintrust.audio.alignment import InputRanges -from braintrust.audio.attachments import UploadFailed, prepare_recording, upload_recording -from braintrust.audio.budget import source_budget -from braintrust.audio.jobs import RecordingJobs -from braintrust.audio.recording import encode_audio -from braintrust.audio.timeline import ClipTimeline, pcm_bytes_to_ms, samples_to_ms -from braintrust.audio.worker import RecordingBusy, encode_in_worker +from braintrust._audio import ( + ClipTimeline, + InputRanges, + RecordingBusy, + RecordingJobs, + UploadFailed, + encode_audio, + encode_in_worker, + pcm_bytes_to_ms, + prepare_recording, + samples_to_ms, + source_budget, + upload_recording, +) from pipecat.frames.frames import TranscriptionFrame # pylint: disable=import-error from ..llm_metrics import _metadata_from_processor