/** * RMS at or below which a frame counts as silence, on the int16 magnitude scale * (full scale 32768). 180 is roughly -45 dBFS: above a quiet room's noise floor * and well below speech, which sits two orders of magnitude higher. */ export declare const VOICE_INPUT_SILENCE_RMS = 180; /** * Analysis frame for the ambient measurement, in milliseconds. * * Short on purpose. The window being measured contains SPEECH, it is the audio * that just triggered the wake, and the estimate below works by finding the * quiet frames inside it. 20 ms is short enough to land wholly inside a stop * closure or an inter-word gap; at 80 ms (the detector's frame) every frame of a * spoken phrase carries some speech energy and there is no quiet frame to find. */ export declare const VOICE_INPUT_AMBIENT_FRAME_MS = 20; /** * Fewest analysis frames that can produce an estimate, 160 ms of audio. * * Below this the low percentile is one or two frames and is no longer a * statistic, so no estimate is reported and the caller falls back to the fixed * constant rather than adapting to a number it cannot stand behind. */ export declare const VOICE_INPUT_AMBIENT_MIN_FRAMES = 8; /** * Which order statistic of the frame levels is taken as the noise floor. * * This is minimum statistics: over a window long enough to contain a pause, the * LOW end of the level distribution tracks the noise floor, because the noise is * present in every frame and the speech is not. The plain minimum is the * textbook form but rides on a single frame; the 20th percentile keeps that * behaviour while surviving one anomalously quiet frame. * * The estimator is biased LOW, the quietest frames of a spoken phrase sit at or * slightly below the true floor, and low is the safe direction here: an * underestimate reproduces today's behaviour (capture rides to the ceiling) * while an overestimate would put the floor above speech, which would mean never * hearing the speaker at all. The margin below is what pays that bias back. */ export declare const VOICE_INPUT_AMBIENT_PERCENTILE = 0.2; /** * How far above measured ambient the floor is placed, as a linear ratio. 4 is * +12 dB. * * Two things have to fit between the noise floor and the floor we set. A steady * noise is only steady in the mean: its own 20 ms frame levels scatter by * several dB, so a floor sitting a dB or two over the mean would still see * "loud" frames and never accumulate silence. And the estimate itself is biased * low, as above. +12 dB clears both. It also stays well clear of speech, which * the fixed constant's own note puts about two orders of magnitude (40 dB) over * a quiet room, leaving roughly 28 dB of headroom under the speaker. */ export declare const VOICE_INPUT_ADAPTIVE_MARGIN = 4; /** * The highest the adaptive floor may go, on the int16 magnitude scale. * * 8x the fixed constant, +18 dB over it, about -27 dBFS. A room noisy enough to * push the floor past this cannot be separated into speech and silence by LEVEL * alone: the next thing above -27 dBFS is the speaker. Clamping here means such * a room keeps the pre-existing behaviour (silence never accumulates, the * ceiling ends the capture), which is a far better failure than a floor set over * the speaker's own level, where nothing is ever heard as speech at all. */ export declare const VOICE_INPUT_ADAPTIVE_FLOOR_MAX: number; /** * How far back the rolling ambient estimate looks, in milliseconds. * * The estimate is a windowed MINIMUM, so the window has to be long enough to * contain at least one quiet moment or it reads the speaker instead of the room. * 1.5 s covers a stop closure or an inter-word gap several times over, and is * still short enough to have forgotten a gain change about a second after it * happens. It sits just above the default `voice.wake.silenceStopMs` on purpose: * the window that decides a stop is then measured against audio from the same * pause, not from the room as it was before the speaker started. */ export declare const VOICE_INPUT_ROLLING_WINDOW_MS = 1500; /** * The floor is never raised above this fraction of the speech level tracked over * the same capture, 3 is about 9.5 dB of clearance under the speaker. * * This is the guard that makes raising the floor DURING a capture safe at all. A * floor that walks up with the room and is not held under something has exactly * one catastrophic failure: it passes the speaker's own level, every frame reads * as silence, and the utterance ends the instant it begins. Bounding it against * the loudest thing this capture has heard means the failure cannot happen, * whatever else is true, there is always most of a factor of three between the * floor and the voice it has to stay under. */ export declare const VOICE_INPUT_SPEECH_FLOOR_RATIO = 3; /** * Half-life of the tracked speech level, in milliseconds. * * Deliberately long, comparable to a whole capture rather than to a pause. The * tracker is a running maximum that decays, and the decay is only there so a * speaker who genuinely gets quieter across a long dictation is not measured * against how loud they were at the start. * * A FAST decay breaks the guard in the exact case it exists for. Once the tracked * level falls to the risen noise floor, the noise becomes the "speech" the guard * is protecting, the cap collapses to a third of the noise, and the floor is * pinned below the thing it was supposed to rise over. Eight seconds keeps the * loudest speech of this utterance in force for the length of the default * ceiling, which is the span any one stop decision is made in. */ export declare const VOICE_INPUT_SPEECH_LEVEL_HALF_LIFE_MS = 8000; /** * The highest the ROLLING floor may go, on the int16 magnitude scale. 32x the * fixed constant, four times {@link VOICE_INPUT_ADAPTIVE_FLOOR_MAX}. * * The 8x cap on the one-shot path is a statement about a MEASUREMENT: that floor * comes from the pre-roll and nothing else, and a pre-roll reading that high is * likelier to be a bad measurement than a genuinely loud room, so it is refused * and the old behaviour stands. * * The rolling path is not one reading. It is re-derived every frame and held * under {@link VOICE_INPUT_SPEECH_FLOOR_RATIO} of the speech heard alongside it, * so the danger the 8x cap approximates, a floor set over the speaker, is * checked directly instead. Applying 8x here would break the case this path * exists for: automatic gain control moves ambient AND speech up together, so a * ceiling fixed at 1440 pins the floor under a raised noise floor while the * speaker sits far above both. 32x remains only as a bound on runaway input. */ export declare const VOICE_INPUT_ROLLING_FLOOR_MAX: number; /** * Default `voice.wake.speechRetriggerMs`: how long a run of loud frames must * last before it counts as speech resuming. * * 150 ms is under the shortest syllable anyone ends a sentence on and over the * longest breath, lip tick or chair creak a close-worn microphone picks up. * Below it, a run counts toward silence rather than resetting it; at or above * it, the speaker is talking again and the trailing-silence count starts over. * 0 restores the pre-existing rule where every loud frame resets. */ export declare const VOICE_INPUT_SPEECH_RETRIGGER_MS = 150; /** * Measure the room's noise floor from a window of audio, on the int16 magnitude * scale. Returns null when the window is too short to measure. * * The window it is given is the wake detection's pre-roll, which is mostly the * wake phrase itself. That is fine, and is the reason for the order statistic * rather than a mean: see {@link VOICE_INPUT_AMBIENT_PERCENTILE}. A mean over * this window would measure the SPEAKER, not the room. */ export declare function estimateAmbientRms(ambient: Float32Array, sampleRate?: number): number | null; /** * Whether `voice.wake.silenceFloorRms` names a level, as opposed to being unset * or 0. The single predicate behind BOTH consequences of setting that row: the * value wins over the measurement, and the floor is then frozen for the whole * utterance rather than following the room. A pinned value is pinned. */ export declare function isSilenceFloorPinned(override: number | undefined): override is number; /** * Decide the silence floor for one utterance. The single place the rule lives. * * In order: * 1. An explicit `voice.wake.silenceFloorRms` above 0 wins outright, someone * who set a level meant that level, and is not asking to be second-guessed. * 2. Otherwise measure the room from `ambient` and place the floor * {@link VOICE_INPUT_ADAPTIVE_MARGIN} above it, clamped into * [{@link VOICE_INPUT_SILENCE_RMS}, {@link VOICE_INPUT_ADAPTIVE_FLOOR_MAX}]. * The LOWER clamp is what keeps a genuinely quiet room behaving exactly as * it does today: measuring near-silence can only ever hold the floor at the * constant, never drop it below and start clipping sentences. * 3. With no sample, or too short a one, the constant stands unchanged. */ export declare function resolveSilenceFloorRms(options: { /** `voice.wake.silenceFloorRms`. 0 or unset means adapt. */ readonly override?: number | undefined; /** Pre-wake audio to measure the room from. */ readonly ambient?: Float32Array | undefined; readonly sampleRate?: number | undefined; }): number; /** Why an utterance stopped. */ export type VoiceInputStopReason = /** The surface asked it to stop, a released key, an explicit cancel. */ 'requested' /** `silenceStopMs` of silence elapsed after speech. */ | 'silence' /** `captureMaxSeconds` reached. */ | 'max-duration' /** The capture stream ended underneath it. */ | 'stream-ended'; export interface VoiceInputPolicy { /** * Hard ceiling in seconds, from `voice.wake.captureMaxSeconds`. 0 removes the * ceiling entirely, leaving silence-stop (post-wake) or the key release * (push-to-talk) as the only thing that ends the utterance. */ readonly captureMaxSeconds: number; /** * Silence in milliseconds that ends the utterance, from * `voice.wake.silenceStopMs`. 0 disables silence-stop, which is what * push-to-talk uses. */ readonly silenceStopMs: number; /** Sample rate of the frames pushed in. Defaults to the capture rate. */ readonly sampleRate?: number | undefined; /** * The silence floor for this utterance, on the int16 magnitude scale, already * decided by {@link resolveSilenceFloorRms}. Unset falls back to the fixed * {@link VOICE_INPUT_SILENCE_RMS}, which is what a caller with no ambient * sample to measure gets anyway. * * This is the STARTING floor. Unless {@link silenceFloorPinned} says * otherwise the recorder may raise it during the capture, and never lowers it * below this value. */ readonly silenceRms?: number | undefined; /** * True when {@link silenceRms} came from an explicit * `voice.wake.silenceFloorRms`. Freezes the floor for the whole utterance: no * rolling adjustment at all. Callers derive it from the same row with * {@link isSilenceFloorPinned} rather than deciding it separately. */ readonly silenceFloorPinned?: boolean | undefined; /** * How long a run of loud frames must last to count as speech, from * `voice.wake.speechRetriggerMs`. Defaults to * {@link VOICE_INPUT_SPEECH_RETRIGGER_MS}; 0 restores the rule where every * loud frame arms the capture and resets trailing silence. */ readonly speechRetriggerMs?: number | undefined; } /** * What the endpointing actually did, in the numbers it decided from. * * Every one of these is a question that was unanswerable after a capture that * behaved wrongly: the room was loud, or it was not; the floor moved, or it was * pinned; the capture ended on silence, or it never found any. A stop reason * alone says which branch ran, not why it was the one that ran. */ export interface VoiceInputEndpointing { /** The floor the utterance started with, before any rolling adjustment. */ readonly initialFloorRms: number; /** The floor in force on the last frame. Equal to the initial one when pinned. */ readonly finalFloorRms: number; /** * The rolling ambient estimate at the stop, the windowed minimum of recent * frame levels. Null when no frame was ever pushed. */ readonly ambientRms: number | null; /** The tracked speech level at the stop, which is what bounded the floor. */ readonly speechLevelRms: number; /** True when `voice.wake.silenceFloorRms` froze the floor. */ readonly floorPinned: boolean; readonly stopReason: VoiceInputStopReason; readonly durationMs: number; /** Trailing silence accumulated at the stop, absorbed short bursts included. */ readonly trailingSilenceMs: number; /** Loud runs shorter than the retrigger that were counted as silence. */ readonly absorbedBurstCount: number; /** The retrigger length in force, in milliseconds. */ readonly speechRetriggerMs: number; } /** A finished utterance, ready to become a speech-to-text request. */ export interface CapturedUtterance { /** Every sample kept, pre-roll first. */ readonly samples: Float32Array; readonly sampleRate: number; readonly durationMs: number; /** Milliseconds of the above that came from before the wake fired. */ readonly preRollMs: number; readonly stopReason: VoiceInputStopReason; /** True when nothing above the silence floor was ever heard. */ readonly silent: boolean; /** What the endpointing decided from, for the capture-end receipt. */ readonly endpointing: VoiceInputEndpointing; } /** * Accumulates frames into one utterance under the stop policy. * * One recorder per utterance; call {@link VoiceInputRecorder.finish} once. It * tracks time from the frames themselves rather than a clock, so a test can push * frames as fast as it likes and get exactly the same decisions a live stream * makes at real-time speed. */ export declare class VoiceInputRecorder { #private; constructor(policy: VoiceInputPolicy); /** Milliseconds of audio held so far, pre-roll included. */ get durationMs(): number; /** * True once a run of loud frames at least `speechRetriggerMs` long has been * seen. Room ticks before the speaker starts must NOT arm this: arming is what * makes silence-stop live, and a capture armed by a chair creak ends * `silenceStopMs` later with nothing in it. */ get heardSpeech(): boolean; /** The floor in force right now, after any rolling adjustment. */ get effectiveSilenceRms(): number; /** Trailing silence accumulated so far, absorbed short bursts included. */ get trailingSilenceMs(): number; /** What the endpointing has decided from so far. Safe to read at any point. */ get endpointing(): Omit; /** * Seed the utterance with audio from before it started, the wake detection's * pre-roll. Must be called before any {@link push}, and does not count toward * the silence tracking: it is by definition the phrase that just triggered. */ seedPreRoll(samples: Float32Array): void; /** * Add one frame. Returns the reason the utterance should stop, or null to keep * capturing. The frame is always kept first, a frame that trips the ceiling is * part of the utterance, not discarded with it. */ push(frame: Float32Array): VoiceInputStopReason | null; /** Close the utterance out. Safe to call once; the recorder is spent after it. */ finish(stopReason: VoiceInputStopReason): CapturedUtterance; } /** A speech-to-text request body, matching the `voice.stt` verb's audio artifact. */ export interface UtteranceAudioArtifact { readonly mimeType: string; readonly format: string; readonly dataBase64: string; readonly sampleRateHz: number; readonly durationMs: number; } /** * Encode an utterance as the audio artifact the `voice.stt` verb takes. * * WAV rather than a compressed container: a host-captured stream has no codec in * front of it, and the provisioned whisper build reads WAV directly. The base64 * is produced without `Buffer` so the same call works in a browser tab. */ export declare function utteranceToAudioArtifact(utterance: CapturedUtterance): UtteranceAudioArtifact; //# sourceMappingURL=voice-input.d.ts.map