diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index e532a4a6c6..3c22734adf 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -3174,6 +3174,10 @@ pub fn run() { request_id: request_id.clone(), meet_url: meet_url.clone(), display_name: "OpenHuman Dev".to_string(), + // Dev-auto launch has no real user identity — the + // wake gate will fail-closed (no wakes fire) which + // is the safe posture for an automated harness. + owner_display_name: String::new(), }; match meet_call::meet_call_open_window(app_handle.clone(), state, args) .await diff --git a/app/src-tauri/src/meet_audio/audio_bridge.js b/app/src-tauri/src/meet_audio/audio_bridge.js index 9d9cfdedd9..618463a8f2 100644 --- a/app/src-tauri/src/meet_audio/audio_bridge.js +++ b/app/src-tauri/src/meet_audio/audio_bridge.js @@ -45,9 +45,13 @@ function ensureContext() { if (ctx) { - console.log( - "[openhuman-audio-bridge] reuse AudioContext state=" + ctx.state - ); + // Resume if suspended (CEF suspends until user gesture; the + // meet_scanner's synthetic clicks count as gestures on some + // builds but not all). + if (ctx.state === "suspended") { + ctx.resume().catch(function () {}); + console.log("[openhuman-audio-bridge] resumed suspended AudioContext"); + } return ctx; } var requestedRate = SAMPLE_RATE; @@ -97,6 +101,29 @@ return out; } + // Track every scheduled AudioBufferSource so __openhumanFlushAudio + // can stop them on barge-in (user re-asks during a long bot reply). + // Without this list, only the queue tail past `nextStartTime` would + // be cancellable; anything already start()-ed plays to completion. + var activeSources = []; + + // Stop in-flight playback and reset the schedule cursor. Called by + // the Rust shell when the brain cancels outbound (new wake fires + // mid-reply). Returns the number of sources that were stopped, so + // the shell can log how much speech got cut. + window.__openhumanFlushAudio = function () { + var stopped = 0; + while (activeSources.length) { + var s = activeSources.pop(); + try { s.stop(); stopped++; } catch (_) {} + try { s.disconnect(); } catch (_) {} + } + if (ctx) { + nextStartTime = ctx.currentTime; + } + return stopped; + }; + // Public push API. Returns the duration in seconds the chunk added // to the queue, mostly for diagnostics; the shell ignores it. window.__openhumanFeedPcm = function (b64) { @@ -110,6 +137,13 @@ var src = ctx.createBufferSource(); src.buffer = buffer; src.connect(dest); + // Also pipe to the page's default audio output so the bot's + // TTS is audible on the host machine. This does NOT cause echo + // because Meet attributes the bot's own speech to "You" (which + // is filtered in core's note_caption) and the media-element + // mute below silences Meet's INCOMING call audio (other + // participants' voices bouncing back through speakers). + src.connect(ctx.destination); // Schedule strictly after the previous chunk so successive // 100 ms feeds line up gaplessly. If the queue has emptied // (caller fell behind), restart at currentTime so we don't try @@ -118,6 +152,11 @@ nextStartTime = ctx.currentTime; } src.start(nextStartTime); + activeSources.push(src); + src.onended = function () { + var idx = activeSources.indexOf(src); + if (idx !== -1) activeSources.splice(idx, 1); + }; nextStartTime += buffer.duration; // High-frequency log gated by a counter so we don't drown the // console at 10 Hz; emit ~1 in 50 frames (~5 s cadence at the @@ -231,5 +270,161 @@ }); }; } - console.log("[openhuman-audio-bridge] install complete"); + // Mute all inbound call audio on this page. The bot's CEF window is + // a headless participant — it reads captions for input and uses the + // virtual mic for output. Any audio that plays through the local + // speakers gets picked up by the user's mic (they're on the same + // machine) and causes an echo loop. We: + // + // 1. Mute all existing