openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
238 lines (237 loc) • 10.1 kB
JavaScript
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 };