H-6763: Submit only completed Voice transcripts as interview answers - #9500
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
6987bc3 to
8f3f859
Compare
PR SummaryMedium Risk Overview The Realtime session mutes the microphone during assistant playback, tags and drops input that overlaps playback, removes tool-argument plumbing and The Brunch bridge normalizes transcript text, submits each The turn controller adds Reviewed by Cursor Bugbot for commit 935aa9f. Bugbot is set up for automated code reviews on this repo. Configure here. |
2f5aac8 to
83ba481
Compare
Realtime previously generated a continue_interview tool call after every semantic VAD commit, so silence or noise could produce model-invented answers. Disable inbound response creation and tools, route completed gpt-4o-transcribe transcripts to the Brunch bridge keyed by connection epoch, item ID, and content index, submit each accepted transcript exactly once, and surface empty or failed input as a recoverable not-heard notice. Amp-Thread-ID: https://ampcode.com/threads/T-01a0626d-0c66-725e-9b03-afe14d779f2d Co-authored-by: Amp <amp@ampcode.com>
Suspend microphone capture during assistant speech and discard overlapping transcripts so speaker echo cannot answer interview questions. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Classify playback overlap when speech begins so late transcript events remain authoritative for user turns that predate assistant audio. Co-authored-by: Cursor <cursoragent@cursor.com>
83ba481 to
25f968a
Compare
Restore canceled pending questions after unusable transcripts, release replay guards on rejection, and classify in-flight silence as unavailable. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5ed5a61. Configure here.
Leave active turns untouched for duplicate and unavailable transcripts, and defer cancelled speech restoration while Voice mode is paused. Co-authored-by: Cursor <cursoragent@cursor.com>


🌟 What is the purpose of this PR?
Stop Voice mode from submitting answers the user never said.
The Realtime session used semantic VAD with
create_response: trueand a requiredcontinue_interview({ answer })tool. Every VAD commit therefore made the model produce a tool call, and the browser bridge submitted the model-writtenanswerto Brunch withsource: "voice". The realinput_audio_transcription.completedresult only drove UI state. Silence or background noise could become invented answers such as "hi", "um", or "one".This PR makes the completed
gpt-4o-transcribetranscript the sole authoritative user answer. Realtime no longer responds or calls tools on its own between turns; it only transcribes the user's audio and renders the speech Petrinaut supplies.Stacked on #9496 and targets its branch; retarget to
mainonce #9496 merges.🔗 Related links
🚫 Blocked by
mainafter it merges🔍 What does this change?
openai-voice-policy.ts): semantic VADcreate_response: false(keepsinterrupt_response: true),tools: [],tool_choice: "none", instructions rewritten so the model only speaks suppliedresponse_text; policy versionbrunch-control-plane-v2.openai-realtime-session.ts): removed the tool-argument events,completeFunctionCall(), and function-call correlation.response.function_call_arguments.deltais ignored; aresponse.donecontaining afunction_callfails closed asinvalid-response. Speech requests remain out-of-band with tools disabled, as in FE-1570: Optimize spoken Brunch responses for Voice mode #9496.realtime-brunch-bridge.ts): consumes onlycompleted/transcription-failedtranscript events for the active connection epoch, normalizes whitespace, tracks processed${epoch}:${itemId}:${contentIndex}identities, and submits each accepted transcript exactly once with IDvoice-realtime:<epoch>:<itemId>:<contentIndex>. Rejections emit a newtranscript-rejected { reason }event (empty | failed | duplicate | unavailable | too-long); stale epochs are dropped. Brunch responses to transcript submissions now flow through the FE-1570 prepared-speech path (concise context + exact canonical question, canonical fallback, caching) or verbatim canonical speech. The inbound function-call delivery plumbing is removed; the preparation lifecycle is otherwise unchanged.voice-turn-controller.ts): new snapshot fieldinputNotice: "none" | "not-heard". Empty or failed transcripts setnot-heard, clear the provisional transcript, and leave the interview andlastCommittedTextunchanged; the notice clears when the user speaks again or an answer is submitted.Pre-Merge Checklist 🚀
🚢 Has this modified a publishable library?
This PR:
📜 Does this require a change to the docs?
The changes in this PR:
🕸️ Does this require a change to the Turbo Graph?
The changes in this PR:
conversation.item.input_audio_transcription.completeddocumentstranscript, optionallanguages, and opt-in per-tokenlogprobs(include: ["item.input_audio_transcription.logprobs"]), but no confidence score, so no threshold was invented. The protections here are: authoritative transcript only, empty-input rejection, per-item idempotency, and diagnostics.unavailable) are dropped without a UI notice, to avoid a misleading "not heard" message; they remain visible as bridge events.🐾 Next steps
inputNotice: "not-heard"in the Petrinaut Voice UI.PetrinautAiVoiceSessionStatein@hashintel/petrinauthas no notice field yet; that is a published-library change with a changeset.logprobsfor diagnostics to gather data before deciding on any confidence gating.mainafter FE-1570: Optimize spoken Brunch responses for Voice mode #9496 merges.🛡 What tests cover this?
apps/petrinaut-website:yarn vitest run src/server/voice src/main/app/voice-interview— 11 files, 152 tests.New or rewritten regression tests:
create_response: false,tools: [],tool_choice: "none"; Realtime neither answers for the user nor calls tools between turns.function_callresponse fails closed without sendingfunction_call_output; keyed transcripts (including empty ones) are emitted verbatim.continue_interviewarguments cannot be submitted; overlapping transcripts rejected asunavailable; over-limit transcripts rejected; Brunch responses to transcript submissions use FE-1570 preparation and exact-question assembly; preparation fallback speaks canonical content exactly once; cancelled preparation speaks nothing.empty/failedrejections return to listening withinputNotice: "not-heard"and no committed text;duplicate/unavailableleave state unchanged; notice clears on speech or submission, including while paused. Existing pause, mute, barge-in, replay, reconnect, and typed-handoff tests still pass.voice-preview.integration.test.ts): end-to-end from browser SDP through a completed transcript to canonical duplex audio, including silence, a legacy tool-argument delta, duplicate completion, prepared speech, replay, fallback, and a fabricated tool call failing closed.Also run:
yarn lint:tsc,yarn lint:eslint(0 errors),oxfmt --check,git diff --check.❓ How to test this?
PETRINAUT_OPENAI_VOICE_ENABLED=trueand an OpenAI key.inputNotice: "not-heard").📹 Demo
None yet.