@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
507 lines (506 loc) • 20 kB
TypeScript
/**
* `transcribe` — the fourth inference type, next to `generate`, `stream` and
* `decide`. Turns audio into text through any registered STT handler, with the
* optional layers that make self-hosted engines usable in production:
*
* - a dictionary of names and jargon, sent to the engine as context and used
* to repair mis-hearings after the fact;
* - a decision-model guard (`tryDecide`) that asks, per dictionary candidate,
* whether the speaker meant the term or the literal word, so a name never
* rewrites the ordinary word it sounds like; fails open like every other
* `decide` consumer;
* - an LLM rewrite (`generate`/`stream`) that fixes spelling and punctuation
* and writes Indic-script words in Latin letters the way people type
* (Hinglish, Tanglish …) without ever translating them;
* - a second-opinion engine whose text the rewrite reconciles with the
* primary one, and a fallback engine for utterances the primary cannot read
* (an Indic model on English speech reports a low language score, or
* nothing at all);
* - client-side streaming for batch engines: a chunked adapter with an energy
* gate, rolling re-transcription and LocalAgreement commits.
*
* Nothing in these layers may lose words: a rewrite that drops more than
* `maxDropRatio` of them is discarded, a timed-out guard applies the dictionary
* as is, and a final pass that returns less than the live text keeps the live
* text.
*
* @module types/transcribe
*/
import type { TTSAudioFormat } from "./tts.js";
import type { STTHandler, STTOptions, STTResult, TranscriptionSegment, WordTiming } from "./stt.js";
/**
* Audio for `transcribe()`: bytes, a path or `file://` / `http(s)://` URL, or
* an async stream of PCM16LE frames (see `STTStreamingOptions.sampleRate`).
*/
export type TranscribeAudioInput = Buffer | ArrayBuffer | Uint8Array | string | AsyncIterable<Buffer | Uint8Array>;
/** One dictionary entry: the correct spelling, how engines mis-hear it, and what it means. */
export type STTDictionaryEntry = {
/** Correct spelling, written exactly like this in the output. */
term: string;
/** Mis-hearings, in any script ("jasper", "जसपे"). Each one found in a transcript becomes a candidate. */
heardAs?: string[];
/** Meaning or usage, given to the decision model and the rewrite model. */
meaning?: string;
};
/** The correction layer that runs on a finished transcript (or each final utterance when streaming). */
export type STTCorrectionOptions = {
/**
* Default: on when a dictionary is given, or when this object is given at
* all with `rewrite` not `false`; off otherwise.
*/
enabled?: boolean;
/**
* Guard for dictionary substitutions. `"decide"` asks the decision provider
* per candidate (term or literal?) and applies the dictionary as is when no
* decision provider is configured or the call fails; `"none"` always applies it.
*/
guard?: "decide" | "none";
/** Timeout for the guard call; on expiry the dictionary is applied as is. Default 4000. */
decideTimeoutMs?: number;
/**
* LLM rewrite: spelling, punctuation, dictionary terms, transliteration.
* `false` disables it (dictionary-only correction). `provider`/`model` name
* any text provider; the instance default is used when omitted.
*/
rewrite?: false | {
provider?: string;
model?: string;
/** On expiry the un-rewritten text is kept. Default 12000. */
timeoutMs?: number;
};
/**
* How Indic-script text is written: `"latin"` the way people type on phones
* (Hinglish / Tanglish — the default), `"native"` keeps the script. Never a translation.
*/
transliterate?: "latin" | "native";
/** Add punctuation and capitalization in the rewrite. Default true. */
punctuate?: boolean;
/**
* A second engine run on the same audio; the rewrite reconciles both
* transcripts (Indian-language words from an Indic engine, English words
* from an English-oriented one).
*/
secondOpinion?: {
provider: string;
model?: string;
};
/** One sentence about who is talking and about what; sent to the engines and the guard. */
context?: string;
/** A rewrite that drops more than this share of the words is discarded. Default 0.25. */
maxDropRatio?: number;
};
/**
* Where an utterance goes when the primary engine cannot read it. `when`
* defaults to `["unsure", "empty"]`: a low language-detection score, or no
* text at all.
*/
export type STTFallbackOptions = {
provider: string;
model?: string;
when?: Array<"unsure" | "empty" | "error">;
};
/** Client-side streaming for engines without a native stream (and tuning for the adapter). */
export type STTStreamingOptions = {
/** `"auto"` uses the handler's native stream when it has one, else the chunked adapter. */
mode?: "auto" | "native" | "chunked";
/** Sample rate of the incoming PCM16LE frames. Default 16000. */
sampleRate?: number;
/** Silence that ends an utterance. Default 750. */
endSilenceMs?: number;
/** Cadence of interim passes on the current utterance. Default 900. */
intervalMs?: number;
/** After this many seconds a turn ends at the next short pause. Default 16. */
softCutSeconds?: number;
/** Hard cut at the quietest recent block, remainder carried over. Default 28. */
maxUtteranceSeconds?: number;
/** Absolute RMS floor for speech onset. Default 0.006. */
onsetRms?: number;
/** Audio kept before the detected onset. Default 350. */
prerollMs?: number;
/** Shortest utterance worth a pass. Default 250. */
minSpeechMs?: number;
};
/**
* Options for `neurolink.transcribe()`. `provider` defaults to
* `NEUROLINK_STT_PROVIDER`, then the first configured STT provider in
* descriptor order, then the built-in local engine (Whistle), which needs no
* credentials. `language` omitted or `"auto"` lets the engine detect it.
*/
export type TranscribeOptions = Omit<STTOptions, "enabled" | "provider" | "language" | "model" | "format"> & {
audio: TranscribeAudioInput;
provider?: string;
model?: string;
/** ISO / BCP-47 code, or `"auto"` (the default) to let the engine detect. */
language?: string;
/** Audio container of a buffer input; inferred from a path or URL. */
format?: TTSAudioFormat;
/** Context prompt for engines that bias on one (Whisper, Qwen3-ASR …). Dictionary terms are appended. */
prompt?: string;
dictionary?: STTDictionaryEntry[];
correction?: STTCorrectionOptions;
fallback?: STTFallbackOptions;
streaming?: STTStreamingOptions;
/** Whole-call timeout. */
timeoutMs?: number;
/** Per-call credentials; the `stt` slice reaches the handlers. */
credentials?: STTCredentialsCarrier;
};
/** `transcribeStream()` takes frames instead of a whole buffer. */
export type TranscribeStreamOptions = Omit<TranscribeOptions, "audio"> & {
audio: AsyncIterable<Buffer | Uint8Array>;
};
/**
* Only the `stt` slice is read here; it is typed loosely so this file does
* not have to import `NeurolinkCredentials` (which imports this file).
*/
export type STTCredentialsCarrier = {
stt?: STTCredentials;
};
/** What the decision model answered for one dictionary candidate. */
export type STTDecisionRecord = {
/** The mis-hearing found in the transcript. */
heard: string;
/** The dictionary term it may stand for. */
term: string;
choice: "term" | "literal";
/** Probability of the chosen reading, when the guard ran. */
probability?: number;
confidence?: number;
/** Why no decision was made ("decision model unavailable", "guard off", …). */
note?: string;
};
/** One candidate language and its engine-specific score (higher is better; scale is the engine's). */
export type STTLanguageScore = {
language: string;
score: number;
};
/** Which engines produced the result. */
export type STTEngineInfo = {
provider: string;
model?: string;
/** The fallback engine took the whole request (or an utterance, when streaming). */
fallbackUsed?: boolean;
/** The second-opinion engine that was reconciled into the text. */
secondOpinion?: string;
};
export type TranscribeTimings = {
transcribeMs: number;
secondOpinionMs?: number;
decideMs?: number;
rewriteMs?: number;
correctionMs?: number;
totalMs: number;
};
/** The result of `transcribe()`: an `STTResult` plus what the layers did to it. */
export type TranscribeResult = STTResult & {
/** Engine text before any correction. */
raw: string;
/** Present when the correction changed the text; `text` then holds the corrected version. */
corrected?: string;
/** `false` when the engine reported a low language-detection score. */
languageDetected?: boolean;
languageScores?: STTLanguageScore[];
decisions?: STTDecisionRecord[];
engine: STTEngineInfo;
timings: TranscribeTimings;
/** Human-readable trail of what happened ("dictionary 2/2", "rewrite timed out · kept text"). */
steps: string[];
};
export type TranscribeStreamEvent = {
type: "interim";
utterance: number;
/** Full current text of the utterance. */
text: string;
/** Prefix two consecutive passes agreed on (LocalAgreement). */
committed: string;
/** The rest, still subject to change. */
tail: string;
seconds: number;
latencyMs: number;
engine: string;
} | {
type: "language";
utterance: number;
language: string;
detected: boolean;
scores?: STTLanguageScore[];
} | {
type: "final";
utterance: number;
text: string;
segment: TranscriptionSegment;
words?: WordTiming[];
language?: string;
languageDetected?: boolean;
engine: string;
fallbackUsed?: boolean;
seconds: number;
} | {
/** Partial text of the streamed rewrite, in order. */
type: "correcting";
utterance: number;
text: string;
} | {
type: "corrected";
utterance: number;
raw: string;
text: string;
decisions: STTDecisionRecord[];
steps: string[];
timings: TranscribeTimings;
} | {
/** An utterance no engine could read; its audio length is reported so nothing disappears silently. */
type: "silence";
utterance: number;
seconds: number;
} | {
type: "error";
utterance?: number;
message: string;
/** `true` when the stream continues with the next utterance. */
recoverable: boolean;
};
/**
* One OpenAI-compatible `/audio/transcriptions` endpoint registered under a
* name of its own, so several self-hosted engines can be told apart — and
* used as each other's `fallback` or `secondOpinion`. `model` is the default
* `model` field sent to it.
*/
export type STTEndpointConfig = {
baseURL: string;
apiKey?: string;
model?: string;
timeoutMs?: number;
};
/** Credential slices for STT handlers; `credentials.stt.<key>` beats the env var. */
export type STTCredentials = {
/** Any OpenAI-compatible `/audio/transcriptions` endpoint (OpenAI, vLLM, LiteLLM, self-hosted wrappers). */
whisper?: {
apiKey?: string;
baseURL?: string;
timeoutMs?: number;
/** Default model name sent to the endpoint (`whisper-1` when omitted). */
model?: string;
};
/**
* Extra OpenAI-compatible endpoints, each registered as a provider under
* its key (`stt.endpoints.indic` → `provider: "indic"`). The env form is
* `NEUROLINK_STT_ENDPOINTS`, a JSON object of the same shape. A key that
* collides with a shipped provider name or alias is ignored with a warning.
*/
endpoints?: Record<string, STTEndpointConfig>;
deepgram?: {
apiKey?: string;
baseURL?: string;
};
elevenlabs?: {
apiKey?: string;
baseURL?: string;
timeoutMs?: number;
};
google?: {
apiKey?: string;
credentialsPath?: string;
};
azure?: {
apiKey?: string;
region?: string;
};
/** The built-in local engine: where its files live and whether they may be fetched. */
whistle?: {
modelDir?: string;
autoDownload?: boolean;
};
};
export type STTProviderCapabilities = {
/** `"native"`: the handler streams itself; `"chunked"`: the adapter streams a batch handler. */
streaming: "native" | "chunked";
diarization: boolean;
/** The engine reports a detected language (and, where it can, a confidence). */
languageDetect: boolean;
/** Accepts a context prompt for biasing. */
prompt: boolean;
wordTimestamps: boolean;
/** Runs on this machine; needs no key. */
local: boolean;
/** Languages the engine supports when it is not open-ended. */
languages?: readonly string[];
};
/**
* Metadata for one STT provider — the single source of truth for its name,
* aliases, credentials, env vars and capabilities. Order in the list is
* precedence for the default provider, exactly as with decision providers.
*/
export type STTProviderDescriptor = {
name: string;
aliases?: readonly string[];
label: string;
credentialsKey: keyof STTCredentials;
/** Env vars that configure it; the first present one makes it "configured". */
envVars: readonly string[];
baseUrlEnv?: string;
defaultModel?: string;
capabilities: STTProviderCapabilities;
};
/**
* What the correction layer needs from the SDK, injected by `NeuroLink` so the
* layer itself stays free of provider plumbing and can be exercised with
* stand-ins. `decide` is `tryDecide` (returns `null` on any failure);
* `rewrite` streams the text so far (cumulative, not deltas) through `onPartial`
* when the caller wants it.
*/
export type STTCorrectionDeps = {
decide: (opts: {
state: Record<string, unknown>;
questions: Record<string, unknown>;
timeoutMs?: number;
}) => Promise<{
answers: Record<string, unknown>;
} | null>;
rewrite: (opts: {
system: string;
user: string;
provider?: string;
model?: string;
maxTokens: number;
timeoutMs: number;
onPartial?: (text: string) => void;
}) => Promise<string>;
};
/** Input to the correction layer, independent of where the text came from. */
export type STTCorrectionInput = {
text: string;
/** Second-opinion transcript of the same audio, when one exists. */
secondOpinion?: string;
dictionary?: STTDictionaryEntry[];
options?: STTCorrectionOptions;
/** `false` when the primary engine reported a low language score: the second opinion then leads. */
languageDetected?: boolean;
onPartial?: (text: string) => void;
};
/** A dictionary alias found in a transcript: a candidate substitution for the guard to judge. */
export type STTDictionaryCandidate = {
entry: STTDictionaryEntry;
/** The alias as written in the dictionary (its match in the text may differ in case). */
heard: string;
};
/** What one rewrite call did; `text` is always usable (the input when the rewrite was not kept). */
export type STTRewriteOutcome = {
text: string;
status: "ok" | "timeout" | "error" | "empty" | "dropped";
ms: number;
/** Share of words the rewrite dropped, when `status` is `"dropped"`. */
dropRatio?: number;
/** Message of the error, when `status` is `"error"`. */
error?: string;
};
/** What a batched line rewrite did; `lines` has exactly as many entries as the input. */
export type STTRewriteLinesOutcome = {
lines: string[];
/** Batches whose output was used, of all batches sent. */
batchesOk: number;
batches: number;
/** Lines whose rewrite was discarded for dropping words. */
linesKept: number;
ms: number;
};
export type STTCorrectionOutput = {
text: string;
decisions: STTDecisionRecord[];
steps: string[];
timings: Pick<TranscribeTimings, "decideMs" | "rewriteMs" | "correctionMs">;
};
/**
* Everything the transcribe orchestration (batch and stream) needs from the
* SDK, bound by `NeuroLink` so the orchestration modules stay testable.
*/
export type TranscribeDeps = STTCorrectionDeps & {
/** `STTProcessor.transcribe` with the request's credentials already applied. */
transcribe: (audio: Buffer, provider: string, options: STTOptions) => Promise<STTResult>;
/** Resolves `undefined`, `""` or an alias to a registered provider name, or throws a typed error. */
resolveProvider: (name?: string) => string;
getHandler: (name: string) => STTHandler | undefined;
};
/** Resolved endpointer tuning: `STTStreamingOptions` with every default applied, plus the input rate. */
export type STTEndpointerConfig = {
/** Sample rate of the samples given to `push()`. */
inputRate: number;
endSilenceMs: number;
softCutSeconds: number;
maxUtteranceSeconds: number;
onsetRms: number;
prerollMs: number;
minSpeechMs: number;
};
/** One utterance as the energy gate builds it, at the input rate. */
export type STTEndpointerUtterance = {
/** 1-based, in order of onset. A dropped cough consumes an id, so ids can skip. */
id: number;
/** Offset of the first sample (pre-roll included) from the start of the stream. */
startSeconds: number;
/** 20 ms blocks, pre-roll included. */
blocks: Float32Array[];
/** RMS of each block, parallel to `blocks`. */
rms: number[];
/** Total samples in `blocks`. */
length: number;
speechMs: number;
silenceMs: number;
/** Set by the caller once a pass produced words: such an utterance is never dropped as a cough. */
heard: boolean;
};
export type STTEndpointerEvent = {
type: "start";
utterance: STTEndpointerUtterance;
} | {
type: "end";
utterance: STTEndpointerUtterance;
reason: "silence" | "softCut" | "hardCut" | "flush";
}
/** Too little speech and nothing heard: a cough or a click. */
| {
type: "drop";
utterance: STTEndpointerUtterance;
};
/** LocalAgreement bookkeeping for one utterance. */
export type STTLocalAgreementState = {
/** Words of the previous pass. */
prevWords: string[];
/** The committed words themselves; a later pass never rewrites them. */
committedWords: string[];
/** `committedWords.length`; never shrinks while the state lives. */
committedN: number;
};
export type STTLocalAgreementUpdate = {
words: string[];
committed: string;
tail: string;
};
/** Which engine of the adapter a pass runs on; the orchestration maps roles to providers. */
export type STTStreamEngineRole = "primary" | "fallback";
/** What the adapter tells `run` about a pass. */
export type STTChunkedPassOptions = {
utterance: number;
final: boolean;
engine: STTStreamEngineRole;
/** Audio length of the utterance so far. */
seconds: number;
};
/** One engine pass over a whole utterance, 16 kHz mono float samples in. */
export type STTChunkedRun = (pcm16k: Float32Array, options: STTChunkedPassOptions) => Promise<STTResult>;
/** Options of `chunkedTranscribeStream`: the streaming tuning plus engine labels and hooks. */
export type STTChunkedStreamOptions = STTStreamingOptions & {
/** Label of the primary engine in events. Default "primary". */
engineLabel?: string;
/** Language takeover: hand an utterance the primary cannot read to the `fallback` role. */
fallback?: {
label: string;
when?: STTFallbackOptions["when"];
};
/**
* Called once per utterance, synchronously, when it ends and before its
* final pass is queued, with the utterance audio at 16 kHz and the role
* that will run the final — so a second opinion can run in parallel.
*/
onUtteranceEnd?: (pcm16k: Float32Array, utterance: number, engine: STTStreamEngineRole) => void;
};