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
JavaScript
;
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