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

290 lines 12.4 kB
import { useState, useCallback, useEffect, useRef } from 'react'; import { sanitizeText } from '../sanitizer'; import { getLocalConfig } from '../configuration'; import { useViseme } from '../../context/visemeContext'; export function useTTS(config, options = {}, autoStart = false, defaultEnableAudio = true, defaultSpeakerActive = true) { const [isPlaying, setIsPlaying] = useState(false); const [speakerMuted, setSpeakerMuted] = useState(getLocalConfig('muteSpeaker', !defaultEnableAudio || !defaultSpeakerActive || autoStart)); const { addViseme, resetVisemeQueue, startProcessing, stopProcessing, } = useViseme(); const [hasUserActivatedSpeak, setHasUserActivatedSpeak] = useState(false); const [error, setError] = useState(null); const audioRef = useRef(null); const audioWrapperRef = useRef(null); const globalSpeakRef = useRef(null); const visemeLoadedRef = useRef(false); const apiUrl = options.apiUrl || '/api/tts'; const loadVisemeData = useCallback((visemeData) => { resetVisemeQueue(); visemeLoadedRef.current = false; if (visemeData && visemeData.length > 0) { console.log(`[useTTS] Loading ${visemeData.length} viseme events`); visemeData.forEach(viseme => { addViseme(viseme.visemeId, viseme.audioOffset); }); visemeLoadedRef.current = true; return true; } else { console.warn('[useTTS] No viseme data available'); return false; } }, [addViseme, resetVisemeQueue]); const createAudioWrapper = useCallback(() => { if (!audioRef.current) { console.warn('[useTTS] Cannot create audio wrapper: audio element is null'); return null; } const wrapper = { state: 'running', onstatechange: null, get currentTime() { return audioRef.current ? audioRef.current.currentTime : 0; } }; const handlePause = () => { wrapper.state = 'suspended'; if (wrapper.onstatechange) { wrapper.onstatechange.call(null, new Event('statechange')); } }; const handlePlay = () => { wrapper.state = 'running'; if (wrapper.onstatechange) { wrapper.onstatechange.call(null, new Event('statechange')); } }; const handleEnded = () => { wrapper.state = 'closed'; if (wrapper.onstatechange) { wrapper.onstatechange.call(null, new Event('statechange')); } }; audioRef.current.addEventListener('pause', handlePause); audioRef.current.addEventListener('play', handlePlay); audioRef.current.addEventListener('ended', handleEnded); const cleanupEventListeners = () => { if (audioRef.current) { audioRef.current.removeEventListener('pause', handlePause); audioRef.current.removeEventListener('play', handlePlay); audioRef.current.removeEventListener('ended', handleEnded); } }; wrapper.cleanup = cleanupEventListeners; console.log('[useTTS] Created audio wrapper for viseme processing'); return wrapper; }, []); const cleanup = useCallback(() => { var _a; console.log('[useTTS] Cleaning up audio and viseme resources'); if (audioWrapperRef.current && audioWrapperRef.current.cleanup) { audioWrapperRef.current.cleanup(); console.log('[useTTS] Cleaned up audio wrapper event listeners'); } audioWrapperRef.current = null; stopProcessing(); console.log('[useTTS] Stopped viseme processing'); if ((_a = audioRef.current) === null || _a === void 0 ? void 0 : _a.src) { URL.revokeObjectURL(audioRef.current.src); console.log('[useTTS] Revoked audio object URL'); audioRef.current = null; } visemeLoadedRef.current = false; }, [stopProcessing]); const stop = useCallback(() => { console.log('[useTTS] Stopping audio playback'); if (audioRef.current) { audioRef.current.pause(); audioRef.current.currentTime = 0; } setIsPlaying(false); cleanup(); }, [cleanup]); const emitEndSpeakEvent = useCallback(() => { console.log('[useTTS] Emitting end speak event'); const e = new CustomEvent('MemoriEndSpeak'); document.dispatchEvent(e); if (options.continuousSpeech && options.onEndSpeakStartListen) { console.log('[useTTS] Starting continuous speech listening'); options.onEndSpeakStartListen(); } }, [options.continuousSpeech, options.onEndSpeakStartListen]); const speak = useCallback(async (text) => { console.log('[useTTS] Starting speech synthesis for text:', text); if (!text || options.preview || speakerMuted) { console.log('[useTTS] Speech cancelled - empty text, preview mode, or muted'); emitEndSpeakEvent(); return; } if (!hasUserActivatedSpeak) { console.log('[useTTS] First user activation of speak'); setHasUserActivatedSpeak(true); } try { stop(); setIsPlaying(true); setError(null); const processedText = sanitizeText(text); console.log('[useTTS] Processed text:', processedText); console.log('[useTTS] Making API request to:', 'http://localhost:3000/api/tts', config.voice); const response = await fetch('http://localhost:3000/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ text: processedText, tenant: config.tenant || 'www.aisuru.com', voice: config.voice, model: config.model || 'tts-1', region: config.region, provider: config.provider, includeVisemes: true, }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.error || `API error: ${response.status}`); } console.log('[useTTS] Checking for viseme data in response headers'); const visemeDataHeader = response.headers.get('X-Viseme-Data'); console.log('[useTTS] Viseme data header present:', visemeDataHeader ? 'Yes' : 'No'); let hasVisemeData = false; if (visemeDataHeader) { console.log('[useTTS] Found viseme data header, attempting to parse'); try { const visemeData = JSON.parse(visemeDataHeader); console.log('[useTTS] Successfully parsed viseme data, entries:', visemeData.length); hasVisemeData = loadVisemeData(visemeData); console.log('[useTTS] Loaded viseme data into queue:', hasVisemeData); } catch (err) { console.error('[useTTS] Failed to parse viseme data:', err); } } else { console.log('[useTTS] No viseme data found in response headers'); } console.log('[useTTS] Getting audio blob from response'); const audioBlob = await response.blob(); console.log('[useTTS] Received audio blob of size:', audioBlob.size); const audioUrl = URL.createObjectURL(audioBlob); console.log('[useTTS] Created audio URL:', audioUrl); console.log('[useTTS] Creating new Audio element'); audioRef.current = new Audio(audioUrl); console.log('[useTTS] Configuring audio event handlers'); if (hasVisemeData) { audioWrapperRef.current = createAudioWrapper(); } audioRef.current.oncanplaythrough = async () => { var _a; console.log('[useTTS] Audio can play through, ready to start playback'); try { if (hasVisemeData && audioWrapperRef.current) { console.log('[useTTS] Starting viseme processing before audio playback'); startProcessing(audioWrapperRef.current); console.log('[useTTS] Viseme processing started successfully'); } console.log('[useTTS] Starting audio playback'); await ((_a = audioRef.current) === null || _a === void 0 ? void 0 : _a.play()); console.log('[useTTS] Audio playback started successfully'); if (audioRef.current) { audioRef.current.oncanplaythrough = null; } } catch (e) { console.error('[useTTS] Error in canplaythrough handler:', e); cleanup(); emitEndSpeakEvent(); } }; audioRef.current.onended = () => { console.log('[useTTS] Audio playback ended normally'); setIsPlaying(false); cleanup(); emitEndSpeakEvent(); }; audioRef.current.onerror = e => { console.error('[useTTS] Audio playback error:', e); setIsPlaying(false); cleanup(); const errorMsg = new Error(`Audio error: ${e}`); setError(errorMsg); emitEndSpeakEvent(); }; console.log('[useTTS] Beginning audio load'); audioRef.current.load(); } catch (err) { console.error('[useTTS] Error during speech synthesis:', err); setIsPlaying(false); cleanup(); const errorMsg = err instanceof Error ? err : new Error(String(err)); setError(errorMsg); try { if ('speechSynthesis' in window) { console.log('[useTTS] Attempting browser fallback synthesis'); const utterance = new SpeechSynthesisUtterance(sanitizeText(text)); window.speechSynthesis.speak(utterance); } } catch (fallbackErr) { console.error('[useTTS] Browser fallback synthesis error:', fallbackErr); } emitEndSpeakEvent(); } }, [ config, speakerMuted, options.preview, hasUserActivatedSpeak, stop, cleanup, loadVisemeData, createAudioWrapper, startProcessing, emitEndSpeakEvent, ]); const toggleMute = useCallback((mute) => { const newMuteState = mute !== undefined ? mute : !speakerMuted; console.log('[useTTS] Toggling mute state to:', newMuteState); setSpeakerMuted(newMuteState); if (newMuteState && isPlaying) { stop(); } }, [speakerMuted, isPlaying, stop]); useEffect(() => { console.log('[useTTS] Updating global memoriSpeaking state:', isPlaying); if (typeof window !== 'undefined') { window.memoriSpeaking = isPlaying; } }, [isPlaying]); useEffect(() => { if (typeof window !== 'undefined') { console.log('[useTTS] Setting up global speak function'); globalSpeakRef.current = window.speak; window.speak = speak; return () => { console.log('[useTTS] Cleaning up global speak function'); window.speak = globalSpeakRef.current; }; } }, [speak]); useEffect(() => { return () => { console.log('[useTTS] Component unmounting, cleaning up'); stop(); }; }, [stop]); return { speak, stop, isPlaying, speakerMuted, toggleMute, hasUserActivatedSpeak, setHasUserActivatedSpeak, error, setError, }; } //# sourceMappingURL=useTTS.js.map