Report who was speaking, and when - #5612
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests.
🚀 New features to boost your workflow:
|
markbackman
force-pushed
the
speaking-observer
branch
6 times, most recently
from
September 3, 2026 02:51
f3bb55a to
434c49f
Compare
markbackman
commented
Sep 3, 2026
| acts on, and follows the speech by however long the ruling took. | ||
| """ | ||
|
|
||
| USER_SPEECH_STARTED = "user_speech_started" |
Contributor
Author
There was a problem hiding this comment.
More verbose naming, but it's more clear.
The idea is that it's useful to know the actual user speaking timings and the user turn timings. This will give us more flexibility to build out visualizations.
aconchillo
reviewed
Sep 4, 2026
| ) | ||
| if isinstance(frame, InterruptionFrame): | ||
| return SpeechEvent(kind=SpeechEventKind.INTERRUPTION, timestamp=self._now()) | ||
| return None |
Contributor
There was a problem hiding this comment.
elif and remove return None
Contributor
There was a problem hiding this comment.
actually don't remove return None I missed the return type
aconchillo
approved these changes
Sep 4, 2026
Contributor
|
LGTM! Very minor comment. |
A conversation is people taking the floor and occasionally taking it from each other, and nothing reported that: the turn observer answers where one turn ends and the next begins, which is a policy, and a policy that ships inside a record can never be revised. SpeakingObserver reports each moment as it happens — voice activity, the turn strategy's verdict, the bot's own audio, and an interruption — leaving what counts as a turn to whoever reads them. A moment is a kind and a time: the kind names whose speech it describes, so nothing on the record repeats what another field already says. The moments that close a stretch of speech name where it began, so an interval reads whole from one record, and a stretch whose closing moment never arrives stays open rather than quietly joining itself to the next one. The user appears at two layers, and each kind names which one it is: the speech itself as the detector heard it, and the turn strategy's ruling on that speech, which is what the rest of the pipeline acts on. Speech that never becomes a turn — a cough, a false start, a pause mid-sentence — reaches only the second, and appears nowhere else. Voice activity is placed where speech began and ended rather than where the detector confirmed it, since the detector's own delay is what an interval drawn from it would otherwise include.
markbackman
force-pushed
the
speaking-observer
branch
from
September 4, 2026 21:29
434c49f to
c8f564a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SpeakingObserver, reporting a conversation's speaking lifecycle as it happens. Seven kinds, each naming its subject and its layer:user_speech_started/user_speech_stopped(the speech itself, as the detector heard it),user_turn_started/user_turn_stopped(the turn strategy's ruling on that speech, which is what the rest of the pipeline acts on),bot_speech_started/bot_speech_stopped, andinterruption.start_secs/stop_secs), so an interval drawn from arrival times is fat at both ends by exactly that. The moments reporttimestamp - start_secsandtimestamp - stop_secsinstead.timestamp - started_aton the same row, so it isn't carried — a derived field is one more thing that can disagree with itself.user_speech_*and nowhere else — including in bars drawn from these events, which would otherwise show someone speaking straight through their own pauses. The lag between the two sources is already accounted for byLatencyBreakdown.TurnTrackingObserveranswers where one turn ends and the next begins — a debounce timer after the bot stops — and that is a policy. A policy that ships inside a record can never be revised; these moments can be grouped again later, differently, over the same history.A record is a kind, a time, and — where one closes a stretch of speech — the time it began:
{ "kind": "bot_speech_stopped", "timestamp": 1000.5, "started_at": 994.2 }Testing
uv run pytest tests/test_speaking_observer.py— 9 tests, 100% line coverage: backdating against the detector's delay, closing moments naming their own start, voice activity and the strategy reported apart, a barge-in reading as an overlap, a stretch whose start was missed, and non-speaking frames left alone.broadcast_frame_instancegives each a fresh one), so deduplicating on the ID alone cannot tell them apart. Only the downstream copy is read, and a test builds both siblings to check it.Context
Part of the observability work for Pipecat Cloud, alongside
LatencyBreakdown(#5445),ServiceMetricsObserver(#5607) andStartupTimingObserver(#5600). These moments are what draws a session's timeline — who held the floor, where two voices overlapped — and the silences between them are what the latency breakdown accounts for.