@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
190 lines (189 loc) • 10.6 kB
JavaScript
/**
* Voice Module - Unified Voice/Speech Integration for NeuroLink
*
* Provides TTS (Text-to-Speech), STT (Speech-to-Text), and
* Realtime Voice capabilities across multiple providers.
*
* Use TTSProcessor (src/lib/utils/ttsProcessor.ts) for TTS.
* Use STTProcessor (src/lib/utils/sttProcessor.ts) for STT.
* Use RealtimeProcessor for realtime voice sessions.
*
* Importing this module does NOT register any handlers as a side effect.
* Call `registerDefaultTTSHandlers()` / `registerDefaultSTTHandlers()` /
* `registerDefaultRealtimeHandlers()` explicitly (or go through
* `ProviderRegistry.registerAllProviders()`, which every documented
* `NeuroLink` entry point already calls) to register every shipped handler
* whose backing API key is present in `process.env`. Registration is
* idempotent and silently skipped on failure.
*
* @module voice
*/
import { MEDIA_HANDLER_CATALOG } from "../factories/mediaHandlerCatalog.js";
import { logger } from "../utils/logger.js";
import { STTProcessor } from "../utils/sttProcessor.js";
import { TTSProcessor } from "../utils/ttsProcessor.js";
import { GoogleTTSHandler } from "../adapters/tts/googleTTSHandler.js";
import { RealtimeProcessor } from "./RealtimeVoiceAPI.js";
// ============================================================================
// ERROR CODES AND CONSTANTS
// ============================================================================
export { AUDIO_FORMAT_DETAILS, DEFAULT_REALTIME_CONFIG, DEFAULT_STT_OPTIONS,
// Type guards
isSTTResult, isTranscriptionSegment, isValidRealtimeConfig, isValidSTTOptions, REALTIME_ERROR_CODES, STT_ERROR_CODES, VOICE_ERROR_CODES, } from "../types/index.js";
// ============================================================================
// ERRORS
// ============================================================================
export { RealtimeError, STTError, VoiceError } from "./errors.js";
// ============================================================================
// REALTIME VOICE API
// ============================================================================
export { BaseRealtimeHandler, RealtimeProcessor } from "./RealtimeVoiceAPI.js";
// ============================================================================
// AUDIO UTILITIES
// ============================================================================
export { AUDIO_SIGNATURES, calculateDuration, convertAudioFormat, createPcmBuffer, createWavFile, createWavHeader, detectAudioFormat, extractPcmSamples, getFileExtension, getMimeType, MIME_TYPES, normalizeAudio, resamplePcm, splitIntoChunks, } from "./audio-utils.js";
// ============================================================================
// STREAM HANDLER
// ============================================================================
export { asyncIterableToStream, ChunkedAudioStream, StreamHandler, StreamMerger, StreamSplitter, streamToAsyncIterable, } from "./stream-handler.js";
// ============================================================================
// TTS PROVIDERS
// ============================================================================
export { GoogleTTSHandler } from "../adapters/tts/googleTTSHandler.js";
export { AzureTTS, AzureTTS as AzureTTSHandler } from "./providers/AzureTTS.js";
export { SixtyDBTTS, SixtyDBTTS as SixtyDBTTSHandler, } from "./providers/SixtyDBTTS.js";
export { CartesiaTTS, CartesiaTTS as CartesiaTTSHandler, } from "./providers/CartesiaTTS.js";
export { ElevenLabsTTS, ElevenLabsTTS as ElevenLabsTTSHandler, } from "./providers/ElevenLabsTTS.js";
export { FishAudioTTS, FishAudioTTS as FishAudioTTSHandler, } from "./providers/FishAudioTTS.js";
export { OpenAITTS, OpenAITTS as OpenAITTSHandler, } from "./providers/OpenAITTS.js";
// ============================================================================
// STT PROVIDERS
// ============================================================================
export { AzureSTT, AzureSTT as AzureSTTHandler } from "./providers/AzureSTT.js";
export { DeepgramSTT, DeepgramSTT as DeepgramSTTHandler, } from "./providers/DeepgramSTT.js";
export { GoogleSTT, GoogleSTT as GoogleSTTHandler, } from "./providers/GoogleSTT.js";
export { OpenAISTT, OpenAISTTHandler, WhisperSTT, WhisperSTTHandler, } from "./providers/OpenAISTT.js";
// ============================================================================
// REALTIME PROVIDERS
// ============================================================================
export { GeminiLive, GeminiLive as GeminiLiveHandler, } from "./providers/GeminiLive.js";
export { OpenAIRealtime, OpenAIRealtime as OpenAIRealtimeHandler, } from "./providers/OpenAIRealtime.js";
// ============================================================================
// AUTO-REGISTRATION
// ============================================================================
import { AzureTTS } from "./providers/AzureTTS.js";
import { CartesiaTTS } from "./providers/CartesiaTTS.js";
import { ElevenLabsTTS } from "./providers/ElevenLabsTTS.js";
import { FishAudioTTS } from "./providers/FishAudioTTS.js";
import { SixtyDBTTS } from "./providers/SixtyDBTTS.js";
import { OpenAITTS } from "./providers/OpenAITTS.js";
import { AzureSTT } from "./providers/AzureSTT.js";
import { DeepgramSTT } from "./providers/DeepgramSTT.js";
import { GoogleSTT } from "./providers/GoogleSTT.js";
import { OpenAISTT } from "./providers/OpenAISTT.js";
import { GeminiLive } from "./providers/GeminiLive.js";
import { OpenAIRealtime } from "./providers/OpenAIRealtime.js";
// Provider names + aliases are the Task-8 catalog's job — only the factory
// (which needs the imported handler class) stays local to this module.
const TTS_HANDLER_FACTORIES = {
// Google TTS doubles as both the AI Studio and Vertex TTS handler.
"google-ai": () => new GoogleTTSHandler(),
"openai-tts": () => new OpenAITTS(),
elevenlabs: () => new ElevenLabsTTS(),
"azure-tts": () => new AzureTTS(),
"fish-audio": () => new FishAudioTTS(),
sixtydb: () => new SixtyDBTTS(),
cartesia: () => new CartesiaTTS(),
};
const TTS_HANDLER_CANDIDATES = MEDIA_HANDLER_CATALOG.filter((entry) => entry.kind === "tts").map((entry) => {
const factory = TTS_HANDLER_FACTORIES[entry.name];
if (!factory) {
throw new Error(`[voice/tts] no handler factory for catalog entry "${entry.name}"`);
}
return { name: entry.name, aliases: entry.aliases, factory };
});
const STT_HANDLER_FACTORIES = {
whisper: () => new OpenAISTT(),
deepgram: () => new DeepgramSTT(),
"google-stt": () => new GoogleSTT(),
"azure-stt": () => new AzureSTT(),
};
const STT_HANDLER_CANDIDATES = MEDIA_HANDLER_CATALOG.filter((entry) => entry.kind === "stt").map((entry) => {
const factory = STT_HANDLER_FACTORIES[entry.name];
if (!factory) {
throw new Error(`[voice/stt] no handler factory for catalog entry "${entry.name}"`);
}
return { name: entry.name, aliases: entry.aliases, factory };
});
const REALTIME_HANDLER_FACTORIES = {
"openai-realtime": () => new OpenAIRealtime(),
"gemini-live": () => new GeminiLive(),
};
const REALTIME_HANDLER_CANDIDATES = MEDIA_HANDLER_CATALOG.filter((entry) => entry.kind === "realtime").map((entry) => {
const factory = REALTIME_HANDLER_FACTORIES[entry.name];
if (!factory) {
throw new Error(`[voice/realtime] no handler factory for catalog entry "${entry.name}"`);
}
return { name: entry.name, aliases: entry.aliases, factory };
});
function registerCandidates(candidates, supports, getRegistered, register, scope, requireConfigured) {
for (const { name, aliases, factory } of candidates) {
// Compute missingName / missingAliases separately so a manually-
// registered primary name doesn't block alias backfill. Important for
// BC: existing callers that register e.g. "elevenlabs" should still
// see "elevenlabs-tts" wired up by this loop.
const missingName = !supports(name);
const missingAliases = (aliases ?? []).filter((alias) => !supports(alias));
if (!missingName && missingAliases.length === 0) {
continue;
}
try {
// If the primary is already registered, reuse that exact handler
// instance for any alias backfill — wiring an alias to a *different*
// factory-fresh instance would silently diverge from the canonical
// primary's behavior (different config, different credentials).
// Only call factory() when we actually need to register the primary.
let handler;
if (!missingName) {
handler = getRegistered(name);
}
if (!handler) {
handler = factory();
if (requireConfigured && !handler.isConfigured()) {
continue;
}
}
if (missingName) {
register(name, handler);
}
for (const alias of missingAliases) {
register(alias, handler);
}
}
catch (err) {
logger.debug(`[${scope}] ${name} auto-registration skipped: ${err instanceof Error ? err.message : String(err)}`);
}
}
}
/**
* Register every shipped TTS handler whose backing credentials are
* present in the environment. Safe to call multiple times.
*/
export function registerDefaultTTSHandlers() {
registerCandidates(TTS_HANDLER_CANDIDATES, (name) => TTSProcessor.supports(name), (name) => TTSProcessor.getHandler(name), (name, handler) => TTSProcessor.registerHandler(name, handler), "voice/tts", true);
}
/**
* Register every shipped STT handler whose backing credentials are
* present in the environment. Safe to call multiple times.
*/
export function registerDefaultSTTHandlers() {
registerCandidates(STT_HANDLER_CANDIDATES, (name) => STTProcessor.supports(name), (name) => STTProcessor.getHandler(name), (name, handler) => STTProcessor.registerHandler(name, handler), "voice/stt", true);
}
/**
* Register every shipped Realtime handler. Realtime handlers don't gate
* registration on isConfigured() because session-time API keys can be
* supplied per-call; missing creds surface when `connect()` is invoked.
*/
export function registerDefaultRealtimeHandlers() {
registerCandidates(REALTIME_HANDLER_CANDIDATES, (name) => RealtimeProcessor.supports(name), (name) => RealtimeProcessor.getHandler(name), (name, handler) => RealtimeProcessor.registerHandler(name, handler), "voice/realtime", false);
}