Status: SCOPE / PLAN ONLY — read-only investigation, no code written. Review with the owner before building.
Date: 2026-06-30
Goal (owner intent): Talk to the social bots by voice (and eventually video) directly through the Oracle-OS dashboard — a new item in the left icon rail that opens an in-dashboard voice/video session, as an *alternative path to Discord*. Video would be usable here (unlike Discord). Owner pictures "a separate similar file for the social bots" for this Oracle-OS voice path.
---
Voice-first MVP is very achievable and lower-risk than it looks, because the hard part — the live conversational brain — is already built and is not Discord-specific. Leo's GeminiLiveBridge is a clean WebSocket client to Gemini Live native-audio; all the Discord coupling lives *outside* it in leo.mjs. We reuse the bridge unchanged and write two small adapters (browser-mic-in, browser-speaker-out).
The one genuinely new piece of infrastructure is a browser↔server realtime transport, because today the dashboard has zero streaming plumbing — no WebSocket server, no getUserMedia, no audio elements. It's a pure HTTP-polling REST app. That's the main build cost, and it's modest (the ws package is already a dependency).
Video is a real extension, not a toggle. The bridge does not send image/video frames to Gemini today (its only "image/video" code is *outbound* map tools). Adding vision means a new bridge method plus a Gemini-Live model that accepts video input plus camera-gating work. Worth planning, but clearly Phase 2+.
Recommended transport: a WebSocket audio bridge, NOT WebRTC. Rationale in §5.
---
Traced through bots/leo.mjs (6,735 lines) and shared/gemini-live-bridge.mjs (2,493 lines).
┌─────────────── leo.mjs (DISCORD-SPECIFIC) ───────────────┐
Discord VC ──► receiver.subscribe(userId) → prism.opus.Decoder │
(user mic) (48kHz stereo Opus) (frameSize 960, 2ch, 48k) │
│ │
▼ decoded 48k stereo PCM │
bridge.sendAudio(chunk) ───────────────────────────────────►│
└──────────────────────────────────────────────────────────┘
│
┌────────────────── gemini-live-bridge.mjs (REUSABLE / Discord-free) ─────┐
│ sendAudio(pcm): _downsample48to16() → 16k mono → WS realtimeInput.audio │
│ { mimeType:"audio/pcm;rate=16000", data:<base64> } ──► Gemini Live WS │
│ │
│ Gemini streams back: serverContent.modelTurn.parts[].inlineData │
│ (audio/pcm ~24kHz) ──► this.onAudioChunk(base64, mimeType) │
│ Also: onTranscript, onInputTranscript, onTurnComplete, onToolCall │
└──────────────────────────────────────────────────────────────────────┬─┘
│
┌─────────────── leo.mjs (DISCORD-SPECIFIC) ───────────────┤
Discord VC ◄── audioPlayer / paced feed ◄── decodeAudioChunk() │
(Leo speaks) createAudioResource(PassThrough, StreamType.Raw) │
leoPacedFeed() upsamples 24k→48k stereo, 20ms frames │
└──────────────────────────────────────────────────────────┘
Key files / anchors:
leo.mjs ~L5125 / L6136 — receiver.subscribe(...), new prism.opus.Decoder({frameSize:960,channels:2,rate:48000}).leo.mjs ~L5205/L5395 — liveBridge.sendAudio(chunk).gemini-live-bridge.mjs — class GeminiLiveBridge (L641), sendAudio() (L1359, downsample 48→16k mono), inbound audio handled at L1234 → onAudioChunk.leo.mjs — bridge.onAudioChunk handler (L3705), leoPacedFeed() (L636), createAudioPlayer/createAudioResource.shared/gemini-live-voice.mjs — this is the model for the "separate similar file" the owner described. It already reuses the bridge for Gemini/Claudey/Groq/X, but its output sink is @discordjs/voice.Discord-specific vs reusable — the important split:
gemini-live-bridge.mjs | ✅ 100% reusable — no Discord imports in the audio path |leo.mjs receiver | ❌ Discord-only — replace with browser mic |@discordjs/voice player) | leo.mjs + gemini-live-voice.mjs | ❌ Discord-only — replace with browser speaker |The takeaway: the bridge already is the "audio-source-agnostic" core. It takes a PCM buffer in and hands a PCM buffer out. Discord is just one pair of adapters bolted onto that seam. Oracle-OS becomes a *second* pair of adapters.
---
Traced command-center-server.mjs (:3001, 4,258 lines) and oracle.html (6,519 lines).
Finding: there is none. The dashboard is a pure HTTP-polling REST app.
command-center-server.mjs is a plain Node http server (import http), serving static oracle.html + JSON /api/* endpoints. No WebSocketServer, no socket.io, no SSE (text/event-stream) anywhere. It talks to bots indirectly — writes .env + a restart-queue, and HTTP-proxies to the lattice engine on ENGINE_PORT (3334). It never holds a live connection to a running bot.oracle.html has no getUserMedia, no mediaDevices, no WebSocket, no RTCPeerConnection, no MediaRecorder, no <audio>/<video> elements at all. It fetches /api/* via fetch() on intervals.ws@^8.20.0 is already a dependency (the bridge uses it as a Gemini client). We can stand up a WebSocketServer with zero new installs.So the browser realtime transport is net-new, but the library is present and the pattern is well-understood.
---
The rail is trivially extensible. In oracle.html:
<!-- icon rail -->, <div class="rail">). Each item is a <div class="rail-btn" data-view="X" onclick="setView('X')"> with an inline SVG + <span class="rail-tip">. Existing views: home, transcripts, topology, nervous (=Kaiverse), learning, config.<div class="view-pane" id="view-X"> (home L1966, nervous L1978, etc.).setView(v) (L4418) toggles .rail-btn.active and shows the one matching pane. It has a hardcoded list ['home','transcripts','topology','nervous','learning','config'] and a VIEW_TITLES map — both need 'voice' added.To add a Voice/Video panel: one new .rail-btn (icon + tip + onclick="setView('voice')"), one new <div class="view-pane" id="view-voice">, add 'voice' to the VIEW_TITLES map and the show/hide array in setView. Surgical, ~15 lines, no risk to existing views. This is the *easiest* part of the whole feature.
---
Two credible transports for browser↔server audio:
Option A — WebSocket carrying raw PCM (RECOMMENDED).
Browser captures mic via getUserMedia → an AudioWorklet (or MediaRecorder) produces PCM16 → send binary frames over a WebSocket to the server → server hands each frame to bridge.sendAudio(). Gemini's reply arrives on bridge.onAudioChunk → server relays base64 PCM frames back over the same WebSocket → browser plays them via Web Audio API (AudioContext, scheduled buffer sources).
ws already installed; maps 1:1 onto the bridge's existing PCM-in/PCM-out contract; no media-server, no ICE/STUN/TURN, no SDP negotiation; trivially localhost-friendly (dashboard is http://localhost:3001); easy to debug (it's just framed bytes). The bridge *already* downsamples input and the current playback code *already* resamples 24k→48k, so the resampling math exists to copy.leoPacedFeed is a working reference to port), and we own audio worklet plumbing.Option B — WebRTC.
Browser RTCPeerConnection streams Opus to the server; server decodes to PCM for the bridge and encodes bridge output back to Opus.
werift/mediasoup/wrtc), SDP + ICE negotiation, and server-side Opus transcode. Much larger surface for a single-user desktop dashboard. Overkill.Recommendation: Option A. It matches the existing code's seams almost exactly and needs no new heavy dependencies. Echo cancellation is the one thing WebRTC gives for free — but for a headphones-on desktop session that's optional, and the browser's getUserMedia({audio:{echoCancellation:true}}) constraint covers most of it client-side.
Where does the bot process live? Two sub-options:
command-center-server.mjs process (or a small sidecar it spawns) hosts the WebSocketServer and owns the GeminiLiveBridge session directly — i.e. Oracle-OS voice does not require the Discord Leo/social bot processes to be running. This makes it a true independent path (matches "alternative to Discord") and avoids cross-process audio piping. Needs a Gemini Live API key in the dashboard server's env (the same native-audio key the bots use).The "separate similar file": create shared/oracle-live-voice.mjs, modeled on gemini-live-voice.mjs, but with the audio sink/source swapped from @discordjs/voice to the WebSocket relay. Same bridge, same personas/resolveGeminiVoice, same tools — just a different mouth and ears. This is exactly the structure the owner described.
---
Sending the browser camera to a bot is a real extension:
1. Bridge does not send vision today. Its only image/video code is *outbound* map tools (satellite/street/flyover posted to Discord). Gemini Live *does* accept video input (inline JPEG frames in realtimeInput), but there is no sendVideo/sendImage method — that's new code on the bridge (small, additive).
2. Model support. Must confirm the selected native-audio Live model accepts image/video input (some native-audio preview models are audio-only). May need a different Live model or a periodic-frame approach (send 1 JPEG/second rather than true video).
3. Browser side: getUserMedia({video:true}), grab frames from a <video>/canvas, JPEG-encode, send over the same WebSocket.
4. Camera gating (owner's explicit tie-in): the camera must turn on only for an active Oracle-OS video session and turn off when it ends. This is a browser-permission + explicit-state concern — a clear "session active" flag drives getUserMedia/track .stop(). This is net-new UI/state and should be built alongside the video panel, not retrofitted.
Video is clean to layer on *after* the voice transport exists, because it rides the same WebSocket.
---
Effort is rough dev-time; risk is likelihood of breaking existing behavior or hitting unknowns.
bridge.sendAudio → onAudioChunk → browser speaker end-to-end with a hardcoded persona. Throwaway HTML page, no rail. | New: WS server + tiny client. Reuse: bridge as-is. | S–M | Med (audio worklet + jitter buffer is the unknown) | Gemini Live key in server env; ws (already present) |WebSocketServer in/around command-center-server.mjs; shared/oracle-live-voice.mjs (bridge + WS sink/source); left-rail "Voice" button + view-voice pane with connect/disconnect, mic level, live transcript, pick-a-bot. Audio only. | New: transport + voice panel + oracle-live-voice.mjs. Reuse: bridge, personas, tools, transcripts. | M–L | Med | Phase 0; server restart via Start-Dashboard.ps1; browser mic permission |_leoSpeakingUntil echo-guard + manual-VAD activityStart/End), reconnect, key-rotation surfaced, per-bot voice selection, transcript persistence. | Reuse patterns from leo.mjs. | M | Low–Med | Phase 1 |sendVideo/sendImage on the bridge (additive); browser camera capture + JPEG frames over the same WS; camera gating (on only during active session). | New: bridge vision method + camera UI/state. | L | Med–High (model must accept video input; verify) | Phase 1; a vision-capable Live model |Dependencies summary (what it needs to run):
http:// is a secure context for getUserMedia in Chrome, so no HTTPS requirement for local use.command-center-server.mjs edits → .\Start-Dashboard.ps1 (restarts :3001 only, leaves engine + bots alone); oracle.html edits → hard-refresh, no restart.---
Voice-first MVP (Phases 0–1, optionally 1.5): Click the new rail icon → panel opens → "Connect" → pick a bot → talk, hear it reply, see the live transcript, "Disconnect." Audio only. This is the achievable, high-value first target and reuses ~everything that makes conversation *work*. The only real unknown is browser-side audio worklet + jitter buffering, de-risked by Phase 0.
Full voice+video (Phases 2+): adds camera→bot vision, camera-gating, and possibly a different Live model. Bigger, with a genuine unknown (model video-input support) — plan it, but gate it behind a working MVP.
---
1. Session ownership: standalone Oracle-OS session (A1, simpler, no Discord memory) vs shared with the running bot (A2/Phase 3, faithful but much more work)? Recommend starting A1.
2. Which bots first in the dashboard voice panel — Leo, or the social four (Gemini/Claudey/Groq/X), or all?
3. Video model: OK to spend a spike confirming a native-audio + video-input Live model exists / picking a frame-rate strategy before committing to Phase 2?
4. Scope confirmation: this is all new surface area (transport + a new shared module + new panel) — none of it touches kaiverse.js or the flight controls, and the oracle.html change is additive. Confirm we're OK opening this track alongside the KAIVERSE visual work.
---
gemini-live-bridge.mjs) — WS to Gemini, VAD, tools, transcripts, key rotation. Personas + voice selection. The 48↔16k / 24→48k resampling math. The gemini-live-voice.mjs structure as a template.ws already installed), a shared/oracle-live-voice.mjs with browser sink/source, and a left-rail Voice panel (trivial).---
---
Added: 2026-06-30 · Status: SPEC + PLAN ONLY, no code written. This section captures the owner's fuller vision described after the feasibility report above, so nothing is lost. It supersedes/expands §7 where they conflict, and it stays reconciled with the feasibility findings (WebSocket-PCM transport, Discord-free gemini-live-bridge, [email protected] already a dep, new shared/oracle-live-voice.mjs, ~15-line left-rail add).
An in-dashboard, Discord-style VOICE + VIDEO call system on the Oracle-OS side — NOT Discord. You talk and (eventually) video-call the bots (and/or humans) from inside the Oracle dashboard itself, with a real call UI, participant tiles, active-speaker highlighting, picture-in-picture, device selection, and an invite/accept join flow. All signalling and media are directed to Oracle OS, not routed through Discord.
A. Entry point — a "VIDEO" tab on voice channels.
On a voice channel (e.g. *Leo Voice*, *ai-social-chat Voice*, etc.), alongside the existing per-channel tabs CHAT / THREADS / METRICS / SETTINGS, add a new "VIDEO" tab. Despite the name, it means voice AND video. Clicking it opens the call.
B. Call view.
Opening the call turns the main window into a Discord-like call view. You can also chat inside the call (text chat remains available in the call view).
C. Invite / add participants.
You can add/invite whoever you want — bots and/or humans. Invitees receive a join REQUEST and must ACCEPT to join. All of this is directed to Oracle OS, not Discord.
D. Participant tiles.
Each participant gets a tile showing, per participant:
E. Picture-in-Picture (PiP) while chatting.
While in a call, if you click the CHAT tab, the video feed pops out into a small floating window pinned to the top/side (Twitch-stream style) so you can keep seeing who's talking while you chat.
F. Device settings.
A settings area to pick which CAMERA and MIC to use. Default selection = "Device Default." The user can change to any available device (enumerated from the browser).
G. RF device toggle.
If an RF camera/device is present, it gets an ENABLE/DISABLE toggle inside the Camera options area. This ties into camera-privacy gating — camera/RF are only on when a session is active AND the user enables it.
H. Behavior rule — bots don't reply to text during voice.
While you're in the VOICE call, the bots do NOT reply to your TEXT messages (they're on voice) — but they CAN SEE the text (it's in the chat). This is possible precisely because this is Oracle-OS-side, not Discord.
Nothing in v2 changes the recommended architecture; it mostly adds UI surface and a join-flow:
oracle.html (additive, hard-refresh-testable — no restart).gemini-live-bridge.mjs via the new shared/oracle-live-voice.mjs (needs command-center-server.mjs + a WebSocketServer; [email protected] already present; applied via .\Start-Dashboard.ps1).sendVideo/sendImage bridge method + a vision-capable Live model — same caveat as §6.navigator.mediaDevices.enumerateDevices() + getUserMedia({ audio/video: { deviceId } }), default deviceId unset ("Device Default")..stop()-ed when the session ends or the toggle is off.Reconciled with the feasibility plan; refresh-testable frontend first, live media second, video third, multi-party last.
Build the entire interface and device-selection layer with no live media wired yet — pure UI + enumerateDevices.
enumerateDevices(), default = "Device Default," user-changeable to any enumerated device.oracle.html edits → hard-refresh, no restart.oracle.html; must not disturb existing per-channel tabs or setView. Enumerating devices without permission returns unlabeled device IDs in some browsers — labels populate after first getUserMedia grant (cosmetic, note in UI).Make the call actually talk to a bot.
gemini-live-bridge (via shared/oracle-live-voice.mjs) → browser speaker.command-center-server.mjs + new shared/oracle-live-voice.mjs → .\Start-Dashboard.ps1 (restarts :3001 only); oracle.html → hard-refresh.Add real video on the transport built in Phase 2.
getUserMedia({video}), render local tile; if the Live model supports vision, send JPEG frames over the same WebSocket (additive sendVideo/sendImage on the bridge)..\Start-Dashboard.ps1; oracle.html → hard-refresh.Turn it into a real multi-party call.
command-center-server.mjs (signalling) → .\Start-Dashboard.ps1; oracle.html → hard-refresh.oracle.html (tabs, call view, tiles, PiP, device pickers, RF toggle, all Phase-1 UI) | Hard-refresh the dashboard — no restart |command-center-server.mjs (WebSocketServer, signalling, text-suppression gating) | .\Start-Dashboard.ps1 — restarts :3001 only, leaves engine + bots + pipeline alone |shared/oracle-live-voice.mjs | picked up on the same .\Start-Dashboard.ps1 |gemini-live-bridge.mjs (additive sendVideo/sendImage) | .\Start-Dashboard.ps1 (server owns the session in the A1 design) |The original plan already covers the voice transport, oracle-live-voice.mjs, and video-send. v2 adds: the tabbed VIDEO entry + full Discord-style call view, participant tiles with avatar-fallback + active-speaker glow, PiP pop-out, device pickers with "Device Default," the RF enable/disable toggle, the text-suppression-during-voice rule, and the invite/accept multi-party join flow (incl. humans). None of this touches kaiverse.js or the flight controls; the oracle.html work is additive. Human-participant media is the one item with materially higher risk and is intentionally last.
*Documentation only — no code changed, no version bumped.*