Skip to content

H-6763: Speak finalized Brunch responses with OpenAI - #9357

Open
kostandinang wants to merge 3 commits into
kostandin/h-6763-openai-realtime-inputfrom
kostandin/h-6763-openai-canonical-speech
Open

H-6763: Speak finalized Brunch responses with OpenAI#9357
kostandinang wants to merge 3 commits into
kostandin/h-6763-openai-realtime-inputfrom
kostandin/h-6763-openai-canonical-speech

Conversation

@kostandinang

@kostandinang kostandinang commented Aug 26, 2026

Copy link
Copy Markdown

🌟 What is the purpose of this PR?

Speak finalized Brunch responses through OpenAI's dedicated Speech API. The exact canonical text already visible in Petrinaut—including a validated pending brunch_ask question—is selected once, sent unchanged for synthesis, and played while the microphone remains closed.

OpenAI Realtime continues to perform transcription only; it never generates assistant responses.

🔗 Related links

🚫 Blocked by

  • #9356 and its downstack prerequisites

🔍 What does this change?

  • Selects only finalized canonical assistant text and schema-validated brunch_ask.input.question values.
    • Reasoning, partial text, tool output, malformed asks, other tools, and non-assistant roles are excluded.
    • Questions come from structured AI SDK parts, never DOM scraping.
    • The real Brunch ask integration test proves an awaiting question is emitted without duplicate visible assistant text.
  • Assigns every speakable segment stable message/part identity and an exact-text fingerprint.
    • The browser sends { segmentId, text }.
    • The server recomputes the fingerprint and rejects any mismatched or malformed request.
  • Adds the app-owned /api/voice/speech edge.
    • It validates same-origin, content type, body size, text length, and segment identity.
    • It sends the exact selected text to OpenAI gpt-4o-mini-tts with server-fixed marin voice and MP3 output.
    • It accepts only audio/mpeg, disables caching, sanitizes upstream errors, and propagates timeouts/client cancellation.
    • It sends no content-changing instruction, “say exactly” prompt, or Realtime response.create event.
  • Adds cancelable, stale-safe browser playback.
    • New generations cancel old fetch/audio work.
    • Audio elements, response streams, blobs, and object URLs are cleaned up on success, failure, cancellation, and disposal.
  • Extends the half-duplex controller so the microphone stays closed throughout synthesis and playback and reopens only after speech drains and Brunch is ready.
  • Keeps canonical text visible if speech fails; there is no generated or alternate speech fallback.
  • Adds accessible synthesis/playback status and the required disclosure: spoken responses use an AI-generated OpenAI voice.
  • Updates ADR-0009, the website README, and the Petrinaut AI-assistant guide. No provider abstraction, package dependency, lockfile change, or ElevenLabs path is added.

Stack record: direct base 7c042206e919b80ef27ce46649bfe5202005b27c; PR head b98ec4772a9702e1cba096e5604e7dc77f46706d.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • does not modify any publishable blocks or libraries, or modifications do not need publishing

📜 Does this require a change to the docs?

The changes in this PR:

  • require changes to docs which are made as part of this PR

No existing documentation screenshot covers this voice surface.

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

🐾 Next steps

Voice PR4 will add delivery caching, authoritative reload reconciliation, stale-ask recovery, authentication, distributed quotas, telemetry, retention cleanup, a kill switch, and real completion/projection evidence. It must not start until the listed platform contracts are available.

🛡 What tests cover this?

  • Canonical-selection tests prove exact finalized assistant/ask selection and exclusion of reasoning, tool output, partial, malformed, and duplicate content.
  • Speech-handler tests prove request bounds, exact-text forwarding, fixed server policy, media-type enforcement, sanitized errors, timeout, disconnect, and stream cancellation.
  • Playback-controller tests prove cancellation, stale-generation rejection, and resource cleanup.
  • Turn-controller/component tests prove queued speech ordering, microphone suppression, failure fallback, disclosure, and accessible status.
  • The Brunch ask integration test proves the structured question reaches Petrinaut without duplicate assistant text.
Validation run
yarn workspace @hashintel/petrinaut test:unit --run
yarn workspace @hashintel/petrinaut lint:eslint
yarn workspace @hashintel/petrinaut lint:tsc
yarn workspace @hashintel/petrinaut build
yarn workspace @apps/petrinaut-website test:unit
yarn workspace @apps/petrinaut-website lint:eslint
yarn workspace @apps/petrinaut-website lint:tsc
yarn workspace @apps/petrinaut-website build
yarn workspace @hashintel/brunch-agent-transport-aisdk test:unit
yarn workspace @hashintel/brunch-agent-transport-aisdk lint:eslint
yarn workspace @hashintel/brunch-agent-transport-aisdk lint:tsc
yarn workspace @hashintel/brunch-agent-transport-aisdk build
yarn workspace @apps/brunch-agent test:unit
yarn workspace @apps/brunch-agent lint:eslint
yarn workspace @apps/brunch-agent lint:tsc
yarn workspace @apps/brunch-agent build
yarn workspace @local/petrinaut-arch-docs lint:arch-docs
yarn oxfmt --check apps/petrinaut-website apps/brunch-agent libs/@hashintel/petrinaut libs/@hashintel/brunch-agent

Results: 214 Petrinaut tests, 72 website tests, 12 transport tests, and 35 Brunch tests passed locally. Lint, typecheck, build, formatting, and architecture-doc checks passed.

The direct-base no-ElevenLabs audit and git diff --check passed. No package manifest or lockfile changed; provider-owned paths are explicitly OpenAI-named.

Credential-dependent checks not run: real OpenAI synthesis, remote Brunch, microphone/browser permission, exact spoken output in desktop Chrome/Edge/Safari, and deployed cancellation/failure behavior.

❓ How to test this?

  1. Run yarn workspace @apps/petrinaut-website test:unit plus its lint, typecheck, and build commands above.
  2. In a protected preview with OpenAI credentials and a real Brunch endpoint, complete one text turn and one voice turn.
  3. Confirm finalized assistant text and a pending structured question are each spoken once and match the visible text exactly.
  4. Cancel/end voice and induce a speech failure; confirm playback stops, the microphone remains safely closed until Brunch is ready, and visible text remains usable.

📹 Demo

Not included: credentialed browser/provider evidence remains an explicit acceptance requirement.

@vercel

vercel Bot commented Aug 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
petrinaut Ready Ready Preview Aug 27, 2026 2:18pm
3 Skipped Deployments
Project Deployment Actions Updated (UTC)
hash Ignored Ignored Preview Aug 27, 2026 2:18pm
hashdotdesign-tokens Ignored Ignored Preview Aug 27, 2026 2:18pm
petrinaut-docs Skipped Skipped Aug 27, 2026 2:18pm

Request Review

@github-actions github-actions Bot added area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team area/tests New or updated tests area/apps labels Aug 26, 2026

kostandinang commented Aug 26, 2026

Copy link
Copy Markdown
Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@kostandinang kostandinang changed the title Speak finalized Brunch responses with OpenAI H-6763: Speak finalized Brunch responses with OpenAI Aug 26, 2026
@kostandinang
kostandinang force-pushed the kostandin/h-6763-openai-canonical-speech branch from d4fe337 to 588eff3 Compare August 26, 2026 22:46
@kostandinang
kostandinang force-pushed the kostandin/h-6763-openai-realtime-input branch from cf09c5c to 7c04220 Compare August 26, 2026 23:04
@kostandinang
kostandinang force-pushed the kostandin/h-6763-openai-canonical-speech branch from 588eff3 to b98ec47 Compare August 26, 2026 23:04
@kostandinang kostandinang self-assigned this Aug 26, 2026
@kostandinang
kostandinang force-pushed the kostandin/h-6763-openai-realtime-input branch from 7c04220 to f27bfec Compare August 27, 2026 10:52
@kostandinang
kostandinang force-pushed the kostandin/h-6763-openai-canonical-speech branch from b98ec47 to 7250b21 Compare August 27, 2026 10:52
@kostandinang
kostandinang force-pushed the kostandin/h-6763-openai-canonical-speech branch from 7250b21 to c8c9656 Compare August 27, 2026 11:19
@kostandinang
kostandinang force-pushed the kostandin/h-6763-openai-realtime-input branch from f27bfec to 20c3818 Compare August 27, 2026 11:19
@cursor

cursor Bot commented Aug 27, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Introduces a new server route that forwards user-visible text to OpenAI with API-key handling and complex async voice state; impact is limited to the gated voice preview, with validation and sanitized errors, but failures or cancellation races could still affect UX.

Overview
Adds text-to-speech for the Brunch voice preview: finalized assistant text and pending validated brunch_ask questions are selected as canonical segments (stable IDs + FNV fingerprint), spoken unchanged through a new /api/voice/speech proxy to OpenAI gpt-4o-mini-tts, while Realtime stays transcription-only.

The voice turn flow now atomically ingests chat status and canonical segments via updateChat, queues speech in order (history is marked seen, not replayed), runs synthesizing/playing phases with the mic closed, and cancels stale fetch/audio on end or failure—leaving visible text and a recoverable error if synthesis fails. Client playback verifies segment fingerprints before POSTing; the server enforces same-origin, bounds, and matching segmentId/text.

UI adds accessible status for synthesis/playback and disclosure that speech is AI-generated. Brunch integration tests assert an awaiting ask emits no duplicate assistant text (initialText empty). Docs (README, ADR-0009, AI assistant guide) and dev/Vercel routing for the speech route are updated.

Reviewed by Cursor Bugbot for commit 9ef8f75. Bugbot is set up for automated code reviews on this repo. Configure here.

kostandinang and others added 2 commits August 27, 2026 14:30
Select only finalized canonical assistant text and validated brunch_ask questions, then forward the exact fingerprinted text through the app-owned OpenAI Speech edge.

Queue cancelable MP3 playback behind Brunch, keep the microphone closed until speech drains, reject stale generations, disclose the AI-generated voice, and preserve visible text on failure.

Amp-Thread-ID: https://ampcode.com/threads/T-01a03fb3-fd3d-737f-b4c6-1fc9282950bf
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7990e51. Configure here.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) area/tests New or updated tests type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

1 participant