/** * Pre-gate addressing eligibility for mic triggers. * * Root cause of the false-delegation storm: the proactive gate saw only raw * transcript text — no speaker identity, no room context, no notion of who the * speech was *addressed to*. A room full of people, a video, and game audio * produced ~1500 mic segments the gate then rationalized as "the user asking * the koi", even though the wearer was never talking to it. * * This layer enforces the core principle BEFORE the expensive gate model ever * runs: delegate ONLY when the WEARER is ADDRESSING the koi. It is pure and * deterministic (all state lives in an injectable tracker), so it is unit * testable against the real mic-window fixture. * * Three signals, cheapest/strongest first: * 1. Ambient room detection — distinct Soniox speaker tags in a rolling * window. A room/video (>=3 voices) means overheard speech is NOT for the * koi; only an explicit address escapes. * 2. Wake-word address — "koi" as a VOCATIVE (utterance-initial, standalone, * or comma-delimited), not a bare substring. This is the strongest signal * and needs no voice-ID. * 3. Wearer voice-ID (best-effort, never required) — a CONFIDENT non-wearer * can never invoke the owner's koi. * * Whether nameless speech may flow at all is the ENGAGEMENT mode's call * (infra/voice-mode): wake = name only · conversation = name opens a sliding * window · ambient = quiet wearer speech always reaches the relevance gate. */ /** * Rolling window for the distinct-speaker count. Long enough that a burst of * cross-talk still reads as "a room" a few seconds later; short enough that * handing the device back to a solo wearer re-baselines to quiet within a * minute. 45s comfortably spans a handful of conversational turns. */ export declare const AMBIENT_WINDOW_MS = 45000; /** * Distinct Soniox speaker tags at/above which the environment is ambient (a * room, a video, several people talking). 3 voices in 45s is not a 1:1 chat * with the device — it is a scene, and overheard speech is not for the koi. */ export declare const AMBIENT_SPEAKER_THRESHOLD = 3; /** * Sub-window + count for the "sustained continuous mixed speech" heuristic. * Many back-to-back segments from >=2 speakers in a short span reads as live * crosstalk / media dialogue even before a third distinct tag appears. */ export declare const SUSTAINED_WINDOW_MS = 20000; export declare const SUSTAINED_SEGMENT_THRESHOLD = 8; /** * At/above this distinct-speaker count voice-ID must NOT auto-enroll the wearer * (that is what poisoned the "You" profile during the 6-speaker room). Shared * with identify.ts so enrollment and addressing agree on "quiet". */ export declare const LOW_AMBIENT_MAX_SPEAKERS = 2; /** * Cosine at/above which a resolved non-wearer identity counts as a CONFIDENT * other (mirrors voiceid's VOICEID_THRESHOLD). A confident other must never be * able to invoke the owner's koi. */ export declare const WEARER_MATCH_CONFIDENCE = 0.72; export interface AmbientSnapshot { /** Distinct Soniox speaker tags in the last AMBIENT_WINDOW_MS. */ distinctSpeakers: number; /** distinctSpeakers >= AMBIENT_SPEAKER_THRESHOLD. */ ambient: boolean; /** Back-to-back mixed-speaker speech (see SUSTAINED_* constants). */ sustained: boolean; } /** * Rolling count of DISTINCT Soniox `speaker` tags over recent mic segments. * Fed by whichever subsystem tails the audio JSONL (triggers.ts); read by both * addressing (classify) and voice-ID enrollment. */ export declare class AmbientSpeakerTracker { private entries; /** Monotonic view of "latest known time" so out-of-order arrivals + explicit * (test/replay) timestamps prune deterministically without touching the wall * clock — the tracker's window is defined purely by the timestamps it sees. */ private clock; note(speaker: string, ts?: number): void; snapshot(now?: number): AmbientSnapshot; private prune; } /** Module singleton: one gateway process, one shared ambient view. */ export declare const ambientTracker: AmbientSpeakerTracker; /** Feed the shared tracker with a non-echo mic voice. */ export declare function noteMicSpeaker(speaker: string | null | undefined, ts: number): void; export declare function getAmbientSnapshot(now?: number): AmbientSnapshot; /** Low-ambient = safe to auto-enroll the wearer (<=2 distinct recent voices). */ export declare function isLowAmbient(now?: number): boolean; /** * True when `text` addresses the koi with the wake word as a VOCATIVE — the * strongest address signal. Position/word-boundary based, NOT a substring * search, so "uninstall koi and reinstall" or "talking to the koi" do NOT * count (the wake word is neither utterance-initial nor comma-set-off there). */ export declare function isWakeAddress(text: string): boolean; export declare function containsKoiMention(text: string): boolean; /** * Cheap "reads like a request/command" gate for quiet mode. Filters filler and * fragments; a real question mark or an imperative/question lead-in qualifies. * NOTE: this alone is NOT sufficient to delegate — quiet mode additionally * requires a positive wearer voice-ID signal (see classifyMicAddressing). */ export declare function looksLikeDirectRequest(text: string): boolean; /** Minimal structural shape of a resolved voice identity (see voiceid). */ export interface ResolvedSpeakerIdentity { wearer: boolean; anon: boolean; score: number; } export interface MicSegment { text: string; speaker: string | null; ts: number; audioPath?: string; } export interface AddressingContext { distinctSpeakers: number; /** distinctSpeakers >= AMBIENT_SPEAKER_THRESHOLD. */ ambient: boolean; /** Sustained continuous mixed-speaker speech (treated as ambient). */ sustained?: boolean; /** How the koi listens (infra/voice-mode): wake | conversation | ambient. */ engagement?: "wake" | "conversation" | "ambient"; /** conversation mode: a recent exchange keeps the window open — speech * passes without the wake word until the conversation goes quiet. */ conversationLive?: boolean; /** Koi's own TTS is playing over the speakers right now — the mic is * hearing the koi. A mid-sentence "koi" in such a segment is almost * certainly the koi saying its own name; only a true VOCATIVE ("koi, …") * may wake, so a person can still barge with "koi, stop". */ koiSpeaking?: boolean; /** * Best-effort voice-ID resolver, keyed by clip path. Returns null when the * clip is still in flight / pruned / voice-ID unavailable — which is the * common case at classify time (resolution lands ~1-2s after the clip). */ resolveIdentity?: (audioPath: string | undefined) => ResolvedSpeakerIdentity | null; } export type AddressingMode = "wake" | "conversation" | "ambient-flow" | "vocative-rescue" | "blocked"; export interface AddressingResult { eligible: boolean; mode: AddressingMode; reason: string; } /** * Decide whether a batch of mic segments is eligible to reach the proactive * pipeline, and how. See the module doc for the principle. * * - ambient environment (>=3 speakers or sustained crosstalk): eligible ONLY * on a wake-word address by the wearer (or, when voice-ID can't confirm * anyone, a wake-word address at all). NO inference is allowed in a room. * - quiet (<=2 speakers): a wake-word address always passes as 'wake'. * Nameless speech flows only per the ENGAGEMENT mode: an open conversation * window ('conversation') or ambient listening ('ambient-flow'). In wake * engagement — and conversation engagement with the window closed — the * name is the only door. * - a CONFIDENT non-wearer is never eligible. */ export declare function classifyMicAddressing(batch: MicSegment[], ctx: AddressingContext): AddressingResult;