UNPKG

@memori.ai/memori-react

Version:

[![npm version](https://img.shields.io/github/package-json/v/memori-ai/memori-react)](https://www.npmjs.com/package/@memori.ai/memori-react) ![Tests](https://github.com/memori-ai/memori-react/workflows/CI/badge.svg?branch=main) ![TypeScript Support](https

341 lines 13.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useSTT = void 0; const react_1 = require("react"); const configuration_1 = require("../configuration"); async function convertToWav(audioBlob) { return new Promise((resolve, reject) => { const AudioContextClass = window.AudioContext || window.webkitAudioContext; if (!AudioContextClass) { reject(new Error('AudioContext not supported in this browser')); return; } const audioContext = new AudioContextClass(); const fileReader = new FileReader(); fileReader.onload = async () => { try { const arrayBuffer = fileReader.result; if (audioContext.state === 'suspended') { await audioContext.resume(); } const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); const wavBlob = audioBufferToWav(audioBuffer); await audioContext.close(); resolve(wavBlob); } catch (error) { reject(error); } }; fileReader.onerror = () => { reject(new Error('Failed to read audio file')); }; fileReader.readAsArrayBuffer(audioBlob); }); } function audioBufferToWav(buffer) { const length = buffer.length; const sampleRate = buffer.sampleRate; const numberOfChannels = buffer.numberOfChannels; const arrayBuffer = new ArrayBuffer(44 + length * numberOfChannels * 2); const view = new DataView(arrayBuffer); const writeString = (offset, string) => { for (let i = 0; i < string.length; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } }; writeString(0, 'RIFF'); view.setUint32(4, 36 + length * numberOfChannels * 2, true); writeString(8, 'WAVE'); writeString(12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, numberOfChannels, true); view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * numberOfChannels * 2, true); view.setUint16(32, numberOfChannels * 2, true); view.setUint16(34, 16, true); writeString(36, 'data'); view.setUint32(40, length * numberOfChannels * 2, true); let offset = 44; for (let i = 0; i < length; i++) { for (let channel = 0; channel < numberOfChannels; channel++) { const sample = Math.max(-1, Math.min(1, buffer.getChannelData(channel)[i])); view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true); offset += 2; } } return new Blob([arrayBuffer], { type: 'audio/wav' }); } function useSTT(config, processSpeechAndSendMessage, options = {}, defaultEnableAudio = true) { const [recordingState, setRecordingState] = (0, react_1.useState)('idle'); const [microphoneMuted, setMicrophoneMuted] = (0, react_1.useState)((0, configuration_1.getLocalConfig)('muteMicrophone', !defaultEnableAudio)); const [hasUserActivatedRecord, setHasUserActivatedRecord] = (0, react_1.useState)(false); const [lastTranscription, setLastTranscription] = (0, react_1.useState)(null); const [isListening, setIsListening] = (0, react_1.useState)(false); const mediaRecorderRef = (0, react_1.useRef)(null); const audioStreamRef = (0, react_1.useRef)(null); const chunksRef = (0, react_1.useRef)([]); const isRecordingRef = (0, react_1.useRef)(false); const isMountedRef = (0, react_1.useRef)(true); const apiUrl = options.apiUrl || '/api/stt'; const initializeRecording = (0, react_1.useCallback)(async () => { try { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { throw new Error('Media recording is not supported in this browser'); } const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true, sampleRate: 16000, }, }); audioStreamRef.current = stream; let mimeType = ''; const supportedFormats = [ 'audio/mp4', 'audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus', 'audio/wav', ]; for (const format of supportedFormats) { if (MediaRecorder.isTypeSupported(format)) { mimeType = format; break; } } const mediaRecorder = new MediaRecorder(stream, mimeType ? { mimeType } : {}); mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { chunksRef.current.push(event.data); } }; mediaRecorder.onstop = async () => { setRecordingState('processing'); setIsListening(false); try { if (chunksRef.current.length === 0) { throw new Error('No audio data recorded'); } let audioBlob = new Blob(chunksRef.current, { type: mediaRecorder.mimeType, }); if (audioBlob.size === 0) { throw new Error('Recorded audio is empty'); } if (config.provider === 'azure') { try { audioBlob = await convertToWav(audioBlob); } catch (conversionError) { throw new Error('Failed to convert audio to WAV format for Azure'); } } const result = await transcribeAudio(audioBlob); if (result.text && result.text.trim().length > 0) { if (processSpeechAndSendMessage) { processSpeechAndSendMessage(result.text); } setLastTranscription(result); if (options.onTranscriptionComplete) { options.onTranscriptionComplete(result); } } setRecordingState('idle'); } catch (err) { const errorMsg = err instanceof Error ? err : new Error(String(err)); setRecordingState('error'); if (options.onError) { options.onError(errorMsg); } } finally { chunksRef.current = []; isRecordingRef.current = false; } }; mediaRecorder.onerror = () => { const errorMsg = new Error('Recording failed'); setRecordingState('error'); isRecordingRef.current = false; if (options.onError) { options.onError(errorMsg); } }; mediaRecorderRef.current = mediaRecorder; return true; } catch (err) { const errorMsg = err instanceof Error ? err : new Error('Failed to access microphone'); setRecordingState('error'); if (options.onError) { options.onError(errorMsg); } return false; } }, [config.provider, options]); const transcribeAudio = (0, react_1.useCallback)(async (audioBlob) => { var _a; const formData = new FormData(); let fileExtension = 'webm'; if (config.provider === 'azure') { fileExtension = 'wav'; } else if ((_a = mediaRecorderRef.current) === null || _a === void 0 ? void 0 : _a.mimeType) { if (mediaRecorderRef.current.mimeType.includes('webm')) { fileExtension = 'webm'; } else if (mediaRecorderRef.current.mimeType.includes('mp4')) { fileExtension = 'mp4'; } else if (mediaRecorderRef.current.mimeType.includes('ogg')) { fileExtension = 'ogg'; } } formData.append('audio', audioBlob, `recording.${fileExtension}`); formData.append('provider', config.provider); formData.append('tenant', config.tenant || 'www.aisuru.com'); if (config.language) { formData.append('language', config.language); } if (config.model) { formData.append('model', config.model); } if (config.region) { formData.append('region', config.region); } const response = await fetch(apiUrl, { method: 'POST', body: formData, }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.error || `API error: ${response.status}`); } const data = await response.json(); if (!data.success || !data.result) { throw new Error('Invalid response from transcription service'); } return data.result; }, [config, apiUrl]); const startRecording = (0, react_1.useCallback)(async () => { if (microphoneMuted || recordingState === 'recording') { return; } if (!hasUserActivatedRecord) { setHasUserActivatedRecord(true); } try { setRecordingState('recording'); if (!mediaRecorderRef.current) { const initialized = await initializeRecording(); if (!initialized) { return; } } chunksRef.current = []; isRecordingRef.current = true; if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'inactive') { mediaRecorderRef.current.start(); setIsListening(true); } } catch (err) { const errorMsg = err instanceof Error ? err : new Error('Failed to start recording'); setRecordingState('error'); isRecordingRef.current = false; if (options.onError) { options.onError(errorMsg); } } }, [ microphoneMuted, recordingState, hasUserActivatedRecord, initializeRecording, options, ]); const stopRecording = (0, react_1.useCallback)(() => { if (!isRecordingRef.current) { return; } try { setIsListening(false); if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') { mediaRecorderRef.current.stop(); } } catch (err) { const errorMsg = err instanceof Error ? err : new Error('Failed to stop recording'); setRecordingState('error'); isRecordingRef.current = false; if (options.onError) { options.onError(errorMsg); } } }, [recordingState, options]); const toggleRecording = (0, react_1.useCallback)(async () => { if (recordingState === 'recording') { stopRecording(); } else if (recordingState === 'idle') { await startRecording(); } }, [recordingState, startRecording, stopRecording]); const toggleMute = (0, react_1.useCallback)((mute) => { const newMuteState = mute !== undefined ? mute : !microphoneMuted; setMicrophoneMuted(newMuteState); if (newMuteState && recordingState === 'recording') { stopRecording(); } }, [microphoneMuted, recordingState, stopRecording]); const cleanup = (0, react_1.useCallback)(() => { isRecordingRef.current = false; if (mediaRecorderRef.current) { if (mediaRecorderRef.current.state === 'recording') { mediaRecorderRef.current.stop(); } mediaRecorderRef.current = null; } if (audioStreamRef.current) { audioStreamRef.current.getTracks().forEach(track => track.stop()); audioStreamRef.current = null; } chunksRef.current = []; setIsListening(false); setRecordingState('idle'); }, []); (0, react_1.useEffect)(() => { return () => { isMountedRef.current = false; cleanup(); }; }, [cleanup]); (0, react_1.useEffect)(() => { if (typeof window !== 'undefined') { window.memoriListening = isListening; } }, [isListening]); return { recordingState, microphoneMuted, hasUserActivatedRecord, lastTranscription, isListening, startRecording, stopRecording, toggleRecording, toggleMute, transcribeAudio, setHasUserActivatedRecord, cleanup, }; } exports.useSTT = useSTT; //# sourceMappingURL=useSTT.js.map