UNPKG

react-native-deepgram

Version:

React Native SDK for Deepgram's AI-powered speech-to-text, real-time transcription, and text intelligence APIs. Supports live audio streaming, file transcription, sentiment analysis, and topic detection for iOS and Android.

366 lines (359 loc) 14 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useDeepgramTextToSpeech = useDeepgramTextToSpeech; var _buffer = require("buffer"); var _react = require("react"); var _reactNative = require("react-native"); var _index = require("./constants/index.js"); var _index2 = require("./helpers/index.js"); if (!globalThis.Buffer) globalThis.Buffer = _buffer.Buffer; const DEFAULT_TTS_MODEL = 'aura-2-asteria-en'; const DEFAULT_TTS_SAMPLE_RATE = 24_000; const DEFAULT_TTS_HTTP_ENCODING = 'linear16'; const DEFAULT_TTS_STREAM_ENCODING = 'linear16'; const DEFAULT_TTS_CONTAINER = 'none'; const DEFAULT_TTS_MP3_BITRATE = 48_000; const normalizeStreamEncoding = encoding => { switch (encoding) { case 'linear16': case 'mulaw': case 'alaw': return encoding; default: return DEFAULT_TTS_STREAM_ENCODING; } }; const ensureQueryParam = (params, key, value) => { if (value == null) return; if (Object.prototype.hasOwnProperty.call(params, key) && params[key] != null) { return; } params[key] = value; }; const isMetadataMessage = message => message.type === 'Metadata' && typeof message.request_id === 'string'; const isFlushedMessage = message => message.type === 'Flushed' && typeof message.sequence_id === 'number'; const isClearedMessage = message => message.type === 'Cleared' && typeof message.sequence_id === 'number'; const isWarningMessage = message => message.type === 'Warning' && typeof message.description === 'string' && typeof message.code === 'string'; const asErrorMessage = message => message.type === 'Error' ? message : null; /* ──────────────────────────────────────────────────────────── Wrap the unified native module ──────────────────────────────────────────────────────────── */ const Deepgram = (() => { /** Throws if the native side isn’t linked */ function getModule() { const mod = _reactNative.NativeModules.Deepgram; if (!mod) { throw new Error('Deepgram native module not found. ' + 'Did you rebuild the app after installing / adding the module?'); } return mod; } return { startPlayer: (sr = 16_000, ch = 1) => getModule().startPlayer(sr, ch), setAudioConfig: (sr = 16_000, ch = 1) => getModule().setAudioConfig(sr, ch), feedAudio: chunk => { const u8 = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk); getModule().feedAudio(_buffer.Buffer.from(u8).toString('base64')); }, playAudioChunk: chunk => { const u8 = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk); return getModule().playAudioChunk(_buffer.Buffer.from(u8).toString('base64')); }, stopPlayer: () => getModule().stopPlayer() }; })(); /* ──────────────────────────────────────────────────────────── Hook: useDeepgramTextToSpeech ──────────────────────────────────────────────────────────── */ function useDeepgramTextToSpeech({ onBeforeSynthesize = () => {}, onSynthesizeSuccess = () => {}, onSynthesizeError = () => {}, onBeforeStream = () => {}, onStreamStart = () => {}, onAudioChunk = () => {}, onStreamError = () => {}, onStreamEnd = () => {}, onStreamMetadata = () => {}, onStreamFlushed = () => {}, onStreamCleared = () => {}, onStreamWarning = () => {}, options = {} } = {}) { const resolvedHttpOptions = (0, _react.useMemo)(() => { const encoding = options.http?.encoding ?? options.encoding ?? DEFAULT_TTS_HTTP_ENCODING; const model = options.http?.model ?? options.model ?? DEFAULT_TTS_MODEL; const derivedSampleRate = (() => { const explicit = options.http?.sampleRate ?? options.sampleRate; if (explicit != null) return explicit; if (encoding === 'linear16') return DEFAULT_TTS_SAMPLE_RATE; if (encoding === 'mulaw' || encoding === 'alaw') return 8000; return undefined; })(); const container = (() => { const provided = options.http?.container ?? options.container; if (provided) return provided; if (encoding === 'opus') return 'ogg'; if (encoding === 'linear16' || encoding === 'mulaw' || encoding === 'alaw') { return DEFAULT_TTS_CONTAINER; } return undefined; })(); const bitRate = (() => { const provided = options.http?.bitRate ?? options.bitRate; if (provided != null) return provided; if (encoding === 'mp3') return DEFAULT_TTS_MP3_BITRATE; return undefined; })(); return { model, sampleRate: derivedSampleRate, encoding, container, format: options.http?.format ?? options.format, bitRate, callback: options.http?.callback ?? options.callback, callbackMethod: options.http?.callbackMethod ?? options.callbackMethod, mipOptOut: options.http?.mipOptOut ?? options.mipOptOut, queryParams: { ...(options.queryParams ?? {}), ...(options.http?.queryParams ?? {}) } }; }, [options]); const resolvedStreamOptions = (0, _react.useMemo)(() => { const model = options.stream?.model ?? options.model ?? DEFAULT_TTS_MODEL; const encoding = normalizeStreamEncoding(options.stream?.encoding ?? options.encoding); const sampleRate = (() => { const explicit = options.stream?.sampleRate ?? options.sampleRate; if (explicit != null) return explicit; if (encoding === 'mulaw' || encoding === 'alaw') return 8000; return DEFAULT_TTS_SAMPLE_RATE; })(); return { model, sampleRate, encoding, mipOptOut: options.stream?.mipOptOut ?? options.mipOptOut, queryParams: { ...(options.queryParams ?? {}), ...(options.stream?.queryParams ?? {}) }, autoFlush: options.stream?.autoFlush ?? true }; }, [options]); /* ---------- HTTP (one-shot synth) ---------- */ const abortCtrl = (0, _react.useRef)(null); const synthesize = (0, _react.useCallback)(async text => { onBeforeSynthesize(); try { const apiKey = globalThis.__DEEPGRAM_API_KEY__; if (!apiKey) throw new Error('Deepgram API key missing'); if (!text?.trim()) throw new Error('Text is empty'); const httpParams = { ...resolvedHttpOptions.queryParams }; ensureQueryParam(httpParams, 'model', resolvedHttpOptions.model); ensureQueryParam(httpParams, 'encoding', resolvedHttpOptions.encoding); ensureQueryParam(httpParams, 'sample_rate', resolvedHttpOptions.sampleRate); ensureQueryParam(httpParams, 'container', resolvedHttpOptions.container); ensureQueryParam(httpParams, 'format', resolvedHttpOptions.format); ensureQueryParam(httpParams, 'bit_rate', resolvedHttpOptions.bitRate); ensureQueryParam(httpParams, 'callback', resolvedHttpOptions.callback); ensureQueryParam(httpParams, 'callback_method', resolvedHttpOptions.callbackMethod); ensureQueryParam(httpParams, 'mip_opt_out', resolvedHttpOptions.mipOptOut); const params = (0, _index2.buildParams)(httpParams); const url = params ? `${_index.DEEPGRAM_BASEURL}/speak?${params}` : `${_index.DEEPGRAM_BASEURL}/speak`; abortCtrl.current?.abort(); abortCtrl.current = new AbortController(); const res = await fetch(url, { method: 'POST', headers: { 'Authorization': `Token ${apiKey}`, 'Content-Type': 'application/json', 'Accept': 'application/octet-stream' }, body: JSON.stringify({ text }), signal: abortCtrl.current.signal }); if (!res.ok) { const errText = await res.text(); throw new Error(`HTTP ${res.status}: ${errText}`); } const audio = await res.arrayBuffer(); await Deepgram.playAudioChunk(audio); onSynthesizeSuccess(audio); return audio; } catch (err) { if (err?.name === 'AbortError') { throw err; } onSynthesizeError(err); throw err; } }, [onBeforeSynthesize, onSynthesizeSuccess, onSynthesizeError, resolvedHttpOptions]); /* ---------- WebSocket (streaming synth) ---------- */ const ws = (0, _react.useRef)(null); const closeStream = () => { ws.current?.close(1000, 'cleanup'); ws.current = null; Deepgram.stopPlayer(); }; const sendMessage = (0, _react.useCallback)(message => { if (!ws.current || ws.current.readyState !== WebSocket.OPEN) { return false; } try { ws.current.send(JSON.stringify(message)); return true; } catch (err) { onStreamError(err); return false; } }, [onStreamError]); const flushStream = (0, _react.useCallback)(() => sendMessage({ type: 'Flush' }), [sendMessage]); const clearStream = (0, _react.useCallback)(() => sendMessage({ type: 'Clear' }), [sendMessage]); const closeStreamGracefully = (0, _react.useCallback)(() => sendMessage({ type: 'Close' }), [sendMessage]); const sendText = (0, _react.useCallback)((text, config) => { if (!ws.current || ws.current.readyState !== WebSocket.OPEN) { return false; } const trimmed = text?.trim(); if (!trimmed) { return false; } const didSend = sendMessage({ type: 'Text', text: trimmed, ...(config?.sequenceId != null ? { sequence_id: config.sequenceId } : {}) }); const shouldFlush = config?.flush ?? resolvedStreamOptions.autoFlush ?? true; if (didSend && shouldFlush) { flushStream(); } return didSend; }, [flushStream, resolvedStreamOptions.autoFlush, sendMessage]); const startStreaming = (0, _react.useCallback)(async text => { onBeforeStream(); try { const apiKey = globalThis.__DEEPGRAM_API_KEY__; if (!apiKey) throw new Error('Deepgram API key missing'); if (!text?.trim()) throw new Error('Text is empty'); const wsParams = { ...resolvedStreamOptions.queryParams }; ensureQueryParam(wsParams, 'model', resolvedStreamOptions.model); ensureQueryParam(wsParams, 'encoding', resolvedStreamOptions.encoding); ensureQueryParam(wsParams, 'sample_rate', resolvedStreamOptions.sampleRate); ensureQueryParam(wsParams, 'mip_opt_out', resolvedStreamOptions.mipOptOut); const wsParamString = (0, _index2.buildParams)(wsParams); const url = wsParamString ? `${_index.DEEPGRAM_BASEWSS}/speak?${wsParamString}` : `${_index.DEEPGRAM_BASEWSS}/speak`; ws.current = new WebSocket(url, undefined, { headers: { Authorization: `Token ${apiKey}` } }); // Ensure WebSocket receives binary data as ArrayBuffer ws.current.binaryType = 'arraybuffer'; ws.current.onopen = () => { Deepgram.startPlayer(Number(resolvedStreamOptions.sampleRate) || DEFAULT_TTS_SAMPLE_RATE, 1); sendText(text); onStreamStart(); }; ws.current.onmessage = ev => { if (ev.data instanceof ArrayBuffer) { Deepgram.feedAudio(ev.data); onAudioChunk(ev.data); } else if (ev.data instanceof Blob) { ev.data.arrayBuffer().then(buffer => { Deepgram.feedAudio(buffer); onAudioChunk(buffer); }); } else if (typeof ev.data === 'string') { try { const message = JSON.parse(ev.data); switch (message.type) { case 'Metadata': if (isMetadataMessage(message)) { onStreamMetadata(message); } break; case 'Flushed': if (isFlushedMessage(message)) { onStreamFlushed(message); } break; case 'Cleared': if (isClearedMessage(message)) { onStreamCleared(message); } break; case 'Warning': if (isWarningMessage(message)) { onStreamWarning(message); } break; case 'Error': { const err = asErrorMessage(message); const description = err && typeof err.description === 'string' ? err.description : undefined; const code = err && typeof err.code === 'string' ? err.code : undefined; onStreamError(new Error(description ?? code ?? 'TTS error')); break; } default: // Ignore other informational messages. break; } } catch { // Ignore non-JSON string messages } } }; ws.current.onerror = onStreamError; ws.current.onclose = () => { onStreamEnd(); closeStream(); }; } catch (err) { onStreamError(err); closeStream(); throw err; } }, [onBeforeStream, onStreamStart, onAudioChunk, onStreamError, onStreamEnd, onStreamMetadata, onStreamFlushed, onStreamCleared, onStreamWarning, resolvedStreamOptions, sendText]); const stopStreaming = (0, _react.useCallback)(() => { try { closeStream(); onStreamEnd(); } catch (err) { onStreamError(err); } }, [onStreamEnd, onStreamError]); /* ---------- cleanup on unmount ---------- */ (0, _react.useEffect)(() => () => { abortCtrl.current?.abort(); closeStream(); }, []); return { synthesize, startStreaming, sendMessage, sendText, flushStream, clearStream, closeStreamGracefully, stopStreaming }; } //# sourceMappingURL=useDeepgramTextToSpeech.js.map