@memori.ai/memori-react 7.34.2 → 8.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/components/Chat/Chat.d.ts +1 -0
  3. package/dist/components/Chat/Chat.js +2 -2
  4. package/dist/components/Chat/Chat.js.map +1 -1
  5. package/dist/components/ChatInputs/ChatInputs.d.ts +1 -0
  6. package/dist/components/ChatInputs/ChatInputs.js +3 -3
  7. package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
  8. package/dist/components/MemoriWidget/MemoriWidget.d.ts +3 -3
  9. package/dist/components/MemoriWidget/MemoriWidget.js +138 -425
  10. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  11. package/dist/context/visemeContext.js +39 -30
  12. package/dist/context/visemeContext.js.map +1 -1
  13. package/dist/helpers/sanitizer.d.ts +6 -0
  14. package/dist/helpers/sanitizer.js +41 -0
  15. package/dist/helpers/sanitizer.js.map +1 -0
  16. package/dist/helpers/tts/ttsVoiceUtility.d.ts +158 -0
  17. package/dist/helpers/tts/ttsVoiceUtility.js +192 -0
  18. package/dist/helpers/tts/ttsVoiceUtility.js.map +1 -0
  19. package/dist/helpers/tts/useTTS.d.ts +26 -0
  20. package/dist/helpers/tts/useTTS.js +274 -0
  21. package/dist/helpers/tts/useTTS.js.map +1 -0
  22. package/dist/index.js +12 -7
  23. package/dist/index.js.map +1 -1
  24. package/esm/components/Chat/Chat.d.ts +1 -0
  25. package/esm/components/Chat/Chat.js +2 -2
  26. package/esm/components/Chat/Chat.js.map +1 -1
  27. package/esm/components/ChatInputs/ChatInputs.d.ts +1 -0
  28. package/esm/components/ChatInputs/ChatInputs.js +3 -3
  29. package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
  30. package/esm/components/MemoriWidget/MemoriWidget.d.ts +3 -3
  31. package/esm/components/MemoriWidget/MemoriWidget.js +139 -426
  32. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  33. package/esm/context/visemeContext.js +39 -30
  34. package/esm/context/visemeContext.js.map +1 -1
  35. package/esm/helpers/sanitizer.d.ts +6 -0
  36. package/esm/helpers/sanitizer.js +32 -0
  37. package/esm/helpers/sanitizer.js.map +1 -0
  38. package/esm/helpers/tts/ttsVoiceUtility.d.ts +158 -0
  39. package/esm/helpers/tts/ttsVoiceUtility.js +182 -0
  40. package/esm/helpers/tts/ttsVoiceUtility.js.map +1 -0
  41. package/esm/helpers/tts/useTTS.d.ts +26 -0
  42. package/esm/helpers/tts/useTTS.js +270 -0
  43. package/esm/helpers/tts/useTTS.js.map +1 -0
  44. package/esm/index.js +12 -7
  45. package/esm/index.js.map +1 -1
  46. package/package.json +2 -2
  47. package/src/components/Chat/Chat.tsx +3 -0
  48. package/src/components/ChatInputs/ChatInputs.tsx +4 -2
  49. package/src/components/MemoriWidget/MemoriWidget.tsx +246 -637
  50. package/src/context/visemeContext.tsx +77 -55
  51. package/src/helpers/sanitizer.ts +71 -0
  52. package/src/helpers/tts/ttsVoiceUtility.ts +275 -0
  53. package/src/helpers/tts/useTTS.ts +431 -0
  54. package/src/index.tsx +14 -10
@@ -0,0 +1,431 @@
1
+ // Improved useTTS.ts with better viseme handling
2
+ import { useState, useCallback, useEffect, useRef } from 'react';
3
+ import { sanitizeText } from '../sanitizer';
4
+ import { getLocalConfig } from '../configuration';
5
+ import Alert from '../../components/ui/Alert';
6
+ import { useViseme } from '../../context/visemeContext';
7
+ import { IAudioContext } from 'standardized-audio-context';
8
+
9
+ /**
10
+ * Configurazione per il TTS
11
+ */
12
+ export interface TTSConfig {
13
+ provider: 'azure' | 'openai';
14
+ voice?: string;
15
+ model?: string;
16
+ region?: string; // richiesto per Azure
17
+ tenant?: string; // Tenant identifier for multi-tenant applications
18
+ }
19
+
20
+ type VisemeData = {
21
+ visemeId: number;
22
+ audioOffset: number;
23
+ };
24
+
25
+ /**
26
+ * Opzioni per l'hook useTTS
27
+ */
28
+ export interface UseTTSOptions {
29
+ apiUrl?: string;
30
+ continuousSpeech?: boolean;
31
+ onEndSpeakStartListen?: () => void;
32
+ preview?: boolean;
33
+ disableSpeaker?: boolean;
34
+ }
35
+
36
+ // Create our own simplified audio context interface for better typing
37
+ interface SimpleAudioWrapper {
38
+ currentTime: number;
39
+ state: 'running' | 'suspended' | 'closed';
40
+ onstatechange: ((this: AudioContext, ev: Event) => any) | null;
41
+ }
42
+
43
+ /**
44
+ * Hook unificato che gestisce la sintesi vocale
45
+ */
46
+ export function useTTS(
47
+ config: TTSConfig,
48
+ options: UseTTSOptions = {},
49
+ autoStart: boolean = false,
50
+ defaultEnableAudio: boolean = true,
51
+ defaultSpeakerActive: boolean = true
52
+ ) {
53
+ // Stato locale
54
+ const [isPlaying, setIsPlaying] = useState(false);
55
+ const [speakerMuted, setSpeakerMuted] = useState(
56
+ getLocalConfig(
57
+ 'muteSpeaker',
58
+ !defaultEnableAudio || !defaultSpeakerActive || autoStart
59
+ )
60
+ );
61
+ // Get viseme methods from your context
62
+ const {
63
+ addViseme,
64
+ resetVisemeQueue,
65
+ startProcessing,
66
+ stopProcessing,
67
+ } = useViseme();
68
+ const [hasUserActivatedSpeak, setHasUserActivatedSpeak] = useState(false);
69
+ const [error, setError] = useState<Error | null>(null);
70
+
71
+ // Riferimenti
72
+ const audioRef = useRef<HTMLAudioElement | null>(null);
73
+ const audioWrapperRef = useRef<SimpleAudioWrapper | null>(null);
74
+ const globalSpeakRef = useRef<Function | null>(null);
75
+ const visemeLoadedRef = useRef<boolean>(false);
76
+ const isSpeakingRef = useRef<boolean>(false);
77
+ const apiUrl = options.apiUrl || '/api/tts';
78
+
79
+ // Load viseme data into the queue
80
+ const loadVisemeData = useCallback(
81
+ (visemeData: VisemeData[]) => {
82
+ // Make sure we're in a clean state before loading new visemes
83
+ resetVisemeQueue();
84
+ visemeLoadedRef.current = false;
85
+
86
+ if (visemeData && visemeData.length > 0) {
87
+ console.log(`[useTTS] Loading ${visemeData.length} viseme events`);
88
+ visemeData.forEach(viseme => {
89
+ addViseme(viseme.visemeId, viseme.audioOffset);
90
+ });
91
+ visemeLoadedRef.current = true;
92
+ return true;
93
+ } else {
94
+ console.warn('[useTTS] No viseme data available');
95
+ return false;
96
+ }
97
+ },
98
+ [addViseme, resetVisemeQueue]
99
+ );
100
+
101
+ // Create audio wrapper for viseme processing
102
+ const createAudioWrapper = useCallback(() => {
103
+ if (!audioRef.current) {
104
+ console.warn('[useTTS] Cannot create audio wrapper: audio element is null');
105
+ return null;
106
+ }
107
+
108
+ // Create a clean wrapper for this audio session
109
+ const wrapper: SimpleAudioWrapper = {
110
+ state: 'running',
111
+ onstatechange: null,
112
+ get currentTime() {
113
+ return audioRef.current ? audioRef.current.currentTime : 0;
114
+ }
115
+ };
116
+
117
+ // Add event listeners to update the state
118
+ const handlePause = () => {
119
+ wrapper.state = 'suspended';
120
+ if (wrapper.onstatechange) {
121
+ wrapper.onstatechange.call(null as any, new Event('statechange'));
122
+ }
123
+ };
124
+
125
+ const handlePlay = () => {
126
+ wrapper.state = 'running';
127
+ if (wrapper.onstatechange) {
128
+ wrapper.onstatechange.call(null as any, new Event('statechange'));
129
+ }
130
+ };
131
+
132
+ const handleEnded = () => {
133
+ wrapper.state = 'closed';
134
+ if (wrapper.onstatechange) {
135
+ wrapper.onstatechange.call(null as any, new Event('statechange'));
136
+ }
137
+ };
138
+
139
+ // Attach event listeners to the audio element
140
+ audioRef.current.addEventListener('pause', handlePause);
141
+ audioRef.current.addEventListener('play', handlePlay);
142
+ audioRef.current.addEventListener('ended', handleEnded);
143
+
144
+ // Store cleanup function
145
+ const cleanupEventListeners = () => {
146
+ if (audioRef.current) {
147
+ audioRef.current.removeEventListener('pause', handlePause);
148
+ audioRef.current.removeEventListener('play', handlePlay);
149
+ audioRef.current.removeEventListener('ended', handleEnded);
150
+ }
151
+ };
152
+
153
+ // Store the cleanup function on the wrapper for later use
154
+ (wrapper as any).cleanup = cleanupEventListeners;
155
+
156
+ console.log('[useTTS] Created audio wrapper for viseme processing');
157
+ return wrapper;
158
+ }, []);
159
+
160
+ /**
161
+ * Performs a complete cleanup of audio and viseme resources
162
+ */
163
+ const cleanup = useCallback(() => {
164
+ console.log('[useTTS] Cleaning up audio and viseme resources');
165
+
166
+ // First, clean up audio wrapper
167
+ if (audioWrapperRef.current && (audioWrapperRef.current as any).cleanup) {
168
+ (audioWrapperRef.current as any).cleanup();
169
+ console.log('[useTTS] Cleaned up audio wrapper event listeners');
170
+ }
171
+ audioWrapperRef.current = null;
172
+
173
+ // Then stop viseme processing
174
+ stopProcessing();
175
+ console.log('[useTTS] Stopped viseme processing');
176
+
177
+ // Finally clean up audio resources
178
+ if (audioRef.current?.src) {
179
+ URL.revokeObjectURL(audioRef.current.src);
180
+ console.log('[useTTS] Revoked audio object URL');
181
+ audioRef.current = null;
182
+ }
183
+
184
+ // Reset flags
185
+ visemeLoadedRef.current = false;
186
+ isSpeakingRef.current = false;
187
+ }, [stopProcessing]);
188
+
189
+ /**
190
+ * Stops audio playback and cleans up
191
+ */
192
+ const stop = useCallback((): void => {
193
+ console.log('[useTTS] Stopping audio playback');
194
+
195
+ // Pause audio first
196
+ if (audioRef.current) {
197
+ audioRef.current.pause();
198
+ audioRef.current.currentTime = 0;
199
+ }
200
+
201
+ // Set UI state
202
+ setIsPlaying(false);
203
+
204
+ // Clean up all resources
205
+ cleanup();
206
+ }, [cleanup]);
207
+
208
+ /**
209
+ * Emette l'evento di fine riproduzione
210
+ */
211
+ const emitEndSpeakEvent = useCallback(() => {
212
+ console.log('[useTTS] Emitting end speak event');
213
+ const e = new CustomEvent('MemoriEndSpeak');
214
+ document.dispatchEvent(e);
215
+
216
+ // Se è impostato il parlato continuo, avvia l'ascolto
217
+ if (options.continuousSpeech && options.onEndSpeakStartListen) {
218
+ console.log('[useTTS] Starting continuous speech listening');
219
+ options.onEndSpeakStartListen();
220
+ }
221
+ }, [options.continuousSpeech, options.onEndSpeakStartListen]);
222
+
223
+ /**
224
+ * Sintetizza il testo in audio e lo riproduce
225
+ */
226
+ const speak = useCallback(
227
+ async (text: string): Promise<void> => {
228
+ if (isSpeakingRef.current) {
229
+ return;
230
+ }
231
+
232
+ if (!text || options.preview || speakerMuted) {
233
+ emitEndSpeakEvent();
234
+ return;
235
+ }
236
+
237
+ isSpeakingRef.current = true;
238
+
239
+ if (!hasUserActivatedSpeak) {
240
+ setHasUserActivatedSpeak(true);
241
+ }
242
+
243
+ try {
244
+ stop();
245
+
246
+ setIsPlaying(true);
247
+ setError(null);
248
+
249
+ const processedText = sanitizeText(text);
250
+
251
+ const response = await fetch('http://localhost:3000/api/tts', {
252
+ method: 'POST',
253
+ headers: {
254
+ 'Content-Type': 'application/json',
255
+ },
256
+ body: JSON.stringify({
257
+ text: processedText,
258
+ tenant: config.tenant || 'www.aisuru.com',
259
+ voice: config.voice,
260
+ model: config.model || 'tts-1',
261
+ region: config.region,
262
+ provider: config.provider,
263
+ includeVisemes: true,
264
+ }),
265
+ });
266
+
267
+ if (!response.ok) {
268
+ const errorData = await response.json().catch(() => ({}));
269
+ throw new Error(errorData.error || `API error: ${response.status}`);
270
+ }
271
+
272
+ const visemeDataHeader = response.headers.get('X-Viseme-Data');
273
+
274
+ let hasVisemeData = false;
275
+ if (visemeDataHeader) {
276
+ try {
277
+ const visemeData: VisemeData[] = JSON.parse(visemeDataHeader);
278
+ hasVisemeData = loadVisemeData(visemeData);
279
+ } catch (err) {
280
+ console.error('[useTTS] Error parsing viseme data:', err);
281
+ }
282
+ }
283
+
284
+ const audioBlob = await response.blob();
285
+ const audioUrl = URL.createObjectURL(audioBlob);
286
+
287
+ audioRef.current = new Audio(audioUrl);
288
+
289
+ if (hasVisemeData) {
290
+ audioWrapperRef.current = createAudioWrapper();
291
+ }
292
+
293
+ audioRef.current.oncanplaythrough = async () => {
294
+ try {
295
+ if (hasVisemeData && audioWrapperRef.current) {
296
+ startProcessing(audioWrapperRef.current as unknown as IAudioContext);
297
+ }
298
+
299
+ await audioRef.current?.play();
300
+
301
+ if (audioRef.current) {
302
+ audioRef.current.oncanplaythrough = null;
303
+ }
304
+ } catch (e: any) {
305
+ cleanup();
306
+ emitEndSpeakEvent();
307
+ }
308
+ };
309
+
310
+ audioRef.current.onended = () => {
311
+ setIsPlaying(false);
312
+ isSpeakingRef.current = false;
313
+ cleanup();
314
+ emitEndSpeakEvent();
315
+ };
316
+
317
+ audioRef.current.onerror = () => {
318
+ setIsPlaying(false);
319
+ isSpeakingRef.current = false;
320
+ cleanup();
321
+
322
+ const errorMsg = new Error(`Audio playback failed. This may be due to a network issue or audio format problem.`);
323
+ setError(errorMsg);
324
+ emitEndSpeakEvent();
325
+ };
326
+
327
+ audioRef.current.load();
328
+
329
+ } catch (err) {
330
+ setIsPlaying(false);
331
+ isSpeakingRef.current = false;
332
+ cleanup();
333
+ const errorMsg = err instanceof Error ? err : new Error(String(err));
334
+ setError(errorMsg);
335
+
336
+ try {
337
+ if ('speechSynthesis' in window) {
338
+ const utterance = new SpeechSynthesisUtterance(sanitizeText(text));
339
+ window.speechSynthesis.speak(utterance);
340
+ }
341
+ } catch (fallbackErr) {
342
+ console.error('[useTTS] Browser fallback synthesis error:', fallbackErr);
343
+ }
344
+
345
+ emitEndSpeakEvent();
346
+ }
347
+ },
348
+ [
349
+ config,
350
+ speakerMuted,
351
+ options.preview,
352
+ hasUserActivatedSpeak,
353
+ stop,
354
+ cleanup,
355
+ loadVisemeData,
356
+ createAudioWrapper,
357
+ startProcessing,
358
+ emitEndSpeakEvent,
359
+ ]
360
+ );
361
+
362
+ /**
363
+ * Imposta lo stato del muto
364
+ */
365
+ const toggleMute = useCallback(
366
+ (mute?: boolean) => {
367
+ const newMuteState = mute !== undefined ? mute : !speakerMuted;
368
+ console.log('[useTTS] Toggling mute state to:', newMuteState);
369
+ setSpeakerMuted(newMuteState);
370
+
371
+ // Se stiamo attivando il muto mentre l'audio sta suonando, fermiamo l'audio
372
+ if (newMuteState && isPlaying) {
373
+ stop();
374
+ }
375
+ },
376
+ [speakerMuted, isPlaying, stop]
377
+ );
378
+
379
+ /**
380
+ * Aggiorna la variabile globale quando cambia isPlaying
381
+ */
382
+ useEffect(() => {
383
+ console.log('[useTTS] Updating global memoriSpeaking state:', isPlaying);
384
+ if (typeof window !== 'undefined') {
385
+ (window as any).memoriSpeaking = isPlaying;
386
+ }
387
+ }, [isPlaying]);
388
+
389
+ /**
390
+ * Hook per esporre la funzione speak globalmente
391
+ */
392
+ useEffect(() => {
393
+ if (typeof window !== 'undefined') {
394
+ console.log('[useTTS] Setting up global speak function');
395
+ // Salviamo una referenza alla funzione originale, se esistente
396
+ globalSpeakRef.current = (window as any).speak;
397
+
398
+ // Assegniamo la nostra funzione
399
+ (window as any).speak = speak;
400
+
401
+ // Pulizia al dismount
402
+ return () => {
403
+ console.log('[useTTS] Cleaning up global speak function');
404
+ // Ripristiniamo la funzione originale se esisteva
405
+ (window as any).speak = globalSpeakRef.current;
406
+ };
407
+ }
408
+ }, [speak]);
409
+
410
+ /**
411
+ * Pulizia delle risorse al dismount
412
+ */
413
+ useEffect(() => {
414
+ return () => {
415
+ console.log('[useTTS] Component unmounting, cleaning up');
416
+ stop();
417
+ };
418
+ }, [stop]);
419
+
420
+ return {
421
+ speak,
422
+ stop,
423
+ isPlaying,
424
+ speakerMuted,
425
+ toggleMute,
426
+ hasUserActivatedSpeak,
427
+ setHasUserActivatedSpeak,
428
+ error,
429
+ setError,
430
+ };
431
+ }
package/src/index.tsx CHANGED
@@ -150,7 +150,7 @@ const Memori: React.FC<Props> = ({
150
150
  }) => {
151
151
  const [memori, setMemori] = useState<IMemori>();
152
152
  const [tenant, setTenant] = useState<Tenant>();
153
- const [speechKey, setSpeechKey] = useState<string | undefined>();
153
+ const [provider, setProvider] = useState<string | undefined>();
154
154
  const { t } = useTranslation();
155
155
 
156
156
  if (!((memoriID && ownerUserID) || (memoriName && ownerUserName))) {
@@ -162,17 +162,21 @@ const Memori: React.FC<Props> = ({
162
162
  const client = memoriApiClient(apiURL, engineURL);
163
163
 
164
164
  const fetchSpeechKey = useCallback(async () => {
165
- const url =
166
- baseURL ||
167
- (tenantID.startsWith('https://') ? tenantID : `https://${tenantID}`);
165
+ const url = baseURL ||
166
+ (tenantID.startsWith('https://') ? tenantID : `https://${tenantID}`);
168
167
  try {
169
- const result = await fetch(`${url}/api/speechkey`);
168
+ const result = await fetch(`${url}/api/speechkey?tenant=${tenantID}`, {
169
+ method: 'GET',
170
+ headers: {
171
+ 'Content-Type': 'application/json',
172
+ },
173
+ });
170
174
  const data = await result.json();
171
175
 
172
- if (data.AZURE_COGNITIVE_SERVICES_TTS_KEY) {
173
- setSpeechKey(data.AZURE_COGNITIVE_SERVICES_TTS_KEY);
176
+ if (data.provider) {
177
+ setProvider(data.provider);
174
178
  } else {
175
- console.log('AZURE_COGNITIVE_SERVICES_TTS_KEY not found');
179
+ console.log('provider not found');
176
180
  }
177
181
  } catch (error) {
178
182
  console.error('Error fetching speech key', error);
@@ -328,7 +332,7 @@ const Memori: React.FC<Props> = ({
328
332
  initialContextVars={initialContextVars}
329
333
  initialQuestion={initialQuestionLayout}
330
334
  authToken={authToken}
331
- AZURE_COGNITIVE_SERVICES_TTS_KEY={speechKey}
335
+ ttsProvider={provider ? provider as 'azure' | 'openai' : 'azure'}
332
336
  autoStart={
333
337
  autoStart !== undefined
334
338
  ? autoStart
@@ -336,7 +340,7 @@ const Memori: React.FC<Props> = ({
336
340
  ? true
337
341
  : autoStart
338
342
  }
339
- enableAudio={enableAudio && !!speechKey}
343
+ enableAudio={enableAudio && !!provider}
340
344
  defaultSpeakerActive={defaultSpeakerActive}
341
345
  disableTextEnteredEvents={disableTextEnteredEvents}
342
346
  onStateChange={onStateChange}