Files
openclaw/src/talk/session-log-runtime.ts
Peter Steinberger 77d9ac30bb refactor: reuse shared coercion helpers (#86419)
* 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
2026-05-25 21:20:41 +01:00

157 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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),
};
}