惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
月光博客
月光博客
MyScale Blog
MyScale Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
爱范儿
爱范儿
P
Proofpoint News Feed
人人都是产品经理
人人都是产品经理
Last Week in AI
Last Week in AI
罗磊的独立博客
G
Google Developers Blog
Y
Y Combinator Blog
博客园 - 【当耐特】
WordPress大学
WordPress大学
大猫的无限游戏
大猫的无限游戏
博客园 - 叶小钗
J
Java Code Geeks
酷 壳 – CoolShell
酷 壳 – CoolShell
V
Visual Studio Blog
美团技术团队
宝玉的分享
宝玉的分享
Jina AI
Jina AI
小众软件
小众软件
T
Tailwind CSS Blog
A
About on SuperTechFans

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix: suppress Google Meet realtime echo · openclaw/opencl...
steipete · 2026-05-04 · via Recent Commits to openclaw:main

@@ -100,6 +100,8 @@ export type GoogleMeetRealtimeEventEntry = RealtimeVoiceBridgeEvent & {

100100

};

101101102102

export const GOOGLE_MEET_AGENT_TRANSCRIPT_DEBOUNCE_MS = 900;

103+

export const GOOGLE_MEET_OUTPUT_ECHO_SUPPRESSION_TAIL_MS = 3_000;

104+

export const GOOGLE_MEET_TRANSCRIPT_ECHO_LOOKBACK_MS = 45_000;

103105104106

export function recordGoogleMeetRealtimeEvent(

105107

events: GoogleMeetRealtimeEventEntry[],

@@ -157,6 +159,80 @@ function readPcm16Stats(audio: Buffer): { rms: number; peak: number } {

157159

};

158160

}

159161162+

function normalizeTranscriptForEchoMatch(text: string): string[] {

163+

return text

164+

.toLowerCase()

165+

.replace(/[']/g, "")

166+

.replace(/[^a-z0-9]+/g, " ")

167+

.trim()

168+

.split(/\s+/)

169+

.filter((token) => token.length > 1);

170+

}

171+172+

function hasMeaningfulEchoOverlap(userTokens: string[], assistantTokens: string[]): boolean {

173+

if (userTokens.length < 4 || assistantTokens.length < 4) {

174+

return false;

175+

}

176+

const assistantTokenSet = new Set(assistantTokens);

177+

const overlap = userTokens.filter((token) => assistantTokenSet.has(token)).length;

178+

return overlap / userTokens.length >= 0.58;

179+

}

180+181+

export function isGoogleMeetLikelyAssistantEchoTranscript(params: {

182+

transcript: GoogleMeetRealtimeTranscriptEntry[];

183+

text: string;

184+

nowMs?: number;

185+

}): boolean {

186+

const userTokens = normalizeTranscriptForEchoMatch(params.text);

187+

if (userTokens.length < 4) {

188+

return false;

189+

}

190+

const nowMs = params.nowMs ?? Date.now();

191+

const recentAssistantText = params.transcript

192+

.filter((entry) => {

193+

if (entry.role !== "assistant") {

194+

return false;

195+

}

196+

const at = Date.parse(entry.at);

197+

return Number.isFinite(at) && nowMs - at <= GOOGLE_MEET_TRANSCRIPT_ECHO_LOOKBACK_MS;

198+

})

199+

.slice(-6)

200+

.map((entry) => entry.text)

201+

.join(" ");

202+

if (!recentAssistantText.trim()) {

203+

return false;

204+

}

205+

const userNormalized = userTokens.join(" ");

206+

const assistantTokens = normalizeTranscriptForEchoMatch(recentAssistantText);

207+

const assistantNormalized = assistantTokens.join(" ");

208+

return (

209+

(userNormalized.length >= 18 && assistantNormalized.includes(userNormalized)) ||

210+

(assistantNormalized.length >= 18 && userNormalized.includes(assistantNormalized)) ||

211+

hasMeaningfulEchoOverlap(userTokens, assistantTokens)

212+

);

213+

}

214+215+

export function extendGoogleMeetOutputEchoSuppression(params: {

216+

audio: Buffer;

217+

audioFormat: GoogleMeetConfig["chrome"]["audioFormat"];

218+

nowMs: number;

219+

lastOutputPlayableUntilMs: number;

220+

suppressInputUntilMs: number;

221+

}): { lastOutputPlayableUntilMs: number; suppressInputUntilMs: number; durationMs: number } {

222+

const bytesPerMs = params.audioFormat === "g711-ulaw-8khz" ? 8 : 48;

223+

const durationMs = Math.ceil(params.audio.byteLength / bytesPerMs);

224+

const playbackStartMs = Math.max(params.nowMs, params.lastOutputPlayableUntilMs);

225+

const playbackEndMs = playbackStartMs + durationMs;

226+

return {

227+

durationMs,

228+

lastOutputPlayableUntilMs: playbackEndMs,

229+

suppressInputUntilMs: Math.max(

230+

params.suppressInputUntilMs,

231+

playbackEndMs + GOOGLE_MEET_OUTPUT_ECHO_SUPPRESSION_TAIL_MS,

232+

),

233+

};

234+

}

235+160236

export function resolveGoogleMeetRealtimeAudioFormat(config: GoogleMeetConfig) {

161237

return config.chrome.audioFormat === "g711-ulaw-8khz"

162238

? REALTIME_VOICE_AUDIO_FORMAT_G711_ULAW_8KHZ

@@ -227,11 +303,15 @@ export async function startCommandRealtimeAudioBridge(params: {

227303

let agentConsultDebounceTimer: ReturnType<typeof setTimeout> | undefined;

228304229305

const suppressInputForOutput = (audio: Buffer) => {

230-

const bytesPerMs = params.config.chrome.audioFormat === "g711-ulaw-8khz" ? 8 : 48;

231-

const durationMs = Math.ceil(audio.byteLength / bytesPerMs);

232-

const until = Date.now() + durationMs + 900;

233-

suppressInputUntil = Math.max(suppressInputUntil, until);

234-

lastOutputPlayableUntilMs = Math.max(lastOutputPlayableUntilMs, until);

306+

const suppression = extendGoogleMeetOutputEchoSuppression({

307+

audio,

308+

audioFormat: params.config.chrome.audioFormat,

309+

nowMs: Date.now(),

310+

lastOutputPlayableUntilMs,

311+

suppressInputUntilMs: suppressInputUntil,

312+

});

313+

suppressInputUntil = suppression.suppressInputUntilMs;

314+

lastOutputPlayableUntilMs = suppression.lastOutputPlayableUntilMs;

235315

};

236316237317

const terminateProcess = (proc: BridgeProcess, signal: NodeJS.Signals = "SIGTERM") => {

@@ -521,6 +601,10 @@ export async function startCommandRealtimeAudioBridge(params: {

521601

recordGoogleMeetRealtimeTranscript(transcript, role, text);

522602

params.logger.info(`[google-meet] realtime ${role}: ${text}`);

523603

if (role === "user" && strategy === "agent") {

604+

if (isGoogleMeetLikelyAssistantEchoTranscript({ transcript, text })) {

605+

params.logger.info(`[google-meet] realtime ignored assistant echo transcript: ${text}`);

606+

return;

607+

}

524608

enqueueAgentConsultForUserTranscript(text);

525609

}

526610

}