UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

238 lines (237 loc) 10.1 kB
import { b as parseFiniteNumber } from "./number-coercion-CLj0HTDM.js"; import { a as asOptionalRecord } from "./record-coerce-DItp3I4t.js"; import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { n as parseBooleanValue } from "./boolean-DmBL0YJK.js"; import { m as normalizeResolvedSecretInputString } from "./types.secrets-kC0nOetj.js"; import "./string-coerce-runtime-GQa0ehRA.js"; import "./secret-input-dpVVFmLG.js"; import { n as DEFAULT_DEEPGRAM_AUDIO_MODEL, t as DEFAULT_DEEPGRAM_AUDIO_BASE_URL } from "./audio-CVCrNvKA.js"; //#region extensions/deepgram/realtime-transcription-provider-factory.ts const DEEPGRAM_REALTIME_DEFAULT_SAMPLE_RATE = 8e3; const DEEPGRAM_REALTIME_DEFAULT_ENCODING = "mulaw"; const DEEPGRAM_REALTIME_DEFAULT_ENDPOINTING_MS = 800; const DEEPGRAM_REALTIME_CONNECT_TIMEOUT_MS = 1e4; const DEEPGRAM_REALTIME_CLOSE_TIMEOUT_MS = 5e3; const DEEPGRAM_REALTIME_MAX_RECONNECT_ATTEMPTS = 5; const DEEPGRAM_REALTIME_RECONNECT_DELAY_MS = 1e3; const DEEPGRAM_REALTIME_MAX_QUEUED_BYTES = 2097152; const DEEPGRAM_REALTIME_MAX_RETAINED_TRANSCRIPT_BYTES = 262144; const DEEPGRAM_REALTIME_FINALIZE_FALLBACK_MS = 4900; function readNestedDeepgramConfig(rawConfig) { const raw = asOptionalRecord(rawConfig); const providers = asOptionalRecord(raw?.providers); return asOptionalRecord(providers?.deepgram ?? raw?.deepgram ?? raw) ?? {}; } function normalizeDeepgramEncoding(value) { const normalized = normalizeOptionalString(value)?.toLowerCase(); if (!normalized) return; if (normalized === "pcm" || normalized === "pcm_s16le" || normalized === "linear16") return "linear16"; if (normalized === "ulaw" || normalized === "g711_ulaw" || normalized === "g711-mulaw") return "mulaw"; if (normalized === "g711_alaw" || normalized === "g711-alaw") return "alaw"; if (normalized === "mulaw" || normalized === "alaw") return normalized; throw new Error(`Invalid Deepgram realtime transcription encoding: ${normalized}`); } function normalizeDeepgramRealtimeBaseUrl(value) { const resolved = normalizeOptionalString(value ?? process.env.DEEPGRAM_BASE_URL); if (!resolved) return DEFAULT_DEEPGRAM_AUDIO_BASE_URL; let parsed; try { parsed = new URL(resolved); } catch { throw new Error("Invalid Deepgram baseUrl: value is not a valid URL"); } const { protocol } = parsed; if (protocol !== "http:" && protocol !== "https:" && protocol !== "ws:" && protocol !== "wss:") throw new Error(`Invalid Deepgram baseUrl: unsupported scheme "${protocol}" (expected http, https, ws, or wss)`); return resolved; } function toDeepgramRealtimeWsUrl(config) { const url = new URL(normalizeDeepgramRealtimeBaseUrl(config.baseUrl)); if (url.protocol === "http:") url.protocol = "ws:"; else if (url.protocol === "https:") url.protocol = "wss:"; url.pathname = `${url.pathname.replace(/\/+$/, "")}/listen`; url.searchParams.set("model", config.model); url.searchParams.set("encoding", config.encoding); url.searchParams.set("sample_rate", String(config.sampleRate)); url.searchParams.set("channels", "1"); url.searchParams.set("interim_results", String(config.interimResults)); url.searchParams.set("endpointing", String(config.endpointingMs)); if (config.language) url.searchParams.set("language", config.language); return url.toString(); } function normalizeProviderConfig(config) { const raw = readNestedDeepgramConfig(config); return { apiKey: normalizeResolvedSecretInputString({ value: raw.apiKey, path: "plugins.entries.voice-call.config.streaming.providers.deepgram.apiKey" }), baseUrl: normalizeOptionalString(raw.baseUrl), model: normalizeOptionalString(raw.model ?? raw.sttModel), language: normalizeOptionalString(raw.language), sampleRate: parseFiniteNumber(raw.sampleRate ?? raw.sample_rate), encoding: normalizeDeepgramEncoding(raw.encoding), interimResults: parseBooleanValue(raw.interimResults ?? raw.interim_results), endpointingMs: parseFiniteNumber(raw.endpointingMs ?? raw.endpointing ?? raw.silenceDurationMs) }; } function readErrorDetail(value) { if (typeof value === "string") return value; const record = asOptionalRecord(value); const message = normalizeOptionalString(record?.message); const code = normalizeOptionalString(record?.code); return message ?? code ?? "Deepgram realtime transcription error"; } function readTranscriptText(event) { return normalizeOptionalString(event.channel?.alternatives?.[0]?.transcript); } function createDeepgramRealtimeTranscriptionSession(config, createRealtimeTranscriptionWebSocketSession) { let speechStarted = false; let finalizedTranscript = ""; let pendingPartial = ""; let finalizeRequested = false; let finalizeFallbackFired = false; let finalizeFallbackTimer; let openedOnce = false; const collapseWhitespace = (value) => value.replace(/\s+/g, " ").trim(); const joinTranscript = (left, right) => collapseWhitespace(left && right ? `${left} ${right}` : left || right); const clearFinalizeFallback = () => { if (finalizeFallbackTimer) { clearTimeout(finalizeFallbackTimer); finalizeFallbackTimer = void 0; } }; const clearTurn = () => { clearFinalizeFallback(); finalizedTranscript = ""; pendingPartial = ""; speechStarted = false; }; const updateTurn = (nextFinalized, nextPartial, transport) => { if (Buffer.byteLength(nextFinalized, "utf8") + Buffer.byteLength(nextPartial, "utf8") > DEEPGRAM_REALTIME_MAX_RETAINED_TRANSCRIPT_BYTES) { clearTurn(); config.onError?.(/* @__PURE__ */ new Error(`Deepgram realtime retained transcript exceeded ${DEEPGRAM_REALTIME_MAX_RETAINED_TRANSCRIPT_BYTES} bytes`)); transport.closeNow(); return false; } finalizedTranscript = nextFinalized; pendingPartial = nextPartial; return true; }; const flushTurn = () => { const full = joinTranscript(finalizedTranscript, pendingPartial); clearTurn(); if (full) config.onTranscript?.(full); }; const flushFinalizedTurn = () => { const full = collapseWhitespace(finalizedTranscript); clearTurn(); if (full) config.onTranscript?.(full); }; const handleEvent = (event, transport) => { switch (event.type) { case "Results": { if (finalizeFallbackFired) return; const text = readTranscriptText(event); if (text && !speechStarted) { speechStarted = true; config.onSpeechStart?.(); } if (event.speech_final || event.from_finalize) { const nextFinalized = text ? joinTranscript(finalizedTranscript, text) : finalizedTranscript; if (!updateTurn(nextFinalized, "", transport)) return; flushTurn(); return; } if (!text) return; if (event.is_final) { const nextFinalized = joinTranscript(finalizedTranscript, text); if (!updateTurn(nextFinalized, "", transport)) return; config.onPartial?.(nextFinalized); } else { if (!updateTurn(finalizedTranscript, text, transport)) return; config.onPartial?.(joinTranscript(finalizedTranscript, text)); } return; } case "SpeechStarted": speechStarted = true; config.onSpeechStart?.(); return; case "Error": case "error": config.onError?.(new Error(readErrorDetail(event.error ?? event.message))); } }; return createRealtimeTranscriptionWebSocketSession({ providerId: "deepgram", callbacks: config, url: () => toDeepgramRealtimeWsUrl(config), headers: { Authorization: `Token ${config.apiKey}` }, readyOnOpen: true, connectTimeoutMs: DEEPGRAM_REALTIME_CONNECT_TIMEOUT_MS, closeTimeoutMs: DEEPGRAM_REALTIME_CLOSE_TIMEOUT_MS, maxReconnectAttempts: DEEPGRAM_REALTIME_MAX_RECONNECT_ATTEMPTS, reconnectDelayMs: DEEPGRAM_REALTIME_RECONNECT_DELAY_MS, maxQueuedBytes: DEEPGRAM_REALTIME_MAX_QUEUED_BYTES, connectTimeoutMessage: "Deepgram realtime transcription connection timeout", connectClosedBeforeReadyMessage: "Deepgram realtime transcription connection closed before ready", reconnectLimitMessage: "Deepgram realtime transcription reconnect limit reached", onOpen: () => { if (openedOnce) flushFinalizedTurn(); else { openedOnce = true; clearTurn(); } finalizeRequested = false; finalizeFallbackFired = false; }, sendAudio: (audio, transport) => { transport.sendBinary(audio); }, onClose: (transport) => { if (finalizeRequested) return; finalizeRequested = true; if (finalizedTranscript) finalizeFallbackTimer = setTimeout(() => { finalizeFallbackTimer = void 0; finalizeFallbackFired = true; try { flushFinalizedTurn(); } catch (error) { try { config.onError?.(error instanceof Error ? error : new Error(String(error))); } catch {} } }, DEEPGRAM_REALTIME_FINALIZE_FALLBACK_MS); transport.sendJson({ type: "Finalize" }); }, onMessage: (event, transport) => handleEvent(event, transport) }); } function buildDeepgramRealtimeTranscriptionProvider({ createRealtimeTranscriptionWebSocketSession }) { return { id: "deepgram", label: "Deepgram Realtime Transcription", aliases: ["deepgram-realtime", "nova-3-streaming"], defaultModel: DEFAULT_DEEPGRAM_AUDIO_MODEL, autoSelectOrder: 35, resolveConfig: ({ rawConfig }) => normalizeProviderConfig(rawConfig), isConfigured: ({ providerConfig }) => Boolean(normalizeProviderConfig(providerConfig).apiKey || process.env.DEEPGRAM_API_KEY), createSession: (req) => { const config = normalizeProviderConfig(req.providerConfig); const apiKey = config.apiKey || process.env.DEEPGRAM_API_KEY; if (!apiKey) throw new Error("Deepgram API key missing"); return createDeepgramRealtimeTranscriptionSession({ ...req, apiKey, baseUrl: normalizeDeepgramRealtimeBaseUrl(config.baseUrl), model: config.model ?? "nova-3", sampleRate: config.sampleRate ?? DEEPGRAM_REALTIME_DEFAULT_SAMPLE_RATE, encoding: config.encoding ?? DEEPGRAM_REALTIME_DEFAULT_ENCODING, interimResults: config.interimResults ?? true, endpointingMs: config.endpointingMs ?? DEEPGRAM_REALTIME_DEFAULT_ENDPOINTING_MS, language: config.language }, createRealtimeTranscriptionWebSocketSession); } }; } //#endregion export { buildDeepgramRealtimeTranscriptionProvider as t };