@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
@@ -28,6 +28,13 @@ interface VisemeContextType {
28
28
 
29
29
  const VisemeContext = createContext<VisemeContextType | undefined>(undefined);
30
30
 
31
+ // Add this interface at the top near your other types
32
+ interface SimpleAudioWrapper {
33
+ currentTime: number;
34
+ state: string;
35
+ onstatechange: ((this: AudioContext, ev: Event) => any) | null;
36
+ }
37
+
31
38
  const VISEME_MAP: Readonly<{ [key: number]: string }> = {
32
39
  0: 'viseme_sil', // silence
33
40
  1: 'viseme_PP', // p, b, m
@@ -142,30 +149,59 @@ export const VisemeProvider: React.FC<{ children: React.ReactNode }> = ({
142
149
  const frameCountRef = useRef(0);
143
150
  const firstVisemeTimeRef = useRef<number | null>(null);
144
151
 
145
- const setAudioContext = useCallback((ctx: IAudioContext) => {
146
- audioContextRef.current = ctx;
147
-
148
- // Listen to audio context state changes
149
- ctx.onstatechange = () => {
150
- // logVisemeEvent('Audio Context State Change', {
151
- // state: ctx.state,
152
- // currentTime: ctx.currentTime,
153
- // });
152
+ // Update the setAudioContext function
153
+ const setAudioContext = useCallback(
154
+ (ctx: IAudioContext | SimpleAudioWrapper) => {
155
+ audioContextRef.current = ctx as IAudioContext;
156
+
157
+ // Listen to audio context state changes
158
+ if (ctx.onstatechange !== null) {
159
+ ctx.onstatechange = () => {
160
+ switch (ctx.state) {
161
+ case 'running':
162
+ setVisemeState('active');
163
+ break;
164
+ case 'suspended':
165
+ setVisemeState('paused');
166
+ break;
167
+ case 'closed':
168
+ setVisemeState('finished');
169
+ stopProcessing();
170
+ break;
171
+ }
172
+ };
173
+ }
174
+ },
175
+ []
176
+ );
154
177
 
155
- switch (ctx.state) {
156
- case 'running':
157
- setVisemeState('active');
158
- break;
159
- case 'suspended':
160
- setVisemeState('paused');
161
- break;
162
- case 'closed':
163
- setVisemeState('finished');
164
- stopProcessing();
165
- break;
178
+ // Update the startProcessing function
179
+ const startProcessing = useCallback(
180
+ (audioCtx: IAudioContext | SimpleAudioWrapper) => {
181
+ if (!audioCtx) {
182
+ console.error('[VisemeContext] No audio context provided');
183
+ return;
166
184
  }
167
- };
168
- }, []);
185
+
186
+ audioContextRef.current = audioCtx as IAudioContext;
187
+
188
+ // Initialize with the current time of the audio context
189
+ audioStartTimeRef.current = audioCtx.currentTime;
190
+
191
+ // Reset frame counter
192
+ frameCountRef.current = 0;
193
+
194
+ // Update state
195
+ setIsProcessing(true);
196
+ setVisemeState('active');
197
+
198
+ console.log('[VisemeContext] Started processing visemes', {
199
+ audioTime: audioCtx.currentTime,
200
+ queueLength: visemeQueueRef.current.length,
201
+ });
202
+ },
203
+ []
204
+ );
169
205
 
170
206
  const addViseme = useCallback(
171
207
  (visemeId: number, audioOffset: number) => {
@@ -209,37 +245,36 @@ export const VisemeProvider: React.FC<{ children: React.ReactNode }> = ({
209
245
  [visemeState]
210
246
  );
211
247
 
212
- const startProcessing = useCallback((audioCtx: IAudioContext) => {
213
- if (!audioCtx) {
214
- // logVisemeError('No audio context provided', { state: visemeState });
215
- return;
216
- }
248
+ // Make resetVisemeQueue more robust
249
+ const resetVisemeQueue = useCallback(() => {
250
+ console.log('[VisemeContext] Resetting viseme queue');
251
+
252
+ // Clear all queued visemes
253
+ visemeQueueRef.current = [];
217
254
 
218
- audioContextRef.current = audioCtx;
219
- audioStartTimeRef.current = audioCtx.currentTime;
255
+ // Reset all other state
256
+ lastVisemeRef.current = null;
257
+ audioStartTimeRef.current = null;
258
+ firstVisemeTimeRef.current = null;
220
259
  frameCountRef.current = 0;
221
- setIsProcessing(true);
222
- setVisemeState('active');
223
-
224
- // logVisemeEvent('Started Processing', {
225
- // audioTime: audioCtx.currentTime,
226
- // queueLength: visemeQueueRef.current.length,
227
- // state: visemeState,
228
- // });
260
+
261
+ // Set state back to idle
262
+ setVisemeState('idle');
229
263
  }, []);
230
264
 
265
+ // Make stopProcessing more robust
231
266
  const stopProcessing = useCallback(() => {
267
+ console.log('[VisemeContext] Stopping viseme processing');
268
+
269
+ // Update processing state first
232
270
  setIsProcessing(false);
233
271
  setVisemeState('finished');
272
+
273
+ // Then reset all refs
234
274
  audioStartTimeRef.current = null;
235
275
  lastVisemeRef.current = null;
236
276
  frameCountRef.current = 0;
237
277
  audioContextRef.current = null;
238
-
239
- // logVisemeEvent('Stopped Processing', {
240
- // queueLength: visemeQueueRef.current.length,
241
- // state: visemeState,
242
- // });
243
278
  }, []);
244
279
 
245
280
  const updateCurrentViseme = useCallback(
@@ -303,19 +338,6 @@ export const VisemeProvider: React.FC<{ children: React.ReactNode }> = ({
303
338
  [isProcessing]
304
339
  );
305
340
 
306
- const resetVisemeQueue = useCallback(() => {
307
- visemeQueueRef.current = [];
308
- lastVisemeRef.current = null;
309
- audioStartTimeRef.current = null;
310
- firstVisemeTimeRef.current = null;
311
- frameCountRef.current = 0;
312
- setVisemeState('idle');
313
-
314
- // logVisemeEvent('Reset Viseme Queue', {
315
- // previousState: visemeState,
316
- // });
317
- }, [visemeState]);
318
-
319
341
  const resetAndStartProcessing = useCallback(
320
342
  (audioCtx: IAudioContext) => {
321
343
  // logVisemeEvent('Reset And Start Processing', {
@@ -0,0 +1,71 @@
1
+ // tts/TextSanitizer.ts
2
+
3
+ /**
4
+ * Rimuove la formattazione Markdown dal testo
5
+ * @param text Testo da processare
6
+ */
7
+ export function stripMarkdown(text: string): string {
8
+ return text
9
+ .replace(/\*\*(.*?)\*\*/g, '$1') // Bold
10
+ .replace(/\*(.*?)\*/g, '$1') // Italic
11
+ .replace(/__(.*?)__/g, '$1') // Bold
12
+ .replace(/_(.*?)_/g, '$1') // Italic
13
+ .replace(/~~(.*?)~~/g, '$1') // Strikethrough
14
+ .replace(/```(.*?)```/gs, '$1') // Code blocks
15
+ .replace(/`(.*?)`/g, '$1') // Inline code
16
+ .replace(/\[(.*?)\]\((.*?)\)/g, '$1'); // Links
17
+ }
18
+
19
+ /**
20
+ * Rimuove gli emoji dal testo
21
+ * @param text Testo da processare
22
+ */
23
+ export function stripEmojis(text: string): string {
24
+ return text.replace(/[\u{1F600}-\u{1F64F}|\u{1F300}-\u{1F5FF}|\u{1F680}-\u{1F6FF}|\u{2600}-\u{26FF}|\u{2700}-\u{27BF}]/gu, '');
25
+ }
26
+
27
+ /**
28
+ * Rimuove i tag HTML dal testo
29
+ * @param text Testo da processare
30
+ */
31
+ export function stripHTML(text: string): string {
32
+ return text.replace(/<[^>]*>/g, '');
33
+ }
34
+
35
+ /**
36
+ * Rimuove i tag di output specifici dal testo
37
+ * @param text Testo da processare
38
+ */
39
+ export function stripOutputTags(text: string): string {
40
+ // Implementa se necessario per il tuo caso specifico
41
+ return text;
42
+ }
43
+
44
+ /**
45
+ * Esegue l'escape dei caratteri speciali HTML
46
+ * @param text Testo da processare
47
+ */
48
+ export function escapeHTML(text: string): string {
49
+ return text
50
+ .replace(/&/g, '&amp;')
51
+ .replace(/</g, '&lt;')
52
+ .replace(/>/g, '&gt;')
53
+ .replace(/"/g, '&quot;')
54
+ .replace(/'/g, '&apos;');
55
+ }
56
+
57
+ /**
58
+ * Sanitizza completamente il testo per la sintesi vocale
59
+ * @param text Testo da sanitizzare
60
+ */
61
+ export function sanitizeText(text: string): string {
62
+ return escapeHTML(
63
+ stripMarkdown(
64
+ stripEmojis(
65
+ stripHTML(
66
+ stripOutputTags(text)
67
+ )
68
+ )
69
+ )
70
+ );
71
+ }
@@ -0,0 +1,275 @@
1
+ // tts/voiceUtils.ts
2
+ import { TTSConfig } from './useTTS';
3
+
4
+ // Azure voices organized by language and gender
5
+ export const AZURE_VOICES = {
6
+ IT: {
7
+ MALE: 'it-IT-DiegoNeural',
8
+ FEMALE: 'it-IT-ElsaNeural'
9
+ },
10
+ DE: {
11
+ MALE: 'de-DE-ConradNeural',
12
+ FEMALE: 'de-DE-KatjaNeural'
13
+ },
14
+ EN: {
15
+ MALE: 'en-GB-RyanNeural',
16
+ FEMALE: 'en-GB-SoniaNeural'
17
+ },
18
+ ES: {
19
+ MALE: 'es-ES-AlvaroNeural',
20
+ FEMALE: 'es-ES-ElviraNeural'
21
+ },
22
+ FR: {
23
+ MALE: 'fr-FR-HenriNeural',
24
+ FEMALE: 'fr-FR-DeniseNeural'
25
+ },
26
+ PT: {
27
+ MALE: 'pt-PT-DuarteNeural',
28
+ FEMALE: 'pt-PT-RaquelNeural'
29
+ },
30
+ UK: {
31
+ MALE: 'uk-UA-OstapNeural',
32
+ FEMALE: 'uk-UA-PolinaNeural'
33
+ },
34
+ RU: {
35
+ MALE: 'ru-RU-DmitryNeural',
36
+ FEMALE: 'ru-RU-SvetlanaNeural'
37
+ },
38
+ PL: {
39
+ MALE: 'pl-PL-MarekNeural',
40
+ FEMALE: 'pl-PL-AgnieszkaNeural'
41
+ },
42
+ FI: {
43
+ MALE: 'fi-FI-HarriNeural',
44
+ FEMALE: 'fi-FI-SelmaNeural'
45
+ },
46
+ EL: {
47
+ MALE: 'el-GR-NestorasNeural',
48
+ FEMALE: 'el-GR-AthinaNeural'
49
+ },
50
+ AR: {
51
+ MALE: 'ar-SA-HamedNeural',
52
+ FEMALE: 'ar-SA-ZariyahNeural'
53
+ },
54
+ ZH: {
55
+ MALE: 'zh-CN-YunxiNeural',
56
+ FEMALE: 'zh-CN-XiaoxiaoNeural'
57
+ },
58
+ JA: {
59
+ MALE: 'ja-JP-KeitaNeural',
60
+ FEMALE: 'ja-JP-NanamiNeural'
61
+ },
62
+ // Add more languages as needed
63
+ };
64
+
65
+ // Default Azure voice if language not found
66
+ export const DEFAULT_AZURE_VOICE = {
67
+ MALE: 'en-US-GuyNeural',
68
+ FEMALE: 'en-US-JennyNeural'
69
+ };
70
+
71
+ // OpenAI voices mapped to approximate language/gender preferences
72
+ // Note: OpenAI voices don't correspond directly to languages, this is an approximation
73
+ export const OPENAI_VOICES = {
74
+ ALL: ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'], // All available voices
75
+ // Voice characteristics based on OpenAI's documentation
76
+ CHARACTERISTICS: {
77
+ 'alloy': { gender: 'NEUTRAL', tone: 'BALANCED' },
78
+ 'echo': { gender: 'MALE', tone: 'DEEP' },
79
+ 'fable': { gender: 'FEMALE', tone: 'EXPRESSIVE' },
80
+ 'onyx': { gender: 'MALE', tone: 'AUTHORITATIVE' },
81
+ 'nova': { gender: 'FEMALE', tone: 'FRIENDLY' },
82
+ 'shimmer': { gender: 'FEMALE', tone: 'BRIGHT' }
83
+ },
84
+ // Voice recommendations by language and gender
85
+ // This is subjective and can be adjusted based on preference
86
+ RECOMMENDED: {
87
+ DEFAULT: {
88
+ MALE: 'onyx',
89
+ FEMALE: 'nova',
90
+ NEUTRAL: 'alloy'
91
+ },
92
+ // Romance languages
93
+ IT: {
94
+ MALE: 'echo',
95
+ FEMALE: 'nova'
96
+ },
97
+ ES: {
98
+ MALE: 'echo',
99
+ FEMALE: 'shimmer'
100
+ },
101
+ FR: {
102
+ MALE: 'echo',
103
+ FEMALE: 'fable'
104
+ },
105
+ PT: {
106
+ MALE: 'onyx',
107
+ FEMALE: 'shimmer'
108
+ },
109
+ // Germanic languages
110
+ DE: {
111
+ MALE: 'onyx',
112
+ FEMALE: 'fable'
113
+ },
114
+ EN: {
115
+ MALE: 'echo',
116
+ FEMALE: 'nova'
117
+ },
118
+ // Other language families
119
+ ZH: {
120
+ MALE: 'echo',
121
+ FEMALE: 'shimmer'
122
+ },
123
+ JA: {
124
+ MALE: 'echo',
125
+ FEMALE: 'nova'
126
+ },
127
+ RU: {
128
+ MALE: 'onyx',
129
+ FEMALE: 'fable'
130
+ }
131
+ // Add more languages as needed
132
+ }
133
+ };
134
+
135
+ // Default OpenAI voice
136
+ export const DEFAULT_OPENAI_VOICE = 'alloy';
137
+
138
+ // Provider configurations
139
+ export const PROVIDER_CONFIG = {
140
+ azure: {
141
+ defaultVoice: DEFAULT_AZURE_VOICE.FEMALE,
142
+ defaultRegion: 'westeurope',
143
+ defaultModel: null, // Azure doesn't use model parameter in the same way
144
+ endpoint: (region: string) =>
145
+ `https://${region}.tts.speech.microsoft.com/cognitiveservices/v1`,
146
+ outputFormat: 'audio-24khz-48kbitrate-mono-mp3'
147
+ },
148
+ openai: {
149
+ defaultVoice: DEFAULT_OPENAI_VOICE,
150
+ defaultModel: 'tts-1',
151
+ voices: OPENAI_VOICES.ALL,
152
+ endpoint: 'https://api.openai.com/v1/audio/speech'
153
+ }
154
+ };
155
+
156
+ /**
157
+ * Gets appropriate voice for the selected provider and language
158
+ *
159
+ * @param {string} lang - Language code (e.g., 'IT', 'EN')
160
+ * @param {string} provider - TTS provider ('azure' or 'openai')
161
+ * @param {string} voiceType - Voice gender preference ('MALE' or 'FEMALE')
162
+ * @returns {string} Voice identifier for the selected provider
163
+ */
164
+ export function getTTSVoice(
165
+ lang?: string,
166
+ provider: 'azure' | 'openai' = 'azure',
167
+ voiceType: 'MALE' | 'FEMALE' | 'NEUTRAL' = 'FEMALE'
168
+ ): string {
169
+ // Normalize language code
170
+ const voiceLang = (lang || 'EN').toUpperCase();
171
+
172
+ // Handle different providers
173
+ if (provider.toLowerCase() === 'openai') {
174
+ // For OpenAI, get recommended voice by language and gender
175
+ const langMap = OPENAI_VOICES.RECOMMENDED[voiceLang as keyof typeof OPENAI_VOICES.RECOMMENDED] || OPENAI_VOICES.RECOMMENDED.DEFAULT;
176
+ return langMap[voiceType as keyof typeof langMap] || OPENAI_VOICES.RECOMMENDED.DEFAULT[voiceType] || DEFAULT_OPENAI_VOICE;
177
+ } else {
178
+ // For Azure, get neural voice by language and gender
179
+ const langVoices = AZURE_VOICES[voiceLang as keyof typeof AZURE_VOICES] || DEFAULT_AZURE_VOICE;
180
+ return langVoices[voiceType as keyof typeof langVoices] || langVoices.FEMALE || DEFAULT_AZURE_VOICE.FEMALE;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Validates if a voice is supported by the provider
186
+ *
187
+ * @param {string} voice - Voice identifier to validate
188
+ * @param {string} provider - TTS provider ('azure' or 'openai')
189
+ * @returns {boolean} True if voice is valid for the provider
190
+ */
191
+ export function isValidVoice(voice: string, provider: 'azure' | 'openai'): boolean {
192
+ if (provider.toLowerCase() === 'openai') {
193
+ return OPENAI_VOICES.ALL.includes(voice);
194
+ }
195
+
196
+ // For Azure, check if it follows the format pattern (simple validation)
197
+ return /^[a-z]{2}-[A-Z]{2}-[A-Za-z]+Neural$/.test(voice);
198
+ }
199
+
200
+ /**
201
+ * Gets default voice for the provider
202
+ *
203
+ * @param {string} provider - TTS provider ('azure' or 'openai')
204
+ * @param {string} voiceType - Voice gender preference ('MALE' or 'FEMALE')
205
+ * @returns {string} Default voice for the provider
206
+ */
207
+ export function getDefaultVoice(
208
+ provider: 'azure' | 'openai',
209
+ voiceType: 'MALE' | 'FEMALE' = 'FEMALE'
210
+ ): string {
211
+ if (provider.toLowerCase() === 'openai') {
212
+ return OPENAI_VOICES.RECOMMENDED.DEFAULT[voiceType] || DEFAULT_OPENAI_VOICE;
213
+ } else {
214
+ return DEFAULT_AZURE_VOICE[voiceType] || DEFAULT_AZURE_VOICE.FEMALE;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Get appropriate default region for the provider
220
+ *
221
+ * @param provider - TTS provider ('azure' or 'openai')
222
+ * @returns Default region for the provider
223
+ */
224
+ export function getDefaultRegion(provider: 'azure' | 'openai'): string | null {
225
+ return (PROVIDER_CONFIG[provider] as { defaultRegion: string }).defaultRegion || null;
226
+ }
227
+
228
+ /**
229
+ * Get appropriate default model for the provider
230
+ *
231
+ * @param provider - TTS provider ('azure' or 'openai')
232
+ * @returns Default model for the provider
233
+ */
234
+ export function getDefaultModel(provider: 'azure' | 'openai'): string | null {
235
+ return (PROVIDER_CONFIG[provider] as { defaultModel: string }).defaultModel || null;
236
+ }
237
+
238
+ /**
239
+ * Ensures voice is valid for provider, or returns default
240
+ *
241
+ * @param voice - Voice to validate
242
+ * @param provider - TTS provider ('azure' or 'openai')
243
+ * @param voiceType - Fallback voice type if invalid
244
+ * @returns Valid voice for the provider
245
+ */
246
+ export function ensureValidVoice(
247
+ voice: string,
248
+ provider: 'azure' | 'openai',
249
+ voiceType: 'MALE' | 'FEMALE' = 'FEMALE'
250
+ ): string {
251
+ if (!voice || !isValidVoice(voice, provider)) {
252
+ return getDefaultVoice(provider, voiceType);
253
+ }
254
+ return voice;
255
+ }
256
+
257
+ /**
258
+ * Creates the appropriate provider-specific TTS configuration
259
+ *
260
+ * @param config - Base TTS configuration
261
+ * @returns Provider-specific configuration with defaults
262
+ */
263
+ export function createTTSConfiguration(config: Partial<TTSConfig>): TTSConfig {
264
+ const provider = config.provider || 'azure';
265
+ const voiceType = (config as { voiceType?: 'MALE' | 'FEMALE' }).voiceType || 'FEMALE';
266
+
267
+ return {
268
+ provider,
269
+ voice: config.voice || getDefaultVoice(provider, voiceType),
270
+ model: config.model || getDefaultModel(provider),
271
+ region: config.region || getDefaultRegion(provider),
272
+ tenant: config.tenant || 'www.aisuru.com',
273
+ voiceType
274
+ } as TTSConfig;
275
+ }