/** * Pure audio/codec helpers, split out of the browser-heavy STT + mic-monitor code * so the fiddly bits (level maths, codec/filename selection, upstream error parsing) * are unit-tested without a real AudioContext, MediaRecorder, or network. */ /** Recorder mime types we try, best → worst. Opus in WebM is smallest + best for * Whisper; Safari only does mp4/…; ogg is a last resort. */ export declare const RECORDER_MIME_CANDIDATES: readonly ["audio/webm;codecs=opus", "audio/webm", "audio/mp4", "audio/ogg"]; /** * RMS loudness of one FFT frame, normalised to ~0–1. `128` is the reference level * (half of a byte-magnitude bin); the result is clamped so a loud frame can't exceed 1. */ export declare function computeRmsLevel(freq: Uint8Array): number; /** * Pick the first recorder mime type the browser actually supports. `isSupported` is * injected (MediaRecorder.isTypeSupported) so this is pure + testable. Returns `""` * when none match — the caller then lets MediaRecorder choose its own default. */ export declare function pickRecorderMimeType(isSupported: (type: string) => boolean): string; /** * Whisper infers the audio format from the filename EXTENSION, so it must match the * recorded container — Safari records `audio/mp4`, which uploaded as `audio.webm` * gets rejected with a 400. Map the blob mime to the right extension. */ export declare function whisperFilename(blobType: string): string; /** * OpenAI rejects clips under 0.1s with a 400 `audio_too_short`, and the VAD can fire * on a cough or a click that records almost nothing. Anything below this floor is * never real speech — dropping it BEFORE upload stops the error-log spam (and a * pointless round-trip). Duration is the reliable signal; the byte floor only catches * a header-only capture when the duration is unknown. */ export declare const MIN_TRANSCRIBE_MS = 250; export declare const MIN_TRANSCRIBE_BYTES = 512; export declare function isTooShortToTranscribe(byteLength: number, durationMs: number): boolean; /** * The phrase set, for the guard that keeps it matchable. Exported for tests only — callers judge * a transcript with `isNoiseTranscript`, which is the function that knows about normalisation, * bias echo and glyph counting. Handing the bare set to a caller would invite a second, wrong * lookup against un-normalised text, which is the defect this export exists to prevent. */ export declare const SILENCE_HALLUCINATION_PHRASES: ReadonlySet; export declare function isNoiseTranscript(text: string, biasPrompt?: string): boolean; /** A repeated phrase must occur at least this many times CONSECUTIVELY, and cover at least * {@link REPETITION_MIN_COVERAGE} of the words, before the transcript is called a loop. Both * bars are deliberately high: a false positive here DROPS A REAL TURN, which is the exact harm * this is meant to prevent. "no, no, no" (3, short) and "very very good" stay ordinary speech. */ export declare const REPETITION_MIN_RUN = 4; export declare const REPETITION_MIN_COVERAGE = 0.5; /** * The second shape, and the bars are the other way round on purpose (#511). * * A unit of at least {@link REPETITION_LONG_UNIT_MIN_WORDS} words repeated only * {@link REPETITION_LONG_UNIT_MIN_RUN} times, covering {@link REPETITION_LONG_UNIT_MIN_COVERAGE} of * the transcript, is also a decoder — because verbatim repetition becomes less human the longer the * unit is. "no, no, no" is a person insisting; an eleven-word clause reproduced word for word, as * essentially the whole turn, is not something people do and is exactly what a stuck decoder emits. * * This exists because of a phantom that reached an agent as a real user message on 2026-08-10: * `"Pottery Barn Please visit www.potterybarn.com for more ideas and inspiration."` — twice. That is * Whisper's advertising-boilerplate hallucination, and it is the case #511 was left open for: the * `no_speech_prob` gate is a `whisper-1` capability, the default model is `gpt-4o-transcribe`, and * `isNoiseTranscript`'s whole-utterance list of known sign-offs can never contain a retailer's ad * copy. Widening that list is whack-a-mole against a training corpus; the shape is not. * * Deliberately NOT a confidence threshold. `include[]=logprobs` is the model-side signal the * streaming API does expose, and it stays rejected for the reason it was rejected before: it is a * blind discard layer on the only transcription path anyone uses, there is no measured distribution * to tune it against, and the live complaint is speech going MISSING. This rule fires on a property * of the text that can be read off the recorded transcript and argued about. */ export declare const REPETITION_LONG_UNIT_MIN_WORDS = 6; export declare const REPETITION_LONG_UNIT_MIN_RUN = 2; /** Near-total, because two repeats is a low bar and coverage is what stops it eating a person who * restated themselves and then carried on. */ export declare const REPETITION_LONG_UNIT_MIN_COVERAGE = 0.85; /** * Is this transcript a decoder repetition loop rather than something a person said? (#512) * * Autoregressive decoders get stuck. Two real ones reached this platform on 2026-08-11, and the * first of them was **sent to the agent as a real user turn**: * * "apps chess academy, chess academy, chess academy, chess academy, chess academy, chess academy" * "chess-academy" ×14 * * `isNoiseTranscript` cannot catch these: it matches a fixed list of silence hallucinations, and a * loop is made of the user's OWN vocabulary, so every phrase in it is plausible. The shape is the * signal, not the words — which is also why this survives whatever the transcription model does * next. Whether `temperature` provoked these is unresolved (see stt.ts); this holds either way, * and it is the half of #512 that does not depend on that answer. * * Judged on the whole utterance and on normalised words, so punctuation and casing between the * repeats ("chess academy, chess academy" vs "chess-academy chess-academy") cannot hide the shape. */ export declare function isRepetitionLoop(text: string): boolean; /** One decoded event from the streaming-transcription SSE (gpt-4o-transcribe). */ export interface TranscriptionStreamEvent { type: string; delta?: string; text?: string; } /** * Parse one SSE `data:` payload from a streaming transcription. Returns null for junk, * heartbeats, or the `[DONE]` sentinel. Pure so the (fiddly) event handling is tested * without a real network stream. Event shapes (per the OpenAI API reference): * { type: "transcript.text.delta", delta: "He" } — incremental * { type: "transcript.text.done", text: "Hello" } — final */ export declare function parseTranscriptionEvent(dataPayload: string): TranscriptionStreamEvent | null; /** * Pull complete `data:` payloads out of an SSE text buffer, returning the leftover * partial line to carry into the next chunk (SSE events can split across network * chunks). Pure so chunk-boundary handling is unit-tested. Only `data:` lines are * returned; `event:`/comment/blank lines are dropped. */ export declare function drainSseData(buffer: string): { data: string[]; rest: string; }; /** * Pull a human reason out of an error body from either side of the proxy. Never throws. * * TWO envelopes reach this, and until #421 it only knew one. OpenAI returns * `{ error: { message } }`; **PAGS's own API returns `{ error: "…" }`, where `error` is a STRING** — * so `?.error?.message` was `undefined` and it fell through to `|| rawBody`. During a deploy the * platform's deliberately reassuring 503 ("The service is updating — please try again in a moment.", * `workers/api/src/index.ts`) therefore reached the user as raw JSON. */ export declare function parseUpstreamErrorDetail(rawBody: string): string; /** Did this error body come from PAGS itself rather than from OpenAI? See above — the string-vs- * object shape of `error` is the discriminator, and it is the only one available at the boundary. */ export declare function isPlatformErrorBody(rawBody: string): boolean; /** * The message a user sees when a transcription request comes back not-OK (#421). * * Two things it must not do, both of which it did. It must not print raw JSON — the one message the * platform writes specifically to be calming was delivered in the least calming form available. And * it must not say **"Whisper"** about a failure that never reached OpenAI: a PAGS redeploy blamed on * the user's AI vendor sends them to look at the wrong thing, and a 503 from our own proxy is * distinguishable right here. */ export declare function describeTranscribeHttpError(status: number, rawBody: string): string; //# sourceMappingURL=audio.d.ts.map