Skip to content

H-6763: Add realtime voice interviewing experiments - #9297

Open
kostandinang wants to merge 19 commits into
mainfrom
kostandin/h-6763-support-for-realtime-audio-interviewing-of-domain-experts
Open

H-6763: Add realtime voice interviewing experiments#9297
kostandinang wants to merge 19 commits into
mainfrom
kostandin/h-6763-support-for-realtime-audio-interviewing-of-domain-experts

Conversation

@kostandinang

@kostandinang kostandinang commented Aug 24, 2026

Copy link
Copy Markdown

🌟 What is the purpose of this PR?

This draft prototypes realtime voice interviewing of domain experts in Petrinaut and separates the voice provider, elicitor, and mock projector so their integration boundaries can be evaluated independently.

It supports three interview configurations:

  • OpenAI Realtime + mock tools establishes the native voice interaction ceiling.
  • OpenAI Realtime + Brunch uses OpenAI for browser audio, transcription, turn detection, and speech rendering while Brunch owns the interview and tool state.
  • ElevenLabs + Brunch uses ElevenLabs Speech Engine as the speech edge while Brunch remains authoritative.

Any supported configuration can add projector=mock to exercise incremental and final handoff into a persisted Petrinaut net. The intent is to compare providers against the same elicitor without moving authoritative conversation state, structured questions, or elicitation policy out of Brunch.

🔗 Related links

☁️ Preview deployment

For the Vercel preview, set OPENAI_API_KEY, ELEVENLABS_API_KEY, and ELEVENLABS_SPEECH_ENGINE_ID for the Petrinaut project’s Preview environment, then redeploy. Keep provider keys server-only; do not expose them as VITE_* variables.

OpenAI + mock works with the OpenAI key alone. Brunch modes additionally need a deployed /api/voice-experiment/brunch-chat relay to Brunch, and ElevenLabs needs its public Speech Engine wss://…/ws bridge.

🔍 What does this change?

  • Adds a shared voice shell selected per page with independent voiceProvider, elicitor, and optional projector URL parameters. Legacy voiceExperiment links remain supported.
  • Keeps Ask AI visible by default. One microphone click starts the selected session and opens the transcript/tool-call panel; minimizing returns to Ask AI without ending an active conversation.
  • Normalizes observable connection, transcript, response, tool, projection-readiness, and error events while provider-specific audio/session protocols stay inside their adapters.
  • Implements OpenAI Realtime over browser WebRTC with server-minted ephemeral credentials and immutable server-side presets for mock and Brunch modes.
  • Uses final, non-empty OpenAI transcripts as the only input admitted to Brunch. Brunch responses and brunch_ask correlation flow through the existing AI SDK transport; OpenAI renders the resulting text as out-of-band audio.
  • Keeps Brunch text authoritative in OpenAI + Brunch mode. Realtime-generated speech transcripts cannot replace the source response in the UI.
  • Implements ElevenLabs Speech Engine around the same real Brunch transport, including finalized-turn forwarding, streamed speech responses, cancellation, and diagnostics polling.
  • Enforces alternating interviewer/expert turns. OpenAI uses server VAD without automatic response creation and reopens the microphone only after output audio has drained; ElevenLabs reopens it after provider playback returns to listening.
  • Reconciles partial, final, delayed, and revised transcript events to avoid duplicate or empty expert turns.
  • Moves the reusable BrunchVoiceBridge into the private AI SDK transport workspace package so both voice integrations share pending-ask and applied-sweep behavior.
  • Adds a provider-neutral draft contract and mock projector. Coherent capture batches and applied brunch_sweep results update one revisioned live net; finishing forces a final projection and falls back to a labelled placeholder when evidence is incomplete.
  • Persists projected nets with transcript, conversation id, revision, source, and warnings. Stale responses are ignored, manual edits are preserved, and background updates do not hijack navigation.
  • Adds credential validation, rate limiting, fixed-origin local proxies, resource cleanup, and focused endpoint, adapter, bridge, diagnostics, transcript, selection, and projection tests.
🏗️ Agent notes

The provider and elicitor are fixed for the lifetime of the page. Changing either requires a reload and a new conversation so provider history, pending asks, queued audio, and microphone state cannot leak between comparisons.

Supported interview selections are:

  • ?voiceProvider=openai&elicitor=mock
  • ?voiceProvider=openai&elicitor=brunch
  • ?voiceProvider=elevenlabs&elicitor=brunch

Add &projector=mock to any selection to exercise live and final mock-net projection. elevenlabs&elicitor=mock is rejected because each Speech Engine conversation is bound to its server-side callback and the current resource is wired to Brunch.

In both real-elicitor paths, Brunch owns authoritative history, elicitation policy, brunch_ask, tool activity, and evidence admission. Provider partial transcripts are display-only; only finalized expert input enters Brunch.

OpenAI Realtime is used as a speech renderer for Brunch output, but model-generated speech is not guaranteed to be verbatim. The UI therefore preserves the exact Brunch response as authoritative and suppresses generated paraphrases from the visible transcript.

The mock projector validates the integration contract; it is not the production Brunch read-time projector. Structured experiment captures produce a state-step-flow graph. An applied Brunch sweep emits readiness and can produce the best available mock draft, while refused sweeps do not trigger projection.

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

The shared bridge export is added to a private workspace package. The Petrinaut library touch only lets the experimental voice dock coexist with Ask AI and does not add a published API.

📜 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

Current provider configuration, URL selection, projector behavior, local run instructions, security boundaries, and known caveats are documented in apps/petrinaut-website/src/main/app/local-storage-demo/voice-experiment/README.md. The linked spike remains the original prototype plan rather than the final comparison record.

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

The changes in this PR:

  • do not affect the execution graph

The website already depended on the private AI SDK transport package; this adds a build/export entry without changing workspace topology or turbo.json.

⚠️ Known issues

  • This is an experiment, not a production voice surface. Repeatable comparison runs, recordings, a scorecard, and the final provider decision are still pending.
  • The projector is deliberately a browser-side mock. It validates live and final integration contracts but does not perform the production Brunch read-time projection.
  • Voice transcripts and draft metadata persist in the local-storage demo, not in a principal-owned durable backend session.
  • OpenAI speech rendering may deviate from Brunch's exact wording; Brunch's source text remains authoritative.
  • ElevenLabs + mock is unsupported without a separately routable Speech Engine callback/resource.
  • The real-Brunch voice routes are local prototype wiring. Production authentication, principal-owned sessions, and deployment routing remain follow-up work.
  • Strict alternation is implemented by gating the microphone; full open-microphone barge-in and stale-audio invalidation remain production-hardening work.
  • ElevenLabs local testing requires a configured Speech Engine resource and a public wss://…/ws endpoint for the local server.

🐾 Next steps

  • Run the same expert script repeatedly through OpenAI + Brunch and ElevenLabs + Brunch, using OpenAI + mock as the native-interaction reference.
  • Record latency, transcription quality, interruption behavior, elicitor/tool fidelity, state integrity, projection behavior, and integration complexity.
  • Attach short recordings and comparison evidence, then record the provider decision.
  • Replace the browser mock projector with the real Brunch read-time projector and move session/transcript persistence behind production authentication.

🛡 What tests cover this?

  • OpenAI and ElevenLabs credential endpoints: request validation, origin checks, provider failures, rate limiting, and response shaping.
  • OpenAI server presets: mock elicitor versus fixed Brunch speech-renderer mode.
  • Browser adapters: connection lifecycle, transcript normalization, strict turn handoff, audio-drain gating, tool events, projection readiness, errors, and cleanup.
  • OpenAI + Brunch: finalized transcript forwarding, authoritative response display, out-of-band speech rendering, generated-paraphrase suppression, applied-sweep signaling, and pending microphone gating.
  • Brunch bridge and ElevenLabs Speech Engine: brunch_ask continuation, streamed responses, cancellation, revised-turn serialization, structured capture diagnostics, and applied-versus-refused sweep handling.
  • Mock projection: capture sanitization, coherent-graph readiness, fallback drafts, revision propagation, and persisted draft metadata.
  • URL selection: all supported provider/elicitor configurations, mock-projector combinations, invalid combinations, and legacy links.

❓ How to test this?

  1. Follow apps/petrinaut-website/src/main/app/local-storage-demo/voice-experiment/README.md to configure local provider values.

  2. Test native OpenAI mock mode at /?voiceProvider=openai&elicitor=mock.

  3. Start the real Brunch server, then test OpenAI against Brunch at /?voiceProvider=openai&elicitor=brunch. Confirm finalized expert speech reaches voice:<conversation-id>, Brunch's response is displayed and spoken, and the microphone reopens only after playback drains.

  4. For ElevenLabs, also start the voice:dev Speech Engine server, expose /ws through a public TLS tunnel, configure the resource URL, and open /?voiceProvider=elevenlabs&elicitor=brunch.

  5. In both Brunch modes, complete at least one brunch_ask suspend/resume cycle and confirm tool activity is visible without duplicate turns.

  6. Repeat the supported URLs with &projector=mock. Confirm coherent captures or an applied sweep create one live draft, later revisions update that draft, manual edits are not overwritten, and Finish and create net forces a final result.

  7. Run the focused checks:

    yarn install --immutable
    yarn workspace @hashintel/brunch-agent-transport-aisdk build
    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 test:unit
    yarn workspace @apps/brunch-agent lint:eslint
    yarn workspace @apps/brunch-agent lint:tsc
    yarn workspace @apps/brunch-agent test:unit
    yarn workspace @apps/brunch-agent build
    yarn workspace @apps/petrinaut-website lint:eslint
    yarn workspace @apps/petrinaut-website lint:tsc
    yarn workspace @apps/petrinaut-website test:unit
    yarn workspace @apps/petrinaut-website build

📹 Demo

Watch the voice interview experiment demo on Loom

@vercel

vercel Bot commented Aug 24, 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 25, 2026 6:01pm
petrinaut-docs Ready Ready Preview Aug 25, 2026 6:01pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
hash Ignored Ignored Preview Aug 25, 2026 6:01pm
hashdotdesign-tokens Ignored Ignored Preview Aug 25, 2026 6:01pm

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/apps labels Aug 24, 2026
@CLAassistant

CLAassistant commented Aug 24, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

) => {
// Never add request bodies, API keys, or upstream response bodies here.
// oxlint-disable-next-line no-console
console.error(`[OpenAI Realtime experiment] ${reason}`, context);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Semgrep identified an issue in your code:
Detected string concatenation with a non-literal variable in a util.format / console.log function. If an attacker injects a format specifier in the string, it will forge the log message. Try to use constant values for the format string.

Dataflow graph
flowchart LR
    classDef invis fill:white, stroke: none
    classDef default fill:#e7f5ff, color:#1c7fd6, stroke: none

    subgraph File0["<b>apps/petrinaut-website/api/voice-experiment/openai-realtime-session.ts</b>"]
        direction LR
        %% Source

        subgraph Source
            direction LR

            v0["<a href=https://github.com/hashintel/hash/blob/3488a1fe5689cbb8140641d72794614556b0d1f7/apps/petrinaut-website/api/voice-experiment/openai-realtime-session.ts#L115 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 115] `[OpenAI Realtime experiment] ${reason</a>"]
        end
        %% Intermediate

        %% Sink

        subgraph Sink
            direction LR

            v1["<a href=https://github.com/hashintel/hash/blob/3488a1fe5689cbb8140641d72794614556b0d1f7/apps/petrinaut-website/api/voice-experiment/openai-realtime-session.ts#L115 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 115] `[OpenAI Realtime experiment] ${reason</a>"]
        end
    end
    %% Class Assignment
    Source:::invis
    Sink:::invis

    File0:::invis

    %% Connections

    Source --> Sink


Loading

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by unsafe-formatstring.

You can view more details about this finding in the Semgrep AppSec Platform.

Comment on lines +7 to +8
const OPENAI_CLIENT_SECRETS_URL =
"https://api.openai.com/v1/realtime/client_secrets";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Semgrep identified an issue in your code:
A hardcoded secret is identified. Store it properly in an environment variable.

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by node_secret.

You can view more details about this finding in the Semgrep AppSec Platform.

const originalVercelEnvironment = process.env.VERCEL_ENV;

beforeEach(() => {
process.env.OPENAI_API_KEY = "primary-secret-that-must-stay-server-side";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Semgrep identified an issue in your code:
A hardcoded API Key is identified. Store it properly in an environment variable.

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by node_api_key.

You can view more details about this finding in the Semgrep AppSec Platform.

) => {
// Never add request bodies, API keys, or upstream response bodies here.
// oxlint-disable-next-line no-console
console.error(`[OpenAI Realtime experiment] ${reason}`, context);
Comment thread apps/brunch-agent/src/elevenlabs-speech-engine.ts Fixed
) => {
// Never add request bodies, API keys, or upstream response bodies here.
// oxlint-disable-next-line no-console
console.error(`[ElevenLabs voice experiment] ${reason}`, context);
const SESSION_ENDPOINT = "/api/voice-experiment/openai-realtime-session";
const REALTIME_CALLS_ENDPOINT = "https://api.openai.com/v1/realtime/calls";
const CONNECTION_TIMEOUT_MS = 20_000;
const WAIT_FOR_USER_TOOL_NAME = "wait_for_user";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Semgrep identified an issue in your code:
A hardcoded username in plain text is identified. Store it properly in an environment variable.

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by node_username.

You can view more details about this finding in the Semgrep AppSec Platform.

operation: Operation,
productName: string = PRODUCT_NAME,
): string {
if (!IDENTIFIER.test(operation)) {

@semgrep-code-hashintel semgrep-code-hashintel Bot Aug 24, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ensure that the regex used to compare with user supplied input is safe from regular expression denial of service.

🍰 Removed in commit eaecb5e 🍰

@kostandinang
kostandinang force-pushed the kostandin/h-6763-support-for-realtime-audio-interviewing-of-domain-experts branch from 64c8ee7 to eaecb5e Compare August 24, 2026 18:28
@github-actions github-actions Bot removed the type/legal Owned by the @legal team label Aug 24, 2026
const SESSION_ENDPOINT = "/api/voice-experiment/openai-realtime-session";
const REALTIME_CALLS_ENDPOINT = "https://api.openai.com/v1/realtime/calls";
const CONNECTION_TIMEOUT_MS = 20_000;
const WAIT_FOR_USER_TOOL_NAME = "wait_for_user";
Permit the regular Petrinaut development origins so OpenAI voice sessions can reach the real Brunch elicitor through the local proxy.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel
vercel Bot temporarily deployed to Preview – petrinaut-docs August 25, 2026 13:02 Inactive
@vercel
vercel Bot temporarily deployed to Preview – petrinaut August 25, 2026 13:02 Inactive
Create and persist a mock Petrinaut net from completed voice interviews so provider and elicitor combinations can be validated end to end.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel
vercel Bot temporarily deployed to Preview – petrinaut-docs August 25, 2026 13:17 Inactive
Make the URL parameter describe the component that projects interview evidence into a net, independently of the elicitor.

Co-authored-by: Cursor <cursoragent@cursor.com>
Expose settled elicitation results as a revisioned live draft so both voice integrations can validate projection before the interview finishes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread apps/petrinaut-website/api/voice-experiment/elevenlabs-conversation-token.test.ts Dismissed
Comment thread apps/petrinaut-website/api/voice-experiment/openai-realtime-session.test.ts Dismissed
Comment thread apps/petrinaut-website/api/voice-experiment/openai-realtime-session.ts Dismissed
@kostandinang
kostandinang marked this pull request as ready for review August 25, 2026 15:17
@cursor

cursor Bot commented Aug 25, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Introduces new server endpoints that mint third-party voice credentials and bridge live audio transcripts into Brunch chat, though scoped to experiments with explicit validation and bounded diagnostics rather than production auth flows.

Overview
Adds realtime voice interviewing to the Petrinaut local-storage demo, with independent URL switches for voice provider (openai / elevenlabs), elicitor (mock / brunch), and optional projector=mock.

OpenAI Realtime gets server-minted ephemeral credentials and fixed server-owned session presets (mock capture tools vs Brunch speech-renderer). The browser adapter admits only finalized expert transcripts into Brunch when elicitor=brunch, keeps Brunch text authoritative in the UI, and gates the mic until playback finishes.

ElevenLabs + Brunch adds a local Speech Engine server in brunch-agent that forwards finalized turns through a shared BrunchVoiceBridge into existing /api/chat, plus a read-only diagnostics API the website polls for transcripts, tool calls, and projection-ready signals.

The voice experiment panel (launcher, transcript, tool diagnostics, logs, live mock projection) wires into local storage so projected nets persist transcript, revision, and warnings, with guards against stale revisions and manual edits.

Provider API routes enforce same-origin / experiment headers, empty bodies (no browser-controlled config), rate limits, and generic error responses without leaking keys.

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

@kostandinang
kostandinang requested a review from lunelson August 25, 2026 15:18
Comment thread apps/brunch-agent/src/elevenlabs-speech-engine.ts
Comment thread apps/brunch-agent/src/voice-experiment-diagnostics.ts
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 1f8fd3a. 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/deps Relates to third-party dependencies (area) 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.

3 participants