Skip to content

fix(voice): show played speech once - #591

Closed
kev (kevchoi) wants to merge 2 commits into
kevchoi/fix-unowned-voice-speechfrom
fix/livekit-realtime-v3
Closed

kev (kevchoi) wants to merge 2 commits into
kevchoi/fix-unowned-voice-speechfrom
fix/livekit-realtime-v3

Conversation

@kevchoi

@kevchoi kev (kevchoi) commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

LiveKit records generated model output in llm_response and played speech in agent_speech. Treating both as conversational replies showed the greeting and closing reply twice.

Represent voice model calls as existing llm_analysis work and keep agent_speech as the response. Generated messages, tool calls, usage, and timing remain inspectable in expandable execution entries. No UI changes are required.

Also preserve voice turn roles, structural ownership when turn.id is absent, and empty user turns. Sessions without recording metadata are accepted. The fixture covers the attached conversation’s 18 voice-session spans, including greeting, tool continuation, and closing speech, with identity metadata removed.

Stacked on #589. Implementation reference: Python SDK #1069.

Validation

  • All 105 trajectory tests pass; formatting and typed-boundary checks pass.
  • Playwright with the same app and attached 19-span trace: greeting and closing transcript counts each drop from two to one, with no browser page errors.
  • Compared baseline and updated WASM output: speech responses, tool execution, recordings, model, timing, and usage are preserved.
  • Independent review found no blocking issue. Other SDK modes have not been verified end to end.

Screenshots

Same app and viewport; baseline #591 versus the updated Lingua WASM.

Before: generated output and played speech appear as duplicate replies.

Before thread
Open full size

After: played speech appears once; model generations are expandable execution rows.

After thread
Open full size

Expanded work before:

Before expanded work
Open full size

Expanded work after: model calls and tool execution remain separate inspectable entries.

After expanded work
Open full size

@kevchoi kev (kevchoi) changed the title fix(voice): preserve Realtime turn roles fix(voice): show played speech once Nov 10, 2026
kev (kevchoi) added a commit that referenced this pull request Nov 10, 2026
working with @delner on the last mile portion for cascaded/gpt-realtime
traces

---

LiveKit records generated output in `llm_response` and played speech in
`agent_speech`. Represent voice model calls as existing `llm_analysis`
work so the thread displays played speech once, while generated
messages, tool calls, usage, and timing remain inspectable. No
`VoiceTurnRole`, public schema additions, or UI changes.

Also preserve unowned `user_speech` intervals as empty standalone turns
when the session has no explicit user turns. Keep their IDs and actual
intervals without inventing transcripts or replies. Other unowned
modeled voice spans are skipped; invalid explicit turn references retain
diagnostics. Loaded empty requests remain hidden by the current UI.

When no explicit or inherited `turn.id` exists, use the nearest
structural `user_turn` or `agent_turn` ancestor in the same session. The
search is bounded. Missing ownership is skipped, and invalid explicit
IDs remain nonfatal diagnostics; healthy spans continue rendering. No
timestamp or text matching.

The Realtime fixture covers the attached conversation’s greeting, tool
exchange, and closing reply in one fixture and regression test. Expected
turns live in JSON; the shared runner checks batching, body/header
imports, randomized load order, and coverage. The same fixture is also
tested with all `turn.id` fields removed. The orphan-speech and tool
fixtures cover separate ownership cases. Implementation reference:
[Python SDK
#1069](braintrustdata/braintrust-sdk-python#1069).

### Validation

- All 105 trajectory tests pass, including the new Realtime regression
and existing orphan-speech coverage. Formatting and typed-boundary
checks pass.
- Fresh Playwright captures compare `03f15aa6` with `13a055ea`: the
supplied trace is unchanged; a copy with only `turn.id` removed regains
assistant speech and model work. Both runs have no browser page errors.
- Independent review found no blockers or dependencies on #591, which is
closed.

The hosted app still needs its Lingua pin/build updated. Audio-only
bubbles and per-turn playback are outside this change.

### Screenshots

Latest change, `03f15aa6` → `13a055ea`, with the same app and viewport.

Supplied trace with IDs intact: unchanged.

![Original trace
before](https://api.prod.starfolk.io/api/v1/public/attachments/64D08PUfbIKFheq7HVd0Ud/original-before.png)
[Open full
size](https://api.prod.starfolk.io/api/v1/public/attachments/64D08PUfbIKFheq7HVd0Ud/original-before.png)

![Original trace
after](https://api.prod.starfolk.io/api/v1/public/attachments/7OUFEIW0xVMXgQdbGxrq70/original-after.png)
[Open full
size](https://api.prod.starfolk.io/api/v1/public/attachments/7OUFEIW0xVMXgQdbGxrq70/original-after.png)

Same trace with only `turn.id` removed, before: assistant speech and
model work are missing.

![Missing turn IDs
before](https://api.prod.starfolk.io/api/v1/public/attachments/21fTH7BgVOfxCOhB4L2ceL/missing-turn-ids-before.png)
[Open full
size](https://api.prod.starfolk.io/api/v1/public/attachments/21fTH7BgVOfxCOhB4L2ceL/missing-turn-ids-before.png)

After: structural ownership restores the conversation and model work.

![Missing turn IDs
after](https://api.prod.starfolk.io/api/v1/public/attachments/3GBmzPs4pGuBhxl9Z28wrw/missing-turn-ids-after.png)
[Open full
size](https://api.prod.starfolk.io/api/v1/public/attachments/3GBmzPs4pGuBhxl9Z28wrw/missing-turn-ids-after.png)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant