@memori.ai/memori-react 8.0.2 → 8.1.0-rc.0
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 +25 -0
- package/dist/components/Chat/Chat.d.ts +0 -2
- package/dist/components/Chat/Chat.js +2 -2
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/ChatHistoryDrawer/ChatHistory.css +32 -0
- package/dist/components/ChatHistoryDrawer/ChatHistory.js +104 -31
- package/dist/components/ChatHistoryDrawer/ChatHistory.js.map +1 -1
- package/dist/components/ChatInputs/ChatInputs.d.ts +0 -2
- package/dist/components/ChatInputs/ChatInputs.js +3 -4
- package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
- package/dist/components/MemoriWidget/MemoriWidget.js +103 -329
- package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/dist/helpers/stt/useSTT.d.ts +40 -0
- package/dist/helpers/stt/useSTT.js +362 -0
- package/dist/helpers/stt/useSTT.js.map +1 -0
- package/dist/locales/de.json +11 -0
- package/dist/locales/en.json +11 -0
- package/dist/locales/es.json +11 -0
- package/dist/locales/fr.json +11 -0
- package/dist/locales/it.json +11 -0
- package/esm/components/Chat/Chat.d.ts +0 -2
- package/esm/components/Chat/Chat.js +2 -2
- package/esm/components/Chat/Chat.js.map +1 -1
- package/esm/components/ChatHistoryDrawer/ChatHistory.css +32 -0
- package/esm/components/ChatHistoryDrawer/ChatHistory.js +104 -31
- package/esm/components/ChatHistoryDrawer/ChatHistory.js.map +1 -1
- package/esm/components/ChatInputs/ChatInputs.d.ts +0 -2
- package/esm/components/ChatInputs/ChatInputs.js +3 -4
- package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
- package/esm/components/MemoriWidget/MemoriWidget.js +103 -329
- package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/esm/helpers/stt/useSTT.d.ts +40 -0
- package/esm/helpers/stt/useSTT.js +358 -0
- package/esm/helpers/stt/useSTT.js.map +1 -0
- package/esm/locales/de.json +11 -0
- package/esm/locales/en.json +11 -0
- package/esm/locales/es.json +11 -0
- package/esm/locales/fr.json +11 -0
- package/esm/locales/it.json +11 -0
- package/package.json +2 -3
- package/src/components/Chat/Chat.test.tsx +0 -9
- package/src/components/Chat/Chat.tsx +0 -6
- package/src/components/ChatHistoryDrawer/ChatHistory.css +32 -0
- package/src/components/ChatHistoryDrawer/ChatHistory.tsx +114 -57
- package/src/components/ChatInputs/ChatInputs.test.tsx +0 -6
- package/src/components/ChatInputs/ChatInputs.tsx +2 -7
- package/src/components/MemoriWidget/MemoriWidget.tsx +152 -476
- package/src/helpers/stt/useSTT.ts +551 -0
- package/src/locales/de.json +11 -0
- package/src/locales/en.json +11 -0
- package/src/locales/es.json +11 -0
- package/src/locales/fr.json +11 -0
- package/src/locales/it.json +11 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
// hooks/useSTT.ts
|
|
2
|
+
// Audio format compatibility:
|
|
3
|
+
// - MediaRecorder supports: webm, mp4, ogg formats
|
|
4
|
+
// - Azure STT supported formats:
|
|
5
|
+
// * webm-16khz-16bit-mono-opus (recommended for this config)
|
|
6
|
+
// * webm-24khz-16bit-24kbps-mono-opus
|
|
7
|
+
// * webm-24khz-16bit-mono-opus
|
|
8
|
+
// - OpenAI: Supports multiple formats including webm, mp4, ogg
|
|
9
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
10
|
+
import { getLocalConfig } from '../configuration';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Configuration for STT
|
|
14
|
+
*/
|
|
15
|
+
export interface STTConfig {
|
|
16
|
+
provider: 'azure' | 'openai';
|
|
17
|
+
language?: string;
|
|
18
|
+
model?: string;
|
|
19
|
+
region?: string; // required for Azure
|
|
20
|
+
tenant?: string; // Tenant identifier for multi-tenant applications
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Result from STT transcription
|
|
25
|
+
*/
|
|
26
|
+
export interface STTResult {
|
|
27
|
+
text: string;
|
|
28
|
+
confidence?: number;
|
|
29
|
+
language?: string;
|
|
30
|
+
duration?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Options for the useSTT hook
|
|
35
|
+
*/
|
|
36
|
+
export interface UseSTTOptions {
|
|
37
|
+
apiUrl?: string;
|
|
38
|
+
onTranscriptionComplete?: (result: STTResult) => void;
|
|
39
|
+
onError?: (error: Error) => void;
|
|
40
|
+
continuousRecording?: boolean;
|
|
41
|
+
autoStart?: boolean;
|
|
42
|
+
processSpeechAndSendMessage?: (text: string) => void;
|
|
43
|
+
silenceTimeout?: number; // Timeout in milliseconds for silence detection
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Recording states
|
|
48
|
+
*/
|
|
49
|
+
export type RecordingState = 'idle' | 'recording' | 'processing' | 'error';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Unified hook for handling Speech-to-Text functionality
|
|
53
|
+
*/
|
|
54
|
+
export function useSTT(
|
|
55
|
+
config: STTConfig,
|
|
56
|
+
processSpeechAndSendMessage: (text: string) => void,
|
|
57
|
+
options: UseSTTOptions = {},
|
|
58
|
+
defaultEnableAudio: boolean = true
|
|
59
|
+
) {
|
|
60
|
+
// Local state
|
|
61
|
+
const [recordingState, setRecordingState] = useState<RecordingState>('idle');
|
|
62
|
+
const [microphoneMuted, setMicrophoneMuted] = useState(
|
|
63
|
+
getLocalConfig('muteMicrophone', !defaultEnableAudio)
|
|
64
|
+
);
|
|
65
|
+
const [hasUserActivatedRecord, setHasUserActivatedRecord] = useState(false);
|
|
66
|
+
const [error, setError] = useState<Error | null>(null);
|
|
67
|
+
const [lastTranscription, setLastTranscription] = useState<STTResult | null>(
|
|
68
|
+
null
|
|
69
|
+
);
|
|
70
|
+
const [isListening, setIsListening] = useState(false);
|
|
71
|
+
|
|
72
|
+
// References
|
|
73
|
+
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
|
74
|
+
const audioStreamRef = useRef<MediaStream | null>(null);
|
|
75
|
+
const chunksRef = useRef<Blob[]>([]);
|
|
76
|
+
const isRecordingRef = useRef<boolean>(false);
|
|
77
|
+
const isMountedRef = useRef<boolean>(true);
|
|
78
|
+
const silenceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
79
|
+
const audioContextRef = useRef<AudioContext | null>(null);
|
|
80
|
+
const analyserRef = useRef<AnalyserNode | null>(null);
|
|
81
|
+
const dataArrayRef = useRef<Uint8Array | null>(null);
|
|
82
|
+
const apiUrl = options.apiUrl || '/api/stt';
|
|
83
|
+
const silenceTimeout = options.silenceTimeout || 2; // Default 300ms
|
|
84
|
+
|
|
85
|
+
// Replace the initializeRecording function in your useSTT.ts with this:
|
|
86
|
+
|
|
87
|
+
const initializeRecording = useCallback(async (): Promise<boolean> => {
|
|
88
|
+
try {
|
|
89
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
90
|
+
throw new Error('Media recording is not supported in this browser');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
94
|
+
audio: {
|
|
95
|
+
echoCancellation: true,
|
|
96
|
+
noiseSuppression: true,
|
|
97
|
+
autoGainControl: true,
|
|
98
|
+
sampleRate: 16000, // Optimal for speech recognition
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
audioStreamRef.current = stream;
|
|
103
|
+
|
|
104
|
+
// Initialize audio context for silence detection if continuous recording is enabled
|
|
105
|
+
if (options.continuousRecording) {
|
|
106
|
+
try {
|
|
107
|
+
audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
|
|
108
|
+
analyserRef.current = audioContextRef.current.createAnalyser();
|
|
109
|
+
analyserRef.current.fftSize = 256;
|
|
110
|
+
const bufferLength = analyserRef.current.frequencyBinCount;
|
|
111
|
+
dataArrayRef.current = new Uint8Array(bufferLength);
|
|
112
|
+
|
|
113
|
+
const source = audioContextRef.current.createMediaStreamSource(stream);
|
|
114
|
+
source.connect(analyserRef.current);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
// Silence detection initialization failed but we can continue
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Format selection based on provider and browser support
|
|
121
|
+
let mimeType = '';
|
|
122
|
+
|
|
123
|
+
if (config.provider === 'azure') {
|
|
124
|
+
if (MediaRecorder.isTypeSupported('audio/mp4')) {
|
|
125
|
+
mimeType = 'audio/mp4';
|
|
126
|
+
} else if (MediaRecorder.isTypeSupported('audio/ogg;codecs=opus')) {
|
|
127
|
+
mimeType = 'audio/ogg;codecs=opus';
|
|
128
|
+
} else if (MediaRecorder.isTypeSupported('audio/ogg')) {
|
|
129
|
+
mimeType = 'audio/ogg';
|
|
130
|
+
} else if (MediaRecorder.isTypeSupported('audio/webm')) {
|
|
131
|
+
mimeType = 'audio/webm';
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) {
|
|
135
|
+
mimeType = 'audio/webm;codecs=opus';
|
|
136
|
+
} else if (MediaRecorder.isTypeSupported('audio/webm')) {
|
|
137
|
+
mimeType = 'audio/webm';
|
|
138
|
+
} else if (MediaRecorder.isTypeSupported('audio/mp4')) {
|
|
139
|
+
mimeType = 'audio/mp4';
|
|
140
|
+
} else if (MediaRecorder.isTypeSupported('audio/ogg;codecs=opus')) {
|
|
141
|
+
mimeType = 'audio/ogg;codecs=opus';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const mediaRecorder = new MediaRecorder(
|
|
146
|
+
stream,
|
|
147
|
+
mimeType ? { mimeType } : {}
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
mediaRecorder.ondataavailable = (event: BlobEvent) => {
|
|
151
|
+
if (event.data.size > 0) {
|
|
152
|
+
chunksRef.current.push(event.data);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
mediaRecorder.onstop = async () => {
|
|
157
|
+
if (!isRecordingRef.current || !isMountedRef.current) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
setRecordingState('processing');
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
if (chunksRef.current.length === 0) {
|
|
165
|
+
throw new Error('No audio data recorded');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const audioBlob = new Blob(chunksRef.current, {
|
|
169
|
+
type: mediaRecorder.mimeType,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (audioBlob.size === 0) {
|
|
173
|
+
throw new Error('Recorded audio is empty');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result = await transcribeAudio(audioBlob);
|
|
177
|
+
|
|
178
|
+
if (processSpeechAndSendMessage) {
|
|
179
|
+
processSpeechAndSendMessage(result.text);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
setLastTranscription(result);
|
|
183
|
+
|
|
184
|
+
if (options.onTranscriptionComplete) {
|
|
185
|
+
options.onTranscriptionComplete(result);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
setRecordingState('idle');
|
|
189
|
+
} catch (err) {
|
|
190
|
+
const errorMsg = err instanceof Error ? err : new Error(String(err));
|
|
191
|
+
setError(errorMsg);
|
|
192
|
+
setRecordingState('error');
|
|
193
|
+
|
|
194
|
+
if (options.onError) {
|
|
195
|
+
options.onError(errorMsg);
|
|
196
|
+
}
|
|
197
|
+
} finally {
|
|
198
|
+
chunksRef.current = [];
|
|
199
|
+
isRecordingRef.current = false;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
mediaRecorder.onerror = () => {
|
|
204
|
+
const errorMsg = new Error('Recording failed');
|
|
205
|
+
setError(errorMsg);
|
|
206
|
+
setRecordingState('error');
|
|
207
|
+
isRecordingRef.current = false;
|
|
208
|
+
|
|
209
|
+
if (options.onError) {
|
|
210
|
+
options.onError(errorMsg);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
mediaRecorderRef.current = mediaRecorder;
|
|
215
|
+
return true;
|
|
216
|
+
} catch (err) {
|
|
217
|
+
const errorMsg =
|
|
218
|
+
err instanceof Error ? err : new Error('Failed to access microphone');
|
|
219
|
+
setError(errorMsg);
|
|
220
|
+
setRecordingState('error');
|
|
221
|
+
|
|
222
|
+
if (options.onError) {
|
|
223
|
+
options.onError(errorMsg);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}, [config.provider, options, silenceTimeout]); // Add silenceTimeout to dependencies
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Detect if there's audio activity (not silence)
|
|
232
|
+
*/
|
|
233
|
+
const detectAudioActivity = useCallback((): boolean => {
|
|
234
|
+
if (!analyserRef.current || !dataArrayRef.current) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
analyserRef.current.getByteFrequencyData(dataArrayRef.current);
|
|
239
|
+
|
|
240
|
+
// Calculate average volume level
|
|
241
|
+
const average = dataArrayRef.current.reduce((sum, value) => sum + value, 0) / dataArrayRef.current.length;
|
|
242
|
+
|
|
243
|
+
// Consider audio active if average volume is above threshold
|
|
244
|
+
// Adjust this threshold based on your needs
|
|
245
|
+
const threshold = 10; // Adjust this value as needed
|
|
246
|
+
return average > threshold;
|
|
247
|
+
}, []);
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Start silence detection monitoring
|
|
251
|
+
*/
|
|
252
|
+
const startSilenceDetection = useCallback(() => {
|
|
253
|
+
if (!options.continuousRecording || !analyserRef.current) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const checkAudioActivity = () => {
|
|
258
|
+
if (!isRecordingRef.current || !isMountedRef.current) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const hasActivity = detectAudioActivity();
|
|
263
|
+
|
|
264
|
+
if (hasActivity) {
|
|
265
|
+
// Reset silence timeout when audio activity is detected
|
|
266
|
+
if (silenceTimeoutRef.current) {
|
|
267
|
+
clearTimeout(silenceTimeoutRef.current);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Set new timeout for when user stops speaking
|
|
271
|
+
silenceTimeoutRef.current = setTimeout(() => {
|
|
272
|
+
if (isRecordingRef.current && isMountedRef.current) {
|
|
273
|
+
stopRecording();
|
|
274
|
+
}
|
|
275
|
+
}, silenceTimeout * 1000);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Check audio activity every 50ms for responsive detection
|
|
280
|
+
const intervalId = setInterval(checkAudioActivity, 50);
|
|
281
|
+
|
|
282
|
+
// Store interval ID for cleanup
|
|
283
|
+
(window as any).memoriSilenceDetectionInterval = intervalId;
|
|
284
|
+
}, [options.continuousRecording, detectAudioActivity, silenceTimeout]);
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Stop silence detection monitoring
|
|
288
|
+
*/
|
|
289
|
+
const stopSilenceDetection = useCallback(() => {
|
|
290
|
+
if (silenceTimeoutRef.current) {
|
|
291
|
+
clearTimeout(silenceTimeoutRef.current);
|
|
292
|
+
silenceTimeoutRef.current = null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if ((window as any).memoriSilenceDetectionInterval) {
|
|
296
|
+
clearInterval((window as any).memoriSilenceDetectionInterval);
|
|
297
|
+
(window as any).memoriSilenceDetectionInterval = null;
|
|
298
|
+
}
|
|
299
|
+
}, []);
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Transcribe audio blob using the API
|
|
303
|
+
*/
|
|
304
|
+
const transcribeAudio = useCallback(
|
|
305
|
+
async (audioBlob: Blob): Promise<STTResult> => {
|
|
306
|
+
const formData = new FormData();
|
|
307
|
+
let fileExtension = 'webm'; // default fallback
|
|
308
|
+
|
|
309
|
+
if (mediaRecorderRef.current?.mimeType) {
|
|
310
|
+
if (mediaRecorderRef.current.mimeType.includes('webm')) {
|
|
311
|
+
fileExtension = 'webm';
|
|
312
|
+
} else if (mediaRecorderRef.current.mimeType.includes('mp4')) {
|
|
313
|
+
fileExtension = 'mp4';
|
|
314
|
+
} else if (mediaRecorderRef.current.mimeType.includes('ogg')) {
|
|
315
|
+
fileExtension = 'ogg';
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
formData.append('audio', audioBlob, `recording.${fileExtension}`);
|
|
320
|
+
formData.append('provider', config.provider);
|
|
321
|
+
formData.append('tenant', config.tenant || 'www.aisuru.com');
|
|
322
|
+
|
|
323
|
+
if (config.language) {
|
|
324
|
+
formData.append('language', config.language);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (config.model) {
|
|
328
|
+
formData.append('model', config.model);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (config.region) {
|
|
332
|
+
formData.append('region', config.region);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const response = await fetch(apiUrl, {
|
|
336
|
+
method: 'POST',
|
|
337
|
+
body: formData,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
if (!response.ok) {
|
|
341
|
+
const errorData = await response.json().catch(() => ({}));
|
|
342
|
+
throw new Error(errorData.error || `API error: ${response.status}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const data = await response.json();
|
|
346
|
+
|
|
347
|
+
if (!data.success || !data.result) {
|
|
348
|
+
throw new Error('Invalid response from transcription service');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return data.result;
|
|
352
|
+
},
|
|
353
|
+
[config, apiUrl]
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Start recording audio
|
|
358
|
+
*/
|
|
359
|
+
const startRecording = useCallback(async (): Promise<void> => {
|
|
360
|
+
if (
|
|
361
|
+
!isMountedRef.current ||
|
|
362
|
+
microphoneMuted ||
|
|
363
|
+
recordingState === 'recording'
|
|
364
|
+
) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!hasUserActivatedRecord) {
|
|
369
|
+
setHasUserActivatedRecord(true);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
setError(null);
|
|
374
|
+
setRecordingState('recording');
|
|
375
|
+
|
|
376
|
+
// Initialize recording if needed
|
|
377
|
+
if (!mediaRecorderRef.current) {
|
|
378
|
+
const initialized = await initializeRecording();
|
|
379
|
+
if (!initialized) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Reset chunks and start recording
|
|
385
|
+
chunksRef.current = [];
|
|
386
|
+
isRecordingRef.current = true;
|
|
387
|
+
|
|
388
|
+
if (
|
|
389
|
+
mediaRecorderRef.current &&
|
|
390
|
+
mediaRecorderRef.current.state === 'inactive'
|
|
391
|
+
) {
|
|
392
|
+
mediaRecorderRef.current.start(100); // Collect data every 100ms
|
|
393
|
+
setIsListening(true);
|
|
394
|
+
|
|
395
|
+
// Start silence detection if continuous recording is enabled
|
|
396
|
+
if (options.continuousRecording) {
|
|
397
|
+
startSilenceDetection();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
} catch (err) {
|
|
401
|
+
const errorMsg =
|
|
402
|
+
err instanceof Error ? err : new Error('Failed to start recording');
|
|
403
|
+
setError(errorMsg);
|
|
404
|
+
setRecordingState('error');
|
|
405
|
+
isRecordingRef.current = false;
|
|
406
|
+
|
|
407
|
+
if (options.onError) {
|
|
408
|
+
options.onError(errorMsg);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}, [
|
|
412
|
+
microphoneMuted,
|
|
413
|
+
recordingState,
|
|
414
|
+
hasUserActivatedRecord,
|
|
415
|
+
initializeRecording,
|
|
416
|
+
options,
|
|
417
|
+
startSilenceDetection,
|
|
418
|
+
]);
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Stop recording audio
|
|
422
|
+
*/
|
|
423
|
+
const stopRecording = useCallback((): void => {
|
|
424
|
+
if (!isRecordingRef.current) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
setIsListening(false);
|
|
430
|
+
|
|
431
|
+
// Stop silence detection
|
|
432
|
+
stopSilenceDetection();
|
|
433
|
+
|
|
434
|
+
if (
|
|
435
|
+
mediaRecorderRef.current &&
|
|
436
|
+
mediaRecorderRef.current.state === 'recording'
|
|
437
|
+
) {
|
|
438
|
+
mediaRecorderRef.current.stop();
|
|
439
|
+
}
|
|
440
|
+
} catch (err) {
|
|
441
|
+
const errorMsg =
|
|
442
|
+
err instanceof Error ? err : new Error('Failed to stop recording');
|
|
443
|
+
setError(errorMsg);
|
|
444
|
+
setRecordingState('error');
|
|
445
|
+
isRecordingRef.current = false;
|
|
446
|
+
|
|
447
|
+
if (options.onError) {
|
|
448
|
+
options.onError(errorMsg);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}, [recordingState, options, stopSilenceDetection]);
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Toggle recording state
|
|
455
|
+
*/
|
|
456
|
+
const toggleRecording = useCallback(async (): Promise<void> => {
|
|
457
|
+
if (recordingState === 'recording') {
|
|
458
|
+
stopRecording();
|
|
459
|
+
} else if (recordingState === 'idle') {
|
|
460
|
+
await startRecording();
|
|
461
|
+
}
|
|
462
|
+
}, [recordingState, startRecording, stopRecording]);
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Toggle microphone mute state
|
|
466
|
+
*/
|
|
467
|
+
const toggleMute = useCallback(
|
|
468
|
+
(mute?: boolean): void => {
|
|
469
|
+
const newMuteState = mute !== undefined ? mute : !microphoneMuted;
|
|
470
|
+
setMicrophoneMuted(newMuteState);
|
|
471
|
+
|
|
472
|
+
if (newMuteState && recordingState === 'recording') {
|
|
473
|
+
stopRecording();
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
[microphoneMuted, recordingState, stopRecording]
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Clean up resources
|
|
481
|
+
*/
|
|
482
|
+
const cleanup = useCallback((): void => {
|
|
483
|
+
isRecordingRef.current = false;
|
|
484
|
+
|
|
485
|
+
if (mediaRecorderRef.current) {
|
|
486
|
+
if (mediaRecorderRef.current.state === 'recording') {
|
|
487
|
+
mediaRecorderRef.current.stop();
|
|
488
|
+
}
|
|
489
|
+
mediaRecorderRef.current = null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (audioStreamRef.current) {
|
|
493
|
+
audioStreamRef.current.getTracks().forEach(track => track.stop());
|
|
494
|
+
audioStreamRef.current = null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Clean up audio context
|
|
498
|
+
if (audioContextRef.current) {
|
|
499
|
+
audioContextRef.current.close();
|
|
500
|
+
audioContextRef.current = null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Stop silence detection
|
|
504
|
+
stopSilenceDetection();
|
|
505
|
+
|
|
506
|
+
chunksRef.current = [];
|
|
507
|
+
setIsListening(false);
|
|
508
|
+
setRecordingState('idle');
|
|
509
|
+
}, [stopSilenceDetection]);
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Cleanup on unmount
|
|
513
|
+
*/
|
|
514
|
+
useEffect(() => {
|
|
515
|
+
return () => {
|
|
516
|
+
isMountedRef.current = false;
|
|
517
|
+
cleanup();
|
|
518
|
+
};
|
|
519
|
+
}, [cleanup]);
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Update global variables
|
|
523
|
+
*/
|
|
524
|
+
useEffect(() => {
|
|
525
|
+
if (typeof window !== 'undefined') {
|
|
526
|
+
(window as any).memoriListening = isListening;
|
|
527
|
+
}
|
|
528
|
+
}, [isListening]);
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
// State
|
|
532
|
+
recordingState,
|
|
533
|
+
microphoneMuted,
|
|
534
|
+
hasUserActivatedRecord,
|
|
535
|
+
error,
|
|
536
|
+
lastTranscription,
|
|
537
|
+
isListening,
|
|
538
|
+
|
|
539
|
+
// Actions
|
|
540
|
+
startRecording,
|
|
541
|
+
stopRecording,
|
|
542
|
+
toggleRecording,
|
|
543
|
+
toggleMute,
|
|
544
|
+
transcribeAudio,
|
|
545
|
+
setHasUserActivatedRecord,
|
|
546
|
+
setError,
|
|
547
|
+
|
|
548
|
+
// Utils
|
|
549
|
+
cleanup,
|
|
550
|
+
};
|
|
551
|
+
}
|
package/src/locales/de.json
CHANGED
|
@@ -270,6 +270,17 @@
|
|
|
270
270
|
"editAccount": "Konto bearbeiten",
|
|
271
271
|
"save": "Speichern"
|
|
272
272
|
},
|
|
273
|
+
"chatLogs": {
|
|
274
|
+
"anyMessage": "Jede Nachricht",
|
|
275
|
+
"atLeast": "Mindestens {{count}} Nachrichten",
|
|
276
|
+
"atLeast2": "Mindestens 2 Nachrichten",
|
|
277
|
+
"atLeast3": "Mindestens 3 Nachrichten",
|
|
278
|
+
"atLeast5": "Mindestens 5 Nachrichten",
|
|
279
|
+
"atLeast10": "Mindestens 10 Nachrichten",
|
|
280
|
+
"atLeast15": "Mindestens 15 Nachrichten",
|
|
281
|
+
"atLeast20": "Mindestens 20 Nachrichten",
|
|
282
|
+
"customMinimumMessages": "Anpassen Sie die Anzahl der Nachrichten"
|
|
283
|
+
},
|
|
273
284
|
"success": "Erfolg",
|
|
274
285
|
"Error": "Fehler",
|
|
275
286
|
"internal server error": "Oupsie, tut mir leid... Auf dem Server ist ein Fehler aufgetreten",
|
package/src/locales/en.json
CHANGED
|
@@ -292,6 +292,17 @@
|
|
|
292
292
|
"editAccount": "Edit account",
|
|
293
293
|
"save": "Save"
|
|
294
294
|
},
|
|
295
|
+
"chatLogs": {
|
|
296
|
+
"anyMessage": "Any message",
|
|
297
|
+
"atLeast": "At least {{count}} messages",
|
|
298
|
+
"atLeast2": "At least 2 messages",
|
|
299
|
+
"atLeast3": "At least 3 messages",
|
|
300
|
+
"atLeast5": "At least 5 messages",
|
|
301
|
+
"atLeast10": "At least 10 messages",
|
|
302
|
+
"atLeast15": "At least 15 messages",
|
|
303
|
+
"atLeast20": "At least 20 messages",
|
|
304
|
+
"customMinimumMessages": "Customize the number of messages"
|
|
305
|
+
},
|
|
295
306
|
"success": "Success",
|
|
296
307
|
"Error": "Error",
|
|
297
308
|
"internal server error": "Oupsie, sorry... Something went wrong on the server",
|
package/src/locales/es.json
CHANGED
|
@@ -270,6 +270,17 @@
|
|
|
270
270
|
"editAccount": "Editar cuenta",
|
|
271
271
|
"save": "Ahorrar"
|
|
272
272
|
},
|
|
273
|
+
"chatLogs": {
|
|
274
|
+
"anyMessage": "Cualquier mensaje",
|
|
275
|
+
"atLeast": "Al menos {{count}} mensajes",
|
|
276
|
+
"atLeast2": "Al menos 2 mensajes",
|
|
277
|
+
"atLeast3": "Al menos 3 mensajes",
|
|
278
|
+
"atLeast5": "Al menos 5 mensajes",
|
|
279
|
+
"atLeast10": "Al menos 10 mensajes",
|
|
280
|
+
"atLeast15": "Al menos 15 mensajes",
|
|
281
|
+
"atLeast20": "Al menos 20 mensajes",
|
|
282
|
+
"customMinimumMessages": "Personalizar el número de mensajes"
|
|
283
|
+
},
|
|
273
284
|
"success": "Éxito",
|
|
274
285
|
"Error": "Error",
|
|
275
286
|
"internal server error": "Oupsie, lo siento... Algo salió mal en el servidor.",
|
package/src/locales/fr.json
CHANGED
|
@@ -279,6 +279,17 @@
|
|
|
279
279
|
"editAccount": "Modifier le compte",
|
|
280
280
|
"save": "Sauvegarder"
|
|
281
281
|
},
|
|
282
|
+
"chatLogs": {
|
|
283
|
+
"anyMessage": "Tout message",
|
|
284
|
+
"atLeast": "Au moins {{count}} messages",
|
|
285
|
+
"atLeast2": "Au moins 2 messages",
|
|
286
|
+
"atLeast3": "Au moins 3 messages",
|
|
287
|
+
"atLeast5": "Au moins 5 messages",
|
|
288
|
+
"atLeast10": "Au moins 10 messages",
|
|
289
|
+
"atLeast15": "Au moins 15 messages",
|
|
290
|
+
"atLeast20": "Au moins 20 messages",
|
|
291
|
+
"customMinimumMessages": "Personnaliser le nombre de messages"
|
|
292
|
+
},
|
|
282
293
|
"success": "Succès",
|
|
283
294
|
"Error": "Erreur",
|
|
284
295
|
"internal server error": "Oupsie, désolé... Quelque chose s'est mal passé sur le serveur",
|
package/src/locales/it.json
CHANGED
|
@@ -293,6 +293,17 @@
|
|
|
293
293
|
"editAccount": "Modifica account",
|
|
294
294
|
"save": "Salva"
|
|
295
295
|
},
|
|
296
|
+
"chatLogs": {
|
|
297
|
+
"atLeast": "Almeno {{count}} messaggi",
|
|
298
|
+
"anyMessage": "Qualunque messaggio",
|
|
299
|
+
"atLeast2": "Almeno 2 messaggi",
|
|
300
|
+
"atLeast3": "Almeno 3 messaggi",
|
|
301
|
+
"atLeast5": "Almeno 5 messaggi",
|
|
302
|
+
"atLeast10": "Almeno 10 messaggi",
|
|
303
|
+
"atLeast15": "Almeno 15 messaggi",
|
|
304
|
+
"atLeast20": "Almeno 20 messaggi",
|
|
305
|
+
"customMinimumMessages": "Personalizza il numero di messaggi"
|
|
306
|
+
},
|
|
296
307
|
"success": "Operazione andata a buon fine",
|
|
297
308
|
"Error": "Errore",
|
|
298
309
|
"internal server error": "Ops, scusa... qualcosa è andato storto nel server",
|