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.
447 lines (440 loc) • 16 kB
JavaScript
;
import { Buffer } from 'buffer';
if (!globalThis.Buffer) globalThis.Buffer = Buffer;
import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
import { NativeModules } from 'react-native';
import { DEEPGRAM_BASEURL, DEEPGRAM_BASEWSS } from "./constants/index.js";
import { buildParams } from "./helpers/index.js";
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 = 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.from(u8).toString('base64'));
},
playAudioChunk: chunk => {
const u8 = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
return getModule().playAudioChunk(Buffer.from(u8).toString('base64'));
},
stopPlayer: () => getModule().stopPlayer()
};
})();
/* ────────────────────────────────────────────────────────────
Hook: useDeepgramTextToSpeech
──────────────────────────────────────────────────────────── */
export function useDeepgramTextToSpeech({
onBeforeSynthesize = () => {},
onSynthesizeSuccess = () => {},
onSynthesizeError = () => {},
onBeforeStream = () => {},
onStreamStart = () => {},
onAudioChunk = () => {},
onStreamError = () => {},
onStreamEnd = () => {},
onStreamMetadata = () => {},
onStreamFlushed = () => {},
onStreamCleared = () => {},
onStreamWarning = () => {},
options = {},
autoPlayAudio = true,
trackState = false
} = {}) {
const [internalState, setInternalState] = useState({
status: 'idle',
error: null
});
const resolvedHttpOptions = 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 = 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 = useRef(null);
const synthesize = useCallback(async text => {
onBeforeSynthesize();
if (trackState) {
setInternalState({
status: 'loading',
error: null
});
}
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 = buildParams(httpParams);
const url = params ? `${DEEPGRAM_BASEURL}/speak?${params}` : `${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);
if (trackState) {
setInternalState({
status: 'idle',
error: null
});
}
return audio;
} catch (err) {
if (err?.name === 'AbortError') {
throw err;
}
onSynthesizeError(err);
if (trackState) {
setInternalState({
status: 'error',
error: err instanceof Error ? err : new Error(String(err))
});
}
throw err;
}
}, [onBeforeSynthesize, onSynthesizeSuccess, onSynthesizeError, resolvedHttpOptions, trackState]);
/* ---------- WebSocket (streaming synth) ---------- */
const ws = useRef(null);
const closeStream = useCallback(() => {
ws.current?.close(1000, 'cleanup');
ws.current = null;
if (autoPlayAudio) {
Deepgram.stopPlayer();
}
if (trackState) {
setInternalState(prev => ({
...prev,
status: 'idle'
}));
}
}, [autoPlayAudio, trackState]);
const sendMessage = 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);
if (trackState) {
setInternalState({
status: 'error',
error: err instanceof Error ? err : new Error(String(err))
});
}
return false;
}
}, [onStreamError, trackState]);
const flushStream = useCallback(() => sendMessage({
type: 'Flush'
}), [sendMessage]);
const clearStream = useCallback(() => sendMessage({
type: 'Clear'
}), [sendMessage]);
const closeStreamGracefully = useCallback(() => sendMessage({
type: 'Close'
}), [sendMessage]);
const sendText = 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 = useCallback(async text => {
onBeforeStream();
if (trackState) {
setInternalState({
status: 'connecting',
error: null
});
}
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 = buildParams(wsParams);
const url = wsParamString ? `${DEEPGRAM_BASEWSS}/speak?${wsParamString}` : `${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 = () => {
if (autoPlayAudio) {
Deepgram.startPlayer(Number(resolvedStreamOptions.sampleRate) || DEFAULT_TTS_SAMPLE_RATE, 1);
}
sendText(text);
onStreamStart();
if (trackState) {
setInternalState({
status: 'connected',
error: null
});
}
};
ws.current.onmessage = ev => {
if (ev.data instanceof ArrayBuffer) {
if (autoPlayAudio) {
Deepgram.feedAudio(ev.data);
}
onAudioChunk(ev.data);
} else if (ev.data instanceof Blob) {
ev.data.arrayBuffer().then(buffer => {
if (autoPlayAudio) {
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'));
if (trackState) {
setInternalState({
status: 'error',
error: new Error(description ?? code ?? 'TTS error')
});
}
break;
}
default:
// Ignore other informational messages.
break;
}
} catch {
// Ignore non-JSON string messages
}
}
};
ws.current.onerror = err => {
onStreamError(err);
if (trackState) {
setInternalState({
status: 'error',
error: err instanceof Error ? err : new Error(String(err))
});
}
};
ws.current.onclose = () => {
onStreamEnd();
closeStream();
};
} catch (err) {
onStreamError(err);
if (trackState) {
setInternalState({
status: 'error',
error: err instanceof Error ? err : new Error(String(err))
});
}
closeStream();
throw err;
}
}, [onBeforeStream, onStreamStart, onAudioChunk, onStreamError, onStreamEnd, onStreamMetadata, onStreamFlushed, onStreamCleared, onStreamWarning, resolvedStreamOptions, sendText, autoPlayAudio, closeStream, trackState]);
const stopStreaming = useCallback(() => {
try {
closeStream();
onStreamEnd();
} catch (err) {
onStreamError(err);
if (trackState) {
setInternalState({
status: 'error',
error: err instanceof Error ? err : new Error(String(err))
});
}
}
}, [onStreamEnd, onStreamError, closeStream, trackState]);
/* ---------- cleanup on unmount ---------- */
useEffect(() => () => {
abortCtrl.current?.abort();
closeStream();
}, [closeStream]);
return {
synthesize,
startStreaming,
sendMessage,
sendText,
flushStream,
clearStream,
closeStreamGracefully,
stopStreaming,
...(trackState ? {
state: internalState
} : {})
};
}
//# sourceMappingURL=useDeepgramTextToSpeech.js.map