@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.
- package/CHANGELOG.md +35 -0
- package/dist/components/Chat/Chat.d.ts +1 -0
- package/dist/components/Chat/Chat.js +2 -2
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/ChatInputs/ChatInputs.d.ts +1 -0
- package/dist/components/ChatInputs/ChatInputs.js +3 -3
- package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
- package/dist/components/MemoriWidget/MemoriWidget.d.ts +3 -3
- package/dist/components/MemoriWidget/MemoriWidget.js +138 -425
- package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/dist/context/visemeContext.js +39 -30
- package/dist/context/visemeContext.js.map +1 -1
- package/dist/helpers/sanitizer.d.ts +6 -0
- package/dist/helpers/sanitizer.js +41 -0
- package/dist/helpers/sanitizer.js.map +1 -0
- package/dist/helpers/tts/ttsVoiceUtility.d.ts +158 -0
- package/dist/helpers/tts/ttsVoiceUtility.js +192 -0
- package/dist/helpers/tts/ttsVoiceUtility.js.map +1 -0
- package/dist/helpers/tts/useTTS.d.ts +26 -0
- package/dist/helpers/tts/useTTS.js +274 -0
- package/dist/helpers/tts/useTTS.js.map +1 -0
- package/dist/index.js +12 -7
- package/dist/index.js.map +1 -1
- package/esm/components/Chat/Chat.d.ts +1 -0
- package/esm/components/Chat/Chat.js +2 -2
- package/esm/components/Chat/Chat.js.map +1 -1
- package/esm/components/ChatInputs/ChatInputs.d.ts +1 -0
- package/esm/components/ChatInputs/ChatInputs.js +3 -3
- package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
- package/esm/components/MemoriWidget/MemoriWidget.d.ts +3 -3
- package/esm/components/MemoriWidget/MemoriWidget.js +139 -426
- package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/esm/context/visemeContext.js +39 -30
- package/esm/context/visemeContext.js.map +1 -1
- package/esm/helpers/sanitizer.d.ts +6 -0
- package/esm/helpers/sanitizer.js +32 -0
- package/esm/helpers/sanitizer.js.map +1 -0
- package/esm/helpers/tts/ttsVoiceUtility.d.ts +158 -0
- package/esm/helpers/tts/ttsVoiceUtility.js +182 -0
- package/esm/helpers/tts/ttsVoiceUtility.js.map +1 -0
- package/esm/helpers/tts/useTTS.d.ts +26 -0
- package/esm/helpers/tts/useTTS.js +270 -0
- package/esm/helpers/tts/useTTS.js.map +1 -0
- package/esm/index.js +12 -7
- package/esm/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Chat/Chat.tsx +3 -0
- package/src/components/ChatInputs/ChatInputs.tsx +4 -2
- package/src/components/MemoriWidget/MemoriWidget.tsx +246 -637
- package/src/context/visemeContext.tsx +77 -55
- package/src/helpers/sanitizer.ts +71 -0
- package/src/helpers/tts/ttsVoiceUtility.ts +275 -0
- package/src/helpers/tts/useTTS.ts +431 -0
- 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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
219
|
-
|
|
255
|
+
// Reset all other state
|
|
256
|
+
lastVisemeRef.current = null;
|
|
257
|
+
audioStartTimeRef.current = null;
|
|
258
|
+
firstVisemeTimeRef.current = null;
|
|
220
259
|
frameCountRef.current = 0;
|
|
221
|
-
|
|
222
|
-
|
|
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, '&')
|
|
51
|
+
.replace(/</g, '<')
|
|
52
|
+
.replace(/>/g, '>')
|
|
53
|
+
.replace(/"/g, '"')
|
|
54
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|