UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

135 lines (134 loc) • 5.13 kB
/** * Dictionary matching for transcript correction. * * A dictionary entry names a term (a person, product, acronym …), the ways * speech engines mis-hear it, and what it means. Every alias found in a * transcript becomes a *candidate*: it is not applied blindly, because the * alias of a name is usually an ordinary word too ("part" for a name that * sounds like it). The guard (`guard.ts`) decides per candidate; this module * only finds them and applies the verdicts. * * Matching is whole-word and script-agnostic: a word boundary is any * character that is not a letter, a combining mark or a digit. Marks matter * for Indic scripts, where vowel signs and viramas are `\p{M}`, not `\p{L}` — * a letters-only boundary would let an alias match the first half of a word. * * @module voice/correction/dictionary */ /** * Any Indic script, Devanagari (U+0900) through Sinhala (U+0DFF): Bengali, * Gurmukhi, Gujarati, Odia, Tamil, Telugu, Kannada and Malayalam sit between. */ const INDIC_SCRIPT_RE = /[ऀ-෿]/u; /** Characters that continue a word: letters, combining marks, digits. */ const WORD_CHAR = "[\\p{L}\\p{M}\\p{N}]"; function escapeRegExp(text) { return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } /** * Whole-word, case-insensitive matcher for `phrase`. Whitespace inside a * multi-word alias matches any run of whitespace, since engines differ in how * they space words. */ function wholeWordPattern(phrase, flags = "iu") { const body = escapeRegExp(phrase.trim()).replace(/\s+/g, "\\s+"); return new RegExp(`(?<!${WORD_CHAR})${body}(?!${WORD_CHAR})`, flags); } /** Whether `text` contains any Indic-script character. */ export function hasIndicScript(text) { return INDIC_SCRIPT_RE.test(text); } /** * Every alias of every entry that occurs in `text` as a whole word. * * An entry whose term already appears in the text contributes no candidates: * the engine heard the term itself, so an alias elsewhere in the same text is * more likely the ordinary word. An alias in an Indic script is skipped when * the text has none (it cannot match, and the check is cheaper than a regex). * Each (entry, alias) pair is reported once, however often it occurs. */ export function findDictionaryCandidates(text, dictionary) { if (!text || !dictionary?.length) { return []; } const textHasIndic = hasIndicScript(text); const out = []; for (const entry of dictionary) { const term = entry?.term?.trim(); if (!term || !entry.heardAs?.length) { continue; } if (wholeWordPattern(term).test(text)) { continue; } const seen = new Set(); for (const alias of entry.heardAs) { const heard = typeof alias === "string" ? alias.trim() : ""; if (!heard || seen.has(heard.toLowerCase())) { continue; } seen.add(heard.toLowerCase()); if (hasIndicScript(heard) && !textHasIndic) { continue; } if (wholeWordPattern(heard).test(text)) { out.push({ entry, heard }); } } } return out; } /** * Replace every candidate the guard did not call `"literal"` with its term. * * `decisions[i]` belongs to `candidates[i]`; a missing decision applies the * term, which is the same fail-open default the guard uses. Every occurrence * is replaced — the decision was asked about the alias in this transcript, so * all of its occurrences share the answer. */ export function applyDictionary(text, candidates, decisions) { let out = text; candidates.forEach((candidate, index) => { if (decisions[index]?.choice === "literal") { return; } const term = candidate.entry.term.trim(); // A function replacement, so a "$" in the term is never read as a group reference. out = out.replace(wholeWordPattern(candidate.heard, "giu"), () => term); }); return out; } /** * The dictionary as a table for the rewrite model's system prompt, or `""` * when there is nothing to show. */ export function dictionaryBlock(dictionary) { const rows = (dictionary ?? []) .filter((entry) => entry?.term?.trim()) .map((entry) => { const heard = (entry.heardAs ?? []) .map((alias) => alias.trim()) .filter(Boolean) .join(", "); return `${entry.term.trim()} | ${heard || "-"} | ${entry.meaning?.trim() || "-"}`; }); return rows.length ? `Dictionary (term | also heard as | meaning):\n${rows.join("\n")}` : ""; } /** * Distinct dictionary terms, in dictionary order, for engines that bias * recognition on a vocabulary or a context prompt. */ export function dictionaryVocabulary(dictionary) { const seen = new Set(); const out = []; for (const entry of dictionary ?? []) { const term = entry?.term?.trim(); if (term && !seen.has(term.toLowerCase())) { seen.add(term.toLowerCase()); out.push(term); } } return out; }