Capture live meeting audio through the call-host bridge - #5374
Merged
Conversation
Adds "Capture system audio" as a second mode on the FaceTime call-host page (/voice/call-host?mode=capture). It reuses the same BlackHole 16ch device, PCM wire format, and whisper STT endpointing as the call bridge, but only transcribes — it never runs the LLM/tools pipeline and needs no BlackHole 2ch reply path. Stopping a capture writes the timestamped transcript to the daily journal under a "Meeting capture" heading and files it as a Brain inbox item with auto-classify off, so no AI provider is ever called until the user asks for a summary. - server/services/voice/captureSession.js: the capture session state machine (single-attach host, start/stop, timestamped transcript, journal + inbox write on stop), mirroring callSession.js. - server/sockets/voice.js: voice:capture:start/stop handlers; the existing voice:call:audio handler now routes to whichever host (call or capture) owns the socket, with each side mutually exclusive of the other since both want the same physical device. - client/src/pages/VoiceCallHost.jsx: mode toggle (deep-linkable via ?mode=), capture-specific device check (no output device required), and capture-specific start/stop/state wiring. - client/src/lib/callAudioBridge.js: describeDeviceProblem's outputLabel is now optional, since capture mode never plays a reply back. Also fixes a bug in callSession.js: endCall() passed the async getToday() as a Promise into appendJournal without awaiting it, so isIsoDate's typeof guard always rejected it and the FaceTime call transcript was silently never written to the journal. Closes #5311
… the fix The suite mocked brainJournal.getToday as a sync function while production has it async, so the assertion passed against the unawaited call this PR fixes — a pending Promise that appendJournal's isIsoDate guard silently rejected, leaving FaceTime call transcripts unwritten. Matching the real signature makes the case fail without the await and pass with it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/voice/call-host?mode=capture), reusing the same BlackHole 16ch device, PCM wire format, and whisper STT endpointing as the call bridge — but STT only, never the LLM/tools pipeline, and no BlackHole 2ch reply path required.server/services/voice/captureSession.js(single-attach host, start/stop, journal + inbox write) mirrors the existingcallSession.js.server/sockets/voice.jsaddsvoice:capture:start|stopand routes the existingvoice:call:audioevent to whichever host (call or capture) owns the socket; each mode is refused while the other is active on the same tab, since both want the same physical device.client/src/lib/callAudioBridge.js:describeDeviceProblem'soutputLabelis now optional, since capture mode never plays a reply back.callSession.js:endCall()passed the asyncgetToday()intoappendJournalas an unresolved Promise, soisIsoDate'stypeofguard always rejected it and the FaceTime call transcript was silently never written to the journal.Test plan
cd server && npx vitest run— full suite green (1729 files / 35489 tests).cd client && npx vitest run— green except one pre-existing, unrelated flake (GitTab.test.jsx, passes in isolation and on an untouched file).cd client && npm run lint && npm run build— clean.server/services/voice/captureSession.test.js(session state machine, journal/inbox writes, error resilience), additions toserver/sockets/voice.test.js(mutual exclusion, end-to-end audio → STT → transcript → journal/inbox routing, confirmsrunTurn/voice:llm:donenever fires for a capture), additions toclient/src/pages/VoiceCallHost.test.jsx(mode toggle, no-output-device requirement, live state, unmount cleanup) andclient/src/lib/callAudioBridge.test.js(optionaloutputLabel).server/lib/socketEventCatalog.generated.jsonfor the new/changed events.Closes #5311