Skip to content

feat(voice): experimental voice mode with an orb surface (desktop, OpenAI Realtime) - #512

Open
Leo310 wants to merge 22 commits into
mainfrom
feat/voice-mode
Open

Leo310 wants to merge 22 commits into
mainfrom
feat/voice-mode

Conversation

@Leo310

@Leo310 Leo310 commented Sep 19, 2026

Copy link
Copy Markdown
Member

What

Experimental, desktop-only voice mode: a full-duplex conversation with OpenAI's realtime speech model, which does the talking itself and hands anything note-related to the regular agent through a single function call (ask_second_brain, the chat-supervisor pattern). A delegated request runs as an ordinary chat turn on the open thread — persisted, streamed, reviewable on screen — and the spoken answer is read back. The user can keep talking while the agent works; the result is handed to the speech model once it is idle and the user is silent.

While voice is bound to a thread, the chat view swaps to an audio-reactive orb (breathes while listening, pulses with the model's voice, spins a ring while the agent works) with the last transcript lines, a stop button and a "show chat" toggle that reveals the message list under a compact orb strip. Settings: General → Voice (experimental). Composer gets a mic button; there is a "Toggle voice mode" command.

Design notes, all in src/voice/:

  • realtimeProtocol.ts holds every event name and the PCM16 codecs, so API drift is a one-file fix.
  • turnCoordinator.ts is a pure reducer that holds finished results until the model is idle and the user silent (server VAD auto-creates a response after speech_stopped, so a naive response.create collides).
  • supervisorBridge.ts serialises delegations and maps the turn's outcome to JSON the model reads back; ChatSession.sendMessageAndAwait is the new completion hook it relies on.
  • Capture runs in an AudioWorklet shipped as raw text through a blob URL (the ?worker&inline route yields a constructor, not a URL); playback tracks how much of an item was heard so barge-in can truncate the server's transcript.
  • Auth is the openai-insecure-api-key WebSocket subprotocol with the configured OpenAI provider's key (browser sockets cannot set headers; the key already lives on this machine). No new dependencies.

Known limitations of the prototype: headphones assumed (Chromium cannot echo-cancel WebAudio output, so on speakers the model can hear itself); starting voice mode unmounts the composer, so an unsent typed draft is lost; only delegated turns land in the .chat thread (chit-chat since the last delegation is prefixed to the next request as context); no reconnect.

No linked issue — scoped in-session with the maintainer.

How I tested it

bun run check, format, lint, test (1893 tests, including new suites for the protocol codecs, the delivery coordinator, the supervisor bridge, the WebSocket client with a fake socket, the awaitable chat turn, and the v13→v14 migration) and madge --circular with type imports skipped all pass. A one-shot dev build inlines the worklet (registerProcessor present in main.js).

Not yet tried live. A real run needs an OpenAI API key in the slot vault (S2B WT2) and a microphone; the live checklist is: enable in settings → open a chat → mic → say "hello" (voice-only reply) → ask about a note (filler phrase, orb ring, a delegated user message appears in the chat, answer spoken) → talk over the model → ask "are you done?" during a long delegation → stop. Event names were written against the current GA docs but only a live session proves them.

AI assistance: Claude Code (Fable 5.1) designed and wrote the whole change from Leo's brief after a research discussion on speech-to-speech models; Leo chose the backend, platform, UI shape and transcript policy; live verification in a vault is still pending.

Checklist

  • bun run check, bun run format, bun run lint, and bun run test pass locally
  • I tried the change in a real Obsidian vault (or explained above why that isn't applicable) — pending, see above
  • I read CONTRIBUTING.md, including the section on AI assistance
  • If this adds a provider, a bundled skill, a built-in tool, or changes manifest.json: I noted that the docs site needs updating — not applicable yet (experimental, undocumented on the site until it ships)

Leo310 and others added 2 commits September 19, 2026 16:50
…me layer

Groundwork for the experimental voice mode (OpenAI Realtime, chat-supervisor
pattern): a `voice` settings block (schema v14), `ChatSession.sendMessageAndAwait`
so a caller can observe a turn's terminal state and final text, and the
store-free modules — the wire protocol in one file, the delivery coordinator
that holds function results until the model is idle and the user silent, the
supervisor bridge that runs a delegated request as a normal chat turn, and the
WebSocket client.

Co-Authored-By: Claude <noreply@anthropic.com>
…enAI Realtime)

Full-duplex voice conversation with OpenAI's realtime speech model, which handles
the talking itself and hands anything note-related to the regular agent through
one function call (chat-supervisor pattern). A delegated request runs as an
ordinary chat turn on the open thread — persisted, streamed, reviewable — and
the answer is read back; the user can keep talking while it runs, and the result
is delivered once the model is idle and the user silent.

While voice is bound to a thread the chat view swaps to an audio-reactive orb
with a status line, the last transcript lines, stop, and a show-chat toggle that
reveals the message list beneath a compact orb strip. Microphone capture runs
in an AudioWorklet shipped as raw text through a blob URL; playback is scheduled
gaplessly and reports how much of an item was heard so barge-in can truncate.

Settings live under General → Voice (experimental); the composer gets a mic
button and there is a "Toggle voice mode" command. Desktop only, reuses the
OpenAI provider key, no new dependencies.

Co-Authored-By: Claude <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no new actionable failures remain, and all previous findings were resolved.

Summary

This PR adds an experimental desktop voice-conversation mode backed by OpenAI Realtime and integrates delegated requests with ordinary persisted chat turns. Changes since the previous review also redesign list_directory to provide compact, context-budgeted folder overviews and update the associated UI and bundled skill guidance.

  • Adds microphone capture, streamed PCM playback, barge-in handling, transcript coordination, and an audio-reactive voice surface.
  • Adds awaitable chat turns, serialized voice-to-agent delegation, progress narration, and thread/view lifecycle handling.
  • Adds persisted voice settings and migrations, desktop settings controls, and a composer/command entry point.
  • Reworks directory listings around recursive counts, explicit omission markers, model-aware output budgets, and matching tool-output rendering.
  • Advances the Dataview and explore-vault bundled skills while retaining prior shipped bodies for safe upgrades.

Diagram

sequenceDiagram
  participant U as User
  participant V as Voice session
  participant O as OpenAI Realtime
  participant C as Chat session
  participant A as Regular agent

  U->>V: Start voice conversation
  V->>O: Open realtime socket and stream microphone audio
  O-->>V: Speech audio and transcript
  O->>V: ask_second_brain request
  V->>C: sendMessageAndAwait(request)
  C->>A: Run ordinary persisted chat turn
  A-->>C: Stream progress and final answer
  C-->>V: Settled turn result
  V->>O: Function result when model is idle and user is silent
  O-->>V: Spoken answer audio
  V-->>U: Playback and orb/transcript updates
Loading

Reviews (18) · Last reviewed commit: "Merge remote-tracking branch 'origin/mai..."

Comment thread src/views/chat/Chat.svelte
Comment thread src/stores/chatStore.svelte.ts
Comment thread src/voice/audioPlayback.ts Outdated
…runcate the audible item

Three review findings. The voice session is global and owns the microphone, so
the chat view now stops it when it unmounts or moves to another thread instead
of leaving capture running behind a surface that is no longer on screen. A
delegation that lands while a typed reply is running is refused before the pair
is appended, so no phantom turn lingers in the list. Playback tracks timing per
item and barge-in reports the item audible at that moment (clamped, whole
milliseconds), not whichever item last received audio.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread src/views/chat/Chat.svelte Outdated
Leo310 and others added 4 commits September 19, 2026 17:17
Views now register the thread they show with the voice session, which stops
only when the last view showing its bound thread detaches — instead of any one
view's cleanup ending it. Covered by voiceSessionViews.test.ts.

Co-Authored-By: Claude <noreply@anthropic.com>
The voice group belongs with the agent and chat settings rather than General.
In the composer the voice button now sits between the context ring and Send —
it is the other way to start a turn — and uses Lucide's audio-lines waveform
instead of a microphone, which reads as "dictate into the box" rather than a
live spoken exchange. Command renamed to "Toggle voice conversation".

Co-Authored-By: Claude <noreply@anthropic.com>
…box is empty

With nothing to send — no text, no attachments, not editing — the Send button
becomes the waveform that starts a voice conversation, the pattern ChatGPT's
composer established. Typing turns it back into the arrow; Enter on an empty
box still does nothing; Stop still wins while a reply streams. Replaces the
separate voice button.

Co-Authored-By: Claude <noreply@anthropic.com>
The orb now comes in blob (morphing outline, default), aurora (drifting colour
clouds), spectrum (radial equaliser fed by the analyser's frequency bins),
ripple (sonar rings) and pulse (the original disc). The painted looks live in
orbRenderers.ts as pure frame painters; the CSS looks key off data-status and
the level custom property as before. Colours come from the theme accent, with
colour-mixed companions resolved through a probe element for the canvas.
Stored as voice.orbStyle (schema v15 fills it in for existing blocks).

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread src/components/chat/VoiceOrb.svelte
Leo310 and others added 2 commits September 19, 2026 17:57
Toggling the compact orb changed its displayed size at once but its backing
size only on the once-per-60-frame colour re-read, so it could paint oversized
or clipped for about a second.

Co-Authored-By: Claude <noreply@anthropic.com>
A chat renames itself after its first turn. The registry re-keys the session
and the view follows the vault rename, but the voice session kept the old path
and — seeing no view on it any more — stopped, dropping the user back into the
chat mid-answer. It now follows vault renames itself, decides view detachment
after the current flush so the view's same-tick re-attach counts, and resolves
the thread path when a queued delegation starts rather than when it was queued.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread src/voice/voiceSession.svelte.ts
Leo310 and others added 2 commits September 19, 2026 19:34
A delegated turn used to be silence until the answer. Tool starts inside the
running turn (with the model's own lead-in sentence when it wrote one, else a
readable tool name) now flow from ChatSession through the bridge to the voice
session, which asks the speech model for a one-sentence out-of-band update —
conversation: none, so it never becomes part of the pending function call's
state and its audio is never a truncatable item. Progress lines go through the
same delivery gate as results: never over the user, never over a response, a
ready answer always wins, only the newest line survives, at most one every
four seconds.

Co-Authored-By: Claude <noreply@anthropic.com>
…efore the window opens

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread src/voice/voiceSession.svelte.ts
Leo310 and others added 4 commits September 19, 2026 19:39
…auto-title

A rename from the file menu (or a drag) went through renameChatFile without
touching the session registry, so the live session stayed under its old path
while the view — and now voice mode — resolved the new one and found nothing.
The registry now follows vault renames of .chat files and updates the
session's id; the auto-title path's own re-key stays idempotent with it.

Co-Authored-By: Claude <noreply@anthropic.com>
A progress line held for the rate limit could fire after the delegation it
described had already finished, and then be spoken at the start of the next
request. The hold is cancelled when the turn settles, and a held line that
fires with nothing pending is discarded.

Co-Authored-By: Claude <noreply@anthropic.com>
… the default

The morphing outline becomes a clip; three blurred radial clouds in the accent
and its two colour-mixed companions orbit inside it at different speeds, drift
faster and brighter with the voice, and the glow follows the outline rather
than a circle. Blob and aurora stay available for comparison.

Co-Authored-By: Claude <noreply@anthropic.com>
Only the first tool start of a step carried the model's lead-in; every other
one fell back to a generic "running <tool>", which the speech model kept
rephrasing into the same line. Progress lines are now built from the tool's
input when there is no lead-in (the search terms, the note being read, the
page host), and stay silent when there is nothing specific to say. The model
is told what it already said so it varies its wording, duplicates are dropped,
the gap is five seconds, and each request gets at most three lines.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread src/voice/voiceSession.svelte.ts Outdated
Comment thread src/voice/progressNarration.ts
An out-of-band response still sees the whole conversation unless it is given
explicit input, so the speech model kept paraphrasing the user's request
("looking for your tasks this week") instead of the step at hand, and said the
same sentence every time. Narration responses now carry their own two-line
context — the user's last words for language and the progress update — plus
the lines already spoken, and nothing else.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread src/voice/voiceSession.svelte.ts Outdated
…ead-ins as they are

A narration line with no context is structurally the same every time. The
response now gets the context that makes a line good — the user's request,
every step so far, the lines already spoken — without the full conversation,
whose closing question kept pulling the model into re-answering it. A lead-in
sentence the agent wrote itself is spoken close to its meaning (it already
carries the reasoning from step to step); a terse tool step is turned into a
natural sentence with its detail. Openings must vary; repeats are refused.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread src/voice/voiceSession.svelte.ts Outdated
Leo310 and others added 4 commits September 19, 2026 20:20
The corner radii drift between 46% and 54% over nine seconds, so the disc
never quite settles into a circle while keeping its flat, calm look. Nebula
keeps the pronounced blob outline for anyone who wants it.

Co-Authored-By: Claude <noreply@anthropic.com>
The corner-radius lean is now a custom property the keyframes read: 3% at rest,
up to about 13% with the audio level, so the disc leans with the voice and
settles when listening.

Co-Authored-By: Claude <noreply@anthropic.com>
The request, user words, step history, spoken lines and budget were kept on the
session, so a second call queued behind the first was narrated in the wrong
request's terms, a later utterance could change the language mid-request, and
the three-line budget never reset (the check ran after the call was already
pending). Each call now carries its own narration state from function call to
delivered output; a held line is tied to its call and dropped when it settles.
Also: the properties tool's input field is note_name, not path.

Co-Authored-By: Claude <noreply@anthropic.com>
Rest lean 5%, up to about 20% with the voice, six-second cycle.

Co-Authored-By: Claude <noreply@anthropic.com>
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