mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 04:56:25 +00:00
* refactor: share talk event metric extraction * refactor: reuse shared coercion helpers * refactor: reuse shared primitive guards * refactor: reuse shared record guard * refactor: reuse shared primitive helpers * refactor: reuse shared string guards * refactor: reuse shared non-empty string guard * refactor: share plugin primitive coercion helpers * refactor: reuse plugin coercion helpers * refactor: reuse plugin coercion helpers in more plugins * refactor: reuse channel coercion helpers * refactor: reuse monitor coercion helpers * refactor: reuse provider coercion helpers * refactor: reuse core coercion helpers * refactor: reuse runtime coercion helpers * refactor: reuse helper coercion in codex paths * refactor: reuse helper coercion in runtime paths * refactor: reuse codex app-server coercion helpers * refactor: reuse codex record helpers * refactor: reuse migration and qa record helpers * refactor: reuse feishu and core helper guards * refactor: reuse browser and policy coercion helpers * refactor: reuse memory wiki record helper * refactor: share boolean coercion helpers * refactor: reuse finite number coercion * refactor: reuse trimmed string list helpers * refactor: reuse string list normalization * refactor: reuse remaining string list helpers * refactor: reuse string entry normalizer * refactor: share sorted string helpers * refactor: share string list normalization * test: preserve command registry browser imports * refactor: reuse trimmed list helpers * refactor: reuse string dedupe helpers * refactor: reuse local dedupe helpers * refactor: reuse more string dedupe helpers * refactor: reuse command string dedupe helpers * refactor: dedupe memory path lists with helper * refactor: expose string dedupe helpers to plugins * refactor: reuse core string dedupe helpers * refactor: reuse shared unique value helpers * refactor: reuse unique helpers in agent utilities * refactor: reuse unique helpers in config plumbing * refactor: reuse unique helpers in extensions * refactor: reuse unique helpers in core utilities * refactor: reuse unique helpers in qa plugins * refactor: reuse unique helpers in memory plugins * refactor: reuse unique helpers in channel plugins * refactor: reuse unique helpers in core tails * refactor: reuse unique helper in comfy workflow * refactor: reuse unique helpers in test utilities * refactor: expose unique value helper to plugins * refactor: reuse unique helpers for numeric lists * refactor: replace index dedupe filters * refactor: reuse string entry normalization * refactor: reuse string normalization in plugin helpers * refactor: reuse string normalization in extension helpers * refactor: reuse string normalization in channel parsers * refactor: reuse string normalization in memory search * refactor: reuse string normalization in provider parsers * refactor: reuse string normalization in qa helpers * refactor: reuse string normalization in infra parsers * refactor: reuse string normalization in messaging parsers * refactor: reuse string normalization in core parsers * refactor: reuse string normalization in extension parsers * refactor: reuse string normalization in remaining parsers * refactor: reuse string normalization in final parser spots * refactor: reuse string normalization in qa media helpers * refactor: reuse normalization in provider and media lists * refactor: reuse normalization for remaining set filters * refactor: reuse normalization in policy allowlists * refactor: reuse normalization in session and owner lists * refactor: centralize primitive string lists * refactor: reuse lowercase entry helpers * refactor: reuse sorted string helpers * refactor: reuse unique trimmed helpers * refactor: reuse string normalization helpers * refactor: reuse catalog string helpers * refactor: reuse remaining string helpers * refactor: simplify remaining list normalization * refactor: reuse codex auth order normalization * chore: refresh plugin sdk api baseline * fix: make shared string sorting deterministic * chore: refresh plugin sdk api baseline * fix: align host env security ordering
157 lines
4.9 KiB
TypeScript
157 lines
4.9 KiB
TypeScript
import { uniqueStrings } from "../shared/string-normalization.js";
|
||
import type { RealtimeVoiceBridgeEvent, RealtimeVoiceRole } from "./provider-types.js";
|
||
|
||
export type RealtimeVoiceTranscriptEntry = {
|
||
at: string;
|
||
role: RealtimeVoiceRole;
|
||
text: string;
|
||
};
|
||
|
||
export type RealtimeVoiceTranscriptHealth = {
|
||
realtimeTranscriptLines: number;
|
||
lastRealtimeTranscriptAt?: string;
|
||
lastRealtimeTranscriptRole?: RealtimeVoiceRole;
|
||
lastRealtimeTranscriptText?: string;
|
||
recentRealtimeTranscript: RealtimeVoiceTranscriptEntry[];
|
||
};
|
||
|
||
export type RealtimeVoiceBridgeEventLogEntry = RealtimeVoiceBridgeEvent & {
|
||
at: string;
|
||
};
|
||
|
||
export type RealtimeVoiceBridgeEventHealth = {
|
||
lastRealtimeEventAt?: string;
|
||
lastRealtimeEventType?: string;
|
||
lastRealtimeEventDetail?: string;
|
||
recentRealtimeEvents: RealtimeVoiceBridgeEventLogEntry[];
|
||
};
|
||
|
||
export function recordRealtimeVoiceTranscript(
|
||
transcript: RealtimeVoiceTranscriptEntry[],
|
||
role: RealtimeVoiceRole,
|
||
text: string,
|
||
maxEntries = 40,
|
||
): RealtimeVoiceTranscriptEntry {
|
||
const entry = { at: new Date().toISOString(), role, text };
|
||
transcript.push(entry);
|
||
if (transcript.length > maxEntries) {
|
||
transcript.splice(0, transcript.length - maxEntries);
|
||
}
|
||
return entry;
|
||
}
|
||
|
||
export function getRealtimeVoiceTranscriptHealth(
|
||
transcript: RealtimeVoiceTranscriptEntry[],
|
||
): RealtimeVoiceTranscriptHealth {
|
||
const last = transcript.at(-1);
|
||
return {
|
||
realtimeTranscriptLines: transcript.length,
|
||
lastRealtimeTranscriptAt: last?.at,
|
||
lastRealtimeTranscriptRole: last?.role,
|
||
lastRealtimeTranscriptText: last?.text,
|
||
recentRealtimeTranscript: transcript.slice(-5),
|
||
};
|
||
}
|
||
|
||
export function recordRealtimeVoiceBridgeEvent(
|
||
events: RealtimeVoiceBridgeEventLogEntry[],
|
||
event: RealtimeVoiceBridgeEvent,
|
||
maxEntries = 40,
|
||
): void {
|
||
if (event.direction === "client" && event.type === "input_audio_buffer.append") {
|
||
return;
|
||
}
|
||
events.push({ at: new Date().toISOString(), ...event });
|
||
if (events.length > maxEntries) {
|
||
events.splice(0, events.length - maxEntries);
|
||
}
|
||
}
|
||
|
||
export function getRealtimeVoiceBridgeEventHealth(
|
||
events: RealtimeVoiceBridgeEventLogEntry[],
|
||
): RealtimeVoiceBridgeEventHealth {
|
||
const last = events.at(-1);
|
||
return {
|
||
lastRealtimeEventAt: last?.at,
|
||
lastRealtimeEventType: last ? `${last.direction}:${last.type}` : undefined,
|
||
lastRealtimeEventDetail: last?.detail,
|
||
recentRealtimeEvents: events.slice(-10),
|
||
};
|
||
}
|
||
|
||
function normalizeTranscriptForEchoMatch(text: string): string[] {
|
||
return text
|
||
.toLowerCase()
|
||
.replace(/['’]/g, "")
|
||
.replace(/[^a-z0-9]+/g, " ")
|
||
.trim()
|
||
.split(/\s+/)
|
||
.filter((token) => token.length > 1);
|
||
}
|
||
|
||
function hasMeaningfulEchoOverlap(userTokens: string[], assistantTokens: string[]): boolean {
|
||
if (userTokens.length < 4 || assistantTokens.length < 4) {
|
||
return false;
|
||
}
|
||
const uniqueUserTokens = uniqueStrings(userTokens);
|
||
if (uniqueUserTokens.length < 4) {
|
||
return false;
|
||
}
|
||
const assistantTokenSet = new Set(assistantTokens);
|
||
const overlap = uniqueUserTokens.filter((token) => assistantTokenSet.has(token)).length;
|
||
return overlap / uniqueUserTokens.length >= 0.58;
|
||
}
|
||
|
||
export function isLikelyRealtimeVoiceAssistantEchoTranscript(params: {
|
||
transcript: RealtimeVoiceTranscriptEntry[];
|
||
text: string;
|
||
lookbackMs: number;
|
||
nowMs?: number;
|
||
}): boolean {
|
||
const userTokens = normalizeTranscriptForEchoMatch(params.text);
|
||
if (userTokens.length < 4) {
|
||
return false;
|
||
}
|
||
const nowMs = params.nowMs ?? Date.now();
|
||
const recentAssistantText = params.transcript
|
||
.filter((entry) => {
|
||
if (entry.role !== "assistant") {
|
||
return false;
|
||
}
|
||
const at = Date.parse(entry.at);
|
||
return Number.isFinite(at) && nowMs - at <= params.lookbackMs;
|
||
})
|
||
.slice(-6)
|
||
.map((entry) => entry.text)
|
||
.join(" ");
|
||
if (!recentAssistantText.trim()) {
|
||
return false;
|
||
}
|
||
const userNormalized = userTokens.join(" ");
|
||
const assistantTokens = normalizeTranscriptForEchoMatch(recentAssistantText);
|
||
const assistantNormalized = assistantTokens.join(" ");
|
||
return (
|
||
(userNormalized.length >= 18 && assistantNormalized.includes(userNormalized)) ||
|
||
(assistantNormalized.length >= 18 && userNormalized.includes(assistantNormalized)) ||
|
||
hasMeaningfulEchoOverlap(userTokens, assistantTokens)
|
||
);
|
||
}
|
||
|
||
export function extendRealtimeVoiceOutputEchoSuppression(params: {
|
||
audio: Buffer;
|
||
bytesPerMs: number;
|
||
tailMs: number;
|
||
nowMs: number;
|
||
lastOutputPlayableUntilMs: number;
|
||
suppressInputUntilMs: number;
|
||
}): { lastOutputPlayableUntilMs: number; suppressInputUntilMs: number; durationMs: number } {
|
||
const durationMs = Math.ceil(params.audio.byteLength / params.bytesPerMs);
|
||
const playbackStartMs = Math.max(params.nowMs, params.lastOutputPlayableUntilMs);
|
||
const playbackEndMs = playbackStartMs + durationMs;
|
||
return {
|
||
durationMs,
|
||
lastOutputPlayableUntilMs: playbackEndMs,
|
||
suppressInputUntilMs: Math.max(params.suppressInputUntilMs, playbackEndMs + params.tailMs),
|
||
};
|
||
}
|