Repository navigation
Add Pipecat voice traces and opt-in audio - #836
David Elner (delner) wants to merge 18 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: babe2621bc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "Codex (@codex) review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "Codex (@codex) address that feedback".
| async def on_process_frame(self, data: Any) -> None: | ||
| if self._voice is not None: | ||
| return |
There was a problem hiding this comment.
Preserve LLM usage metrics in native voice traces
When a supported Pipecat 1.12 voice pipeline is bound, this early return prevents the legacy _capture_metrics path from seeing any MetricsFrame. NativeObserver.on_push_frame only stores those frames as pipecat.events; it never adds LLMUsageMetricsData or TTFB values to the pipecat.llm_response span. Consequently normal voice calls lose token/cost and time-to-first-token metrics even when Pipecat usage metrics are enabled. Extract and log the metrics in the native observer before bypassing the existing handler.
Useful? React with 👍 / 👎.
| braintrust = "braintrust.integrations.harbor:HarborPlugin" | ||
|
|
||
| [project.optional-dependencies] | ||
| audio = ["numpy>=1.26", "soundfile>=0.13.1"] |
There was a problem hiding this comment.
Include audio dependencies in the all extra
This adds the audio optional extra, but the existing all extra still omits both NumPy and SoundFile. Users following the documented pip install "braintrust[all]" path and then enabling Pipecat audio recording will hit the encoder's missing-dependency error unless they separately discover and install braintrust[audio]; add these dependencies to all so it continues to install every optional feature.
Useful? React with 👍 / 👎.
Abhijeet Prasad (AbhiPrasad)
left a comment
There was a problem hiding this comment.
most of these tests seem not necessary and use a lot of mocks/fakes. Can we just use vcr tests instead? We can probably combine some redundant tests or remove lower value tests in py/src/braintrust/integrations/pipecat/test_pipecat.py as well
8130ef7 to
20b9392
Compare
| def test_prepare_recording_preserves_disabled_result(): | ||
| assert prepare_recording("call", lambda: None) is None |
There was a problem hiding this comment.
I'd take another pass to see if we can remove low value tests like this, or tests that use excess mocks/fakes.
also asking the agent to combine redundant tests is also pretty effective.
a lot of these unit tests don't give me a huge amount of confidence, but maybe that is fine.
There was a problem hiding this comment.
Yeah I tried to do a couple of passes on this, not sure if it was very effective at reduction. I might have to do some more careful combing.
There was a problem hiding this comment.
can we remove these added readmes? The code/file structure should self document itself just fine, these are going to go stale.
| from .options import RecordingOptions | ||
|
|
||
|
|
||
| __all__ = ["RecordingOptions"] |
There was a problem hiding this comment.
why is this the only export from this folder? Does everything else get exported directly?
| from .worker import RecordingBusy | ||
|
|
||
|
|
||
| encoded_budget = ByteBudget(32 * 1024 * 1024) |
There was a problem hiding this comment.
IMO we don't need a separate budget for audio. We can just use the generic attachment budget.
There was a problem hiding this comment.
- I find it hard to reason about all the unit versions (sec/ms, byte offsets, etc.). I wonder if we can have some stronger named helpers to reason about this
- the classes here feel like they mix reponsibilities too much.
class Alignmentdoes both mapping to recording and callingspan.log. I'd rather those two be explicitly two different steps / functions / classes
I did some vibing about the class structure, and I think I'd like to see something like so (following is AI GENERATED):
| Class | Owns / does |
|---|---|
| RecordingSession | Call-level state: accepts capture while active, rotates segments, handles limits, drains on finish. Owns the active segment and tracks pending segment jobs. |
| AudioSegment | One segment’s state and timeline: OPEN → PENDING → READY / OMITTED. When submitted, it holds an immutable PCM snapshot; once the worker finishes, it holds the encoded result or omission reason. |
| AudioEncoder | Converts a segment snapshot into encoded bytes and metadata. Keeps codec details out of recording lifecycle code. |
| RecordingWorker | Runs encoding off the event loop, enforces concurrency limits, and keeps PCM alive until native work actually stops. |
| AlignmentResolver | Purely maps sample ranges to ready segments and produces selection and clip timeline records. It does not know about spans or loggers. |
| RecordingExporter | Turns encoded results into Braintrust attachments and publishes descriptors and selections through the SDK logger. Owns the encoded-byte lease for as long as the exporter retains the attachment. |
PipecatRecordingAdapter
├─ capture PCM ─────────────→ RecordingSession
│ ├─ active AudioSegment
│ └─ submitted AudioSegment
│ ↓
│ RecordingWorker
│ ↓
│ AudioEncoder
│ ↓
│ RecordingExporter → SDK logger
│
└─ turn sample ranges ──────→ AlignmentResolver ───→ RecordingExporter
| from collections import deque | ||
| from contextvars import ContextVar | ||
|
|
||
| from braintrust.audio.timeline import pcm_bytes_to_ms, samples_to_ms |
There was a problem hiding this comment.
So I realized that braintrust.audio is now public API. I think we should make it _audio (the folder name) so that users know it's private/internal to the SDK. Unfortunately we don't have a better way in python to do this.
There was a problem hiding this comment.
also I would really prefer if everything in these files were imported from braintrust._audio, helps me see what exactly the public API of the audio module is in py/src/braintrust/audio/__init__.py
| class Alignment: | ||
| """Publish resolved selections and clip timelines to their owning spans.""" | ||
|
|
||
| def __init__(self, root, recording): |
There was a problem hiding this comment.
feel like all of these should be typed?
| "audio.selection": selections[0] if len(selections) == 1 else None, | ||
| } | ||
| ) | ||
| self.root.log(metadata={"braintrust.alignment.ranges_omitted": self.omitted}) |
There was a problem hiding this comment.
The fact that the Alignment class also logs feels confusing. I would like this to be a separate responsibility. But open to discussion.
| def release_once(): | ||
| nonlocal released | ||
| if not released: | ||
| released = True | ||
| release() |
There was a problem hiding this comment.
there feels like a better way we can do this with some of the python concurrency mechanisms built in.
| segment_duration_seconds: float = 60 | ||
| max_duration_seconds: float = 1800 | ||
| max_buffer_bytes: int = 32 * 1024 * 1024 | ||
| flush_fraction: float = 0.5 |
There was a problem hiding this comment.
why did we pick these? Ditto with some of the other byte numbers we picked.
| max_bytes=8 * 1024 * 1024, | ||
| max_duration_ms=120000, | ||
| max_packets=16000, |
There was a problem hiding this comment.
feel like there are enough of these constants around that we should have a constants.py file and write comments about why they exist.
| def __del__(self): | ||
| if getattr(self, "bytes", 0): | ||
| self.clear() |
There was a problem hiding this comment.
why do we do this?
| self.bytes = 0 | ||
| self.packets.clear() | ||
|
|
||
| def capture(self, channel, pcm, sample_rate, channels, *, observed_ns=None, observed_unix_ms=None): |
There was a problem hiding this comment.
this capture logic is pretty confusing, with a lot of conditionals. I think we need to codify the state machine better here.
I was hoping the usage of AudioSegment would make this better, but maybe not.
Inspect conversations, tool calls, and audio through the existing
setup_pipecat()API. Supports cascade and OpenAI Realtime voice pipelines.braintrust[audio].Example trace
Design
Performance
53-second cascade replay, 24 kHz audio, 30-second rotation, eight attachments. Means of two fresh-process trials on Apple M5 Pro; baseline is tracing with recording disabled.
¹ Provider-audio schedule to local output write, not caller-heard latency. ² Startup-to-final-drain RSS growth. Offline replay excludes network/upload; production latency and concurrency remain unmeasured. Ogg uses 10.7× fewer audio bytes than WAV in this call.