UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

181 lines (180 loc) 8.72 kB
import { t as levenshteinDistance } from "./levenshtein-distance-C0TL9gUO.js"; //#region src/talk/activation-name.ts /** * Realtime voice activation-name matching for direct spoken address. * * The matcher accepts short names at the leading or trailing edge of a * transcript, strips the name before agent routing, and keeps fuzzy matching * conservative so ordinary dictation does not trigger Talk turns. */ const REALTIME_VOICE_ACTIVATION_NAME_MAX_WORDS = 2; /** Count alphanumeric words in a configured activation name. */ function realtimeVoiceActivationNameWordCount(value) { return Array.from(value.matchAll(/[a-z0-9]+/gi)).length; } /** Normalize configured activation names while preserving word boundaries. */ function normalizeRealtimeVoiceActivationName(value) { return value.toLowerCase().replace(/\s+/g, " ").trim() || void 0; } /** Extract the supported leading activation-name prefix from a longer phrase. */ function normalizeRealtimeVoiceActivationNamePrefix(value, maxWords = 2) { const words = Array.from(value.matchAll(/[a-z0-9]+/gi), (match) => match[0]); if (words.length === 0) return; return words.slice(0, maxWords).join(" "); } /** Validate the configured activation name length bound. */ function isSupportedRealtimeVoiceActivationName(value, maxWords = 2) { const wordCount = realtimeVoiceActivationNameWordCount(value); return wordCount >= 1 && wordCount <= maxWords; } /** Normalize and reject unsupported activation names in one reusable step. */ function normalizeSupportedRealtimeVoiceActivationName(value, maxWords = 2) { if (typeof value !== "string") return; const normalized = normalizeRealtimeVoiceActivationName(value); return normalized && isSupportedRealtimeVoiceActivationName(normalized, maxWords) ? normalized : void 0; } /** Prefer longer names first so nested names match the most specific option. */ function sortRealtimeVoiceActivationNames(names) { return names.toSorted((left, right) => right.length - left.length || left.localeCompare(right)); } /** Match and strip a configured activation name from either transcript edge. */ function matchRealtimeVoiceActivationName(text, activationNames, maxWords = 2) { const preparedActivationNames = []; for (const activationName of activationNames) { const normalizedActivationName = normalizeActivationNameCandidate(activationName); if (!normalizedActivationName) continue; preparedActivationNames.push({ activationName, compact: compactActivationName(normalizedActivationName) }); } if (preparedActivationNames.length === 0) return; const candidates = [...leadingActivationNameCandidates(text, maxWords), ...trailingActivationNameCandidates(text, maxWords)].map((candidate) => ({ candidate, compact: compactActivationName(candidate.heardName) })).toSorted((left, right) => right.compact.length - left.compact.length); for (const { candidate, compact: heardCompact } of candidates) for (const { activationName, compact: activationCompact } of preparedActivationNames) { const exactMatch = heardCompact === activationCompact; const fuzzyMatch = isFuzzyActivationNameMatch(candidate, heardCompact, activationCompact); if (exactMatch || fuzzyMatch) return { allowed: true, text: stripEdgeActivationNameCandidate(text, candidate), activationName, heardName: candidate.heardName, match: exactMatch ? "exact" : "fuzzy", edge: candidate.edge }; } } function normalizeActivationNameCandidate(value) { return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim() || void 0; } function compactActivationName(value) { return value.replace(/[^a-z0-9]+/g, ""); } function leadingActivationNameCandidates(text, maxWords) { const nameStart = /^\s*(?:(?:hey|ok|okay)(?:\s*[-,:;]+\s*|\s+))?/i.exec(text)?.[0].length ?? 0; const candidates = []; const candidateStarts = nameStart > 0 ? [0, nameStart] : [0]; for (const startIndex of candidateStarts) { const tokenPattern = /[a-z0-9]+/gi; tokenPattern.lastIndex = startIndex; const startCandidates = []; for (let wordCount = 0; wordCount < maxWords; wordCount += 1) { const token = tokenPattern.exec(text); if (!token) break; const previousEndIndex = wordCount === 0 ? startIndex : startCandidates[wordCount - 1]?.endIndex; const between = text.slice(previousEndIndex, token.index); if (wordCount > 0 && !/^[\s'-]+$/.test(between)) break; const endIndex = token.index + token[0].length; const heardName = normalizeActivationNameCandidate(text.slice(startIndex, endIndex)); if (!heardName) break; const boundary = text.slice(endIndex).match(/^\s*([,.:;!?-]|$)/); startCandidates.push({ edge: "leading", heardName, startIndex, endIndex, strongBoundary: Boolean(boundary) }); } candidates.push(...startCandidates); } return candidates; } function trailingActivationNameCandidates(text, maxWords) { const tokens = Array.from(text.matchAll(/[a-z0-9]+/gi)); const candidates = []; const tokenCount = Math.min(tokens.length, maxWords); for (let wordCount = 1; wordCount <= tokenCount; wordCount += 1) { const startToken = tokens[tokens.length - wordCount]; const endToken = tokens[tokens.length - 1]; if (!startToken || !endToken?.[0]) break; const startIndex = startToken.index ?? 0; const endIndex = (endToken.index ?? 0) + endToken[0].length; if (!/^\s*(?:[,.:;!?-]+\s*)?$/.test(text.slice(endIndex))) break; if (!/(^|[\s,.:;!?-])$/.test(text.slice(0, startIndex))) break; const directAddressBoundary = /(^|[,.:;!?-]\s*)$/.test(text.slice(0, startIndex)); const trailingQuestion = /\?\s*$/.test(text); if (wordCount > 1) { const previousToken = tokens[tokens.length - wordCount + 1]; const between = previousToken ? text.slice(startIndex + startToken[0].length, previousToken.index) : ""; if (!/^[\s'-]+$/.test(between)) break; } const heardName = normalizeActivationNameCandidate(text.slice(startIndex, endIndex)); if (!heardName) break; candidates.push({ edge: "trailing", heardName, startIndex, endIndex, strongBoundary: directAddressBoundary && trailingQuestion }); } return candidates; } function hasOnlyPhoneticSubstitutions(left, right) { if (left.length !== right.length) return false; const vowels = /* @__PURE__ */ new Set([ "a", "e", "i", "o", "u", "y" ]); const liquids = /* @__PURE__ */ new Set(["l", "r"]); let substitutions = 0; for (let index = 0; index < left.length; index += 1) { const leftChar = left[index]; const rightChar = right[index]; if (leftChar === rightChar) continue; const vowelLike = vowels.has(leftChar ?? "") && vowels.has(rightChar ?? ""); const liquidLike = liquids.has(leftChar ?? "") && liquids.has(rightChar ?? ""); if (!vowelLike && !liquidLike) return false; substitutions += 1; } return substitutions > 0; } function commonPrefixLength(left, right) { const limit = Math.min(left.length, right.length); for (let index = 0; index < limit; index += 1) if (left[index] !== right[index]) return index; return limit; } function isFuzzyActivationNameMatch(candidate, heardCompact, activationCompact) { if (!heardCompact || !activationCompact || activationCompact.length < 5) return false; if (!candidate.strongBoundary) return false; if (heardCompact[0] !== activationCompact[0]) return false; const distance = levenshteinDistance(heardCompact, activationCompact); if (candidate.edge === "trailing") return heardCompact.length === activationCompact.length && hasOnlyPhoneticSubstitutions(heardCompact, activationCompact); if (distance <= 1) return true; if (distance === 2 && heardCompact.length >= 4 && activationCompact.length >= 5 && (heardCompact.length !== activationCompact.length || hasOnlyPhoneticSubstitutions(heardCompact, activationCompact) || commonPrefixLength(heardCompact, activationCompact) >= 6)) return true; if (distance === 3 && heardCompact.length >= 7 && activationCompact.length >= 7 && heardCompact.length !== activationCompact.length && commonPrefixLength(heardCompact, activationCompact) >= 5) return true; return false; } function stripEdgeActivationNameCandidate(text, candidate) { if (candidate.edge === "leading") return text.slice(candidate.endIndex).replace(/^\s*(?:[-,:;.!?]+\s*)?/, "").trim(); return text.slice(0, candidate.startIndex).replace(/\s*(?:[-,:;.!?]+\s*)?$/, "").trim(); } //#endregion export { normalizeRealtimeVoiceActivationNamePrefix as a, sortRealtimeVoiceActivationNames as c, normalizeRealtimeVoiceActivationName as i, isSupportedRealtimeVoiceActivationName as n, normalizeSupportedRealtimeVoiceActivationName as o, matchRealtimeVoiceActivationName as r, realtimeVoiceActivationNameWordCount as s, REALTIME_VOICE_ACTIVATION_NAME_MAX_WORDS as t };