react-native-srschat 0.1.60 → 0.1.62

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.
@@ -49,40 +49,38 @@ export const VoiceButton = ({ setInput }) => {
49
49
 
50
50
  if (permissionResult) {
51
51
  const initialized = await initVoice(
52
- // Final result callback - just update input, don't send
52
+ // Final result callback
53
53
  (result, error) => {
54
54
  console.log('Voice final result:', result, 'Error:', error);
55
+
56
+ // Always reset states when the recognition ends
57
+ setIsListening(false);
58
+ setLoading(false);
59
+
55
60
  if (error) {
56
61
  // Don't show alert for permission errors since we handle that elsewhere
57
62
  if (!error.includes('permission')) {
58
63
  Alert.alert('Error', error);
59
64
  }
60
- setIsListening(false);
61
- setLoading(false);
62
65
  return;
63
66
  }
67
+
64
68
  if (result) {
65
69
  if (setInput) {
66
- // Just update the input field, don't send
70
+ // For live transcription mode: just update input, don't auto-send
67
71
  setInput(result);
68
72
  } else {
69
- console.warn('VoiceButton: setInput prop is not provided');
73
+ // For original mode: send the message automatically
74
+ handleVoiceSend(null, result);
70
75
  }
71
76
  }
72
- // Always reset states when the recognition ends
73
- setIsListening(false);
74
- setLoading(false);
75
77
  },
76
78
  // Partial result callback for live transcription
77
- (partialResult) => {
79
+ setInput ? (partialResult) => {
78
80
  if (partialResult) {
79
- if (setInput) {
80
- setInput(partialResult);
81
- } else {
82
- console.warn('VoiceButton: setInput prop is not provided for partial results');
83
- }
81
+ setInput(partialResult);
84
82
  }
85
- }
83
+ } : null
86
84
  );
87
85
 
88
86
  if (!initialized) {
@@ -107,7 +105,8 @@ export const VoiceButton = ({ setInput }) => {
107
105
  }
108
106
 
109
107
  return () => {
110
- cleanup();
108
+ // Optional: only if the entire feature is being removed from the app
109
+ // cleanup();
111
110
  };
112
111
  }, [permissionStatus, permissionChecked]);
113
112
 
@@ -164,7 +163,7 @@ export const VoiceButton = ({ setInput }) => {
164
163
  console.error('Error in toggleRecording:', error);
165
164
  Alert.alert('Error', 'An error occurred while managing voice recognition');
166
165
  setIsListening(false);
167
- await cleanup();
166
+ // Don't call cleanup here - let the natural session cleanup handle it
168
167
  } finally {
169
168
  setLoading(false);
170
169
  }
@@ -3,16 +3,17 @@
3
3
  import { Platform } from 'react-native';
4
4
  import Voice from '@react-native-voice/voice';
5
5
  import { check, PERMISSIONS, request, RESULTS } from 'react-native-permissions';
6
- import useAsyncStorage from '../hooks/useAsyncStorage';
7
6
 
8
7
  let resultCallback = null;
9
8
  let partialResultCallback = null;
10
9
  let silenceTimer = null;
11
- let isCurrentlyRecording = false;
12
10
  let finalResult = '';
13
- let lastPartialResultTime = 0;
14
- const SILENCE_DURATION = 1500; // 1.5 seconds of silence before stopping
15
- const PARTIAL_RESULT_THROTTLE = 100; // Throttle partial results to every 100ms
11
+ const SILENCE_DURATION = 1500;
12
+
13
+ const State = { IDLE: 'IDLE', LISTENING: 'LISTENING', FINALIZING: 'FINALIZING' };
14
+ let state = State.IDLE;
15
+
16
+ let listenersBound = false;
16
17
 
17
18
  // Add this constant for AsyncStorage key
18
19
  const PERMISSION_STORAGE_KEY = '@voice_permission_status';
@@ -27,13 +28,15 @@ export function setPermissionStatusHandlers(getter, setter) {
27
28
  permissionStatusSetter = setter;
28
29
  }
29
30
 
30
- // Initialize Voice handlers
31
+ // Initialize Voice handlers - modified to support live transcription
31
32
  export async function initVoice(onResult, onPartialResult = null) {
32
33
  try {
33
34
  resultCallback = onResult;
34
- partialResultCallback = onPartialResult;
35
+ partialResultCallback = onPartialResult; // Store partial callback
35
36
  finalResult = '';
36
-
37
+
38
+ if (listenersBound) return true;
39
+
37
40
  // Check if Voice module is available
38
41
  if (!Voice) {
39
42
  console.error('Voice module is not available');
@@ -46,129 +49,110 @@ export async function initVoice(onResult, onPartialResult = null) {
46
49
  console.error('Speech recognition is not available on this device');
47
50
  return false;
48
51
  }
49
-
50
- // Check if another recognition session is active
51
- try {
52
- const isRecognizing = await Voice.isRecognizing();
53
- if (isRecognizing) {
54
- console.log('Another recognition session is active, cleaning up...');
55
- await Voice.destroy();
56
- await new Promise(resolve => setTimeout(resolve, 300));
57
- }
58
- } catch (e) {
59
- // Ignore errors checking recognition state
60
- }
52
+
53
+ Voice.removeAllListeners();
61
54
 
62
55
  // Set up all event listeners
63
- Voice.onSpeechStart = (e) => {
64
- console.log('onSpeechStart: ', e);
65
- isCurrentlyRecording = true;
56
+ Voice.onSpeechStart = () => {
57
+ console.log('[onSpeechStart] Setting state to LISTENING');
58
+ state = State.LISTENING;
66
59
  finalResult = '';
67
-
68
- if (silenceTimer) {
69
- clearTimeout(silenceTimer);
70
- silenceTimer = null;
71
- }
72
-
73
- // Start initial silence detection
74
- handleSilenceDetection();
60
+ clearSilenceTimer();
75
61
  };
76
62
 
77
- Voice.onSpeechRecognized = (e) => {
78
- console.log('onSpeechRecognized: ', e);
79
- if (e.isFinal) {
80
- console.log('Speech recognition final');
81
- handleFinalResult();
63
+ // Optional: ignore onSpeechRecognized or just log
64
+ Voice.onSpeechRecognized = () => {};
65
+
66
+ Voice.onSpeechEnd = () => {
67
+ console.log('[onSpeechEnd] Speech ended, current state:', state);
68
+ clearSilenceTimer();
69
+ // Always reset to IDLE when speech ends (sessions should be considered over)
70
+ console.log('[onSpeechEnd] Scheduling IDLE reset');
71
+ if (Platform.OS === 'android') {
72
+ setTimeout(() => {
73
+ console.log('[onSpeechEnd] Android timeout - setting state to IDLE');
74
+ state = State.IDLE;
75
+ }, 800); // Increased delay
76
+ } else {
77
+ console.log('[onSpeechEnd] iOS - setting state to IDLE immediately');
78
+ state = State.IDLE;
82
79
  }
83
80
  };
84
81
 
85
- Voice.onSpeechEnd = async (e) => {
86
- console.log('onSpeechEnd: ', e);
82
+ Voice.onSpeechError = (e) => {
83
+ console.log('[onSpeechError] Error occurred, current state:', state, 'error:', e);
84
+ clearSilenceTimer();
85
+ const code = e.error?.code?.toString();
86
+ const msg = e.error?.message || '';
87
87
 
88
- if (silenceTimer) {
89
- clearTimeout(silenceTimer);
90
- silenceTimer = null;
88
+ // Handle callback first
89
+ if (Platform.OS === 'android' && (code === '7' || code === '5')) {
90
+ if (finalResult && resultCallback) resultCallback(finalResult, null);
91
+ else if (resultCallback) resultCallback(null, null);
92
+ } else if (!msg.includes('No speech detected') && resultCallback) {
93
+ resultCallback(null, msg);
94
+ } else if (resultCallback) {
95
+ resultCallback(null, null);
91
96
  }
92
-
93
- // On iOS, onSpeechEnd can fire multiple times, so check if we're still recording
94
- if (isCurrentlyRecording && finalResult) {
95
- // Don't call handleFinalResult here on iOS as it causes the error
96
- // Instead, just mark that speech ended and let silence detection handle it
97
- if (Platform.OS === 'ios') {
98
- console.log('iOS: Speech ended, waiting for silence detection to handle cleanup');
99
- } else {
100
- await handleFinalResult();
101
- }
97
+
98
+ // Errors end the session immediately, reset to IDLE with delay
99
+ console.log('[onSpeechError] Scheduling IDLE reset');
100
+ if (Platform.OS === 'android') {
101
+ setTimeout(() => {
102
+ console.log('[onSpeechError] Android timeout - setting state to IDLE');
103
+ state = State.IDLE;
104
+ }, 800); // Increased delay to match onSpeechEnd
105
+ } else {
106
+ console.log('[onSpeechError] iOS - setting state to IDLE immediately');
107
+ state = State.IDLE;
102
108
  }
103
109
  };
104
110
 
105
- Voice.onSpeechError = async (e) => {
106
- console.error('onSpeechError: ', e);
107
-
108
- if (silenceTimer) {
109
- clearTimeout(silenceTimer);
110
- silenceTimer = null;
111
+ Voice.onSpeechResults = (e) => {
112
+ console.log('[onSpeechResults] Results received, current state:', state, 'results:', e);
113
+ clearSilenceTimer();
114
+ if (e.value && e.value.length > 0) {
115
+ finalResult = e.value[0];
111
116
  }
112
-
113
- // Check for specific error types
114
- const isNoSpeechError = e.error?.code === "recognition_fail" &&
115
- e.error?.message?.includes("No speech detected");
116
117
 
117
- // iOS error 1101 is often transient and related to service availability
118
- const isTransientIOSError = Platform.OS === 'ios' &&
119
- (e.error?.code === "1101" ||
120
- e.error?.message?.includes("kAFAssistantErrorDomain"));
121
-
122
- await cleanupVoiceSession();
118
+ // Only call callback if we haven't already (avoid double-calling)
119
+ if (state === State.LISTENING && resultCallback) {
120
+ console.log('[onSpeechResults] Calling callback with results');
121
+ resultCallback(finalResult, null);
122
+ } else {
123
+ console.log('[onSpeechResults] Not calling callback - state:', state);
124
+ }
123
125
 
124
- // Only send error to callback for non-transient errors
125
- if (!isNoSpeechError && !isTransientIOSError && resultCallback) {
126
- resultCallback(null, e.error?.message || 'Speech recognition error');
127
- } else if (isTransientIOSError) {
128
- console.log('Transient iOS speech recognition error, attempting to recover');
129
- // Send the final result if we have one
130
- if (finalResult && resultCallback) {
131
- resultCallback(finalResult);
132
- }
126
+ // On Android, we must explicitly stop to avoid session corruption
127
+ if (Platform.OS === 'android') {
128
+ console.log('[onSpeechResults] Android: Explicitly calling stopRecording()');
129
+ stopRecording();
133
130
  }
134
- };
135
131
 
136
- Voice.onSpeechResults = (e) => {
137
- console.log('onSpeechResults: ', e);
138
- if (e.value && e.value.length > 0) {
139
- finalResult = e.value[0];
140
- // Also trigger silence detection for final results
141
- handleSilenceDetection();
132
+ // Results end the session, reset to IDLE with delay
133
+ console.log('[onSpeechResults] Scheduling IDLE reset');
134
+ if (Platform.OS === 'android') {
135
+ setTimeout(() => {
136
+ console.log('[onSpeechResults] Android timeout - setting state to IDLE');
137
+ state = State.IDLE;
138
+ }, 800); // Increased delay
139
+ } else {
140
+ console.log('[onSpeechResults] iOS - setting state to IDLE immediately');
141
+ state = State.IDLE;
142
142
  }
143
143
  };
144
144
 
145
145
  Voice.onSpeechPartialResults = (e) => {
146
- console.log('onSpeechPartialResults: ', e);
147
-
148
- if (silenceTimer) {
149
- clearTimeout(silenceTimer);
150
- }
151
-
152
146
  if (e.value && e.value.length > 0) {
153
147
  finalResult = e.value[0];
154
-
155
- // Throttle partial results to prevent overwhelming the UI and speech service
156
- const now = Date.now();
157
- if (partialResultCallback && (now - lastPartialResultTime) > PARTIAL_RESULT_THROTTLE) {
158
- partialResultCallback(e.value[0]);
159
- lastPartialResultTime = now;
160
- }
161
-
148
+ if (partialResultCallback) partialResultCallback(finalResult);
162
149
  handleSilenceDetection();
163
150
  }
164
151
  };
165
152
 
166
- if (Platform.OS === 'android') {
167
- Voice.onSpeechVolumeChanged = (e) => {
168
- console.log('onSpeechVolumeChanged: ', e);
169
- };
170
- }
153
+ if (Platform.OS === 'android') Voice.onSpeechVolumeChanged = () => {};
171
154
 
155
+ listenersBound = true;
172
156
  return true;
173
157
  } catch (error) {
174
158
  console.error('Error initializing Voice:', error);
@@ -182,194 +166,144 @@ const handleSilenceDetection = () => {
182
166
  }
183
167
 
184
168
  silenceTimer = setTimeout(async () => {
185
- console.log('Silence detected, stopping recording...');
186
- if (isCurrentlyRecording && finalResult) {
169
+ if (state === State.LISTENING) {
187
170
  await handleFinalResult();
188
171
  }
189
172
  }, SILENCE_DURATION);
190
173
  };
191
174
 
192
175
  const handleFinalResult = async () => {
193
- if (!isCurrentlyRecording) return;
194
-
195
- // Set flag immediately to prevent race conditions
196
- isCurrentlyRecording = false;
197
-
198
- // Clear silence timer first
199
- if (silenceTimer) {
200
- clearTimeout(silenceTimer);
201
- silenceTimer = null;
176
+ console.log('[handleFinalResult] Called, current state:', state);
177
+ if (state !== State.LISTENING) {
178
+ console.log('[handleFinalResult] State not LISTENING, returning');
179
+ return;
202
180
  }
203
181
 
204
- // Send result if we have one
205
- if (finalResult && resultCallback) {
206
- resultCallback(finalResult);
207
- }
182
+ // Set to FINALIZING first to prevent double callbacks
183
+ console.log('[handleFinalResult] Setting state to FINALIZING');
184
+ state = State.FINALIZING;
208
185
 
209
- // Give iOS time to finish processing before cleanup
210
- if (Platform.OS === 'ios') {
211
- await new Promise(resolve => setTimeout(resolve, 100));
186
+ // Call the callback with results
187
+ if (finalResult && resultCallback) {
188
+ console.log('[handleFinalResult] Calling callback with result:', finalResult);
189
+ resultCallback(finalResult, null);
212
190
  }
213
191
 
214
- // Stop recording with proper sequence
192
+ // Now stop recording (this will call Voice.stop())
193
+ console.log('[handleFinalResult] Calling stopRecording');
215
194
  await stopRecording();
216
195
  };
217
196
 
218
- const cleanupVoiceSession = async () => {
219
- isCurrentlyRecording = false;
197
+ const cleanupVoiceSession = () => {
198
+ console.log('[cleanupVoiceSession] Called, current state:', state);
199
+ finalResult = '';
200
+ clearSilenceTimer();
220
201
 
202
+ // Add delay before allowing next session on Android
203
+ if (Platform.OS === 'android') {
204
+ setTimeout(() => {
205
+ console.log('[cleanupVoiceSession] Android timeout - setting state to IDLE');
206
+ state = State.IDLE;
207
+ }, 800);
208
+ } else {
209
+ console.log('[cleanupVoiceSession] iOS - setting state to IDLE immediately');
210
+ state = State.IDLE;
211
+ }
212
+ };
213
+
214
+ const clearSilenceTimer = () => {
221
215
  if (silenceTimer) {
222
216
  clearTimeout(silenceTimer);
223
217
  silenceTimer = null;
224
218
  }
219
+ };
225
220
 
221
+ export async function startRecording() {
226
222
  try {
227
- // Check if Voice module is available
228
- if (!Voice) {
229
- console.log('Voice module not available during cleanup');
230
- return;
231
- }
223
+ console.log('[startRecording] Called, current state:', state);
232
224
 
233
- // First try to stop if still recognizing
234
- const isRecognizing = await Voice.isRecognizing();
235
- if (isRecognizing) {
225
+ // On Android, destroy any lingering instance before starting
226
+ if (Platform.OS === 'android') {
236
227
  try {
237
- await Voice.stop();
238
- await new Promise(resolve => setTimeout(resolve, 100));
239
- } catch (e) {
240
- console.error('Error stopping in cleanup:', e);
241
- }
242
- }
243
-
244
- // Then force destroy
245
- await Voice.destroy();
246
- await new Promise(resolve => setTimeout(resolve, 300));
247
-
248
- // Double check and force destroy again if needed
249
- const stillRecognizing = await Voice.isRecognizing();
250
- if (stillRecognizing) {
251
- await Voice.destroy();
252
- await new Promise(resolve => setTimeout(resolve, 300));
253
- }
254
- } catch (error) {
255
- console.error('Error in cleanupVoiceSession:', error);
256
- // Final attempt to destroy on error
257
- try {
258
- if (Voice) {
228
+ console.log('[startRecording] Android: Proactively destroying Voice instance');
259
229
  await Voice.destroy();
230
+ await new Promise(r => setTimeout(r, 100)); // Short delay for destroy to complete
231
+ } catch (e) {
232
+ console.log('[startRecording] Proactive destroy failed, may be okay:', e);
260
233
  }
261
- } catch (e) {
262
- console.error('Final destroy attempt failed:', e);
263
234
  }
264
- }
265
-
266
- finalResult = '';
267
- };
268
235
 
269
- export async function startRecording() {
270
- try {
271
- // Check if Voice module is available
272
236
  if (!Voice) {
273
- console.error('Voice module is not available');
237
+ console.log('[startRecording] Voice not available');
238
+ return false;
239
+ }
240
+ if (state !== State.IDLE) {
241
+ console.log('[startRecording] State not IDLE, returning false');
274
242
  return false;
275
243
  }
276
-
277
- // Ensure cleanup of any existing session
278
- await cleanupVoiceSession();
279
244
 
280
245
  const hasPermission = await requestAudioPermission();
281
246
  if (!hasPermission) {
282
247
  console.error('No permission to record audio');
283
248
  return false;
284
249
  }
285
-
286
- // Reset throttle timer
287
- lastPartialResultTime = 0;
288
250
 
251
+ const recognizing = await Voice.isRecognizing();
252
+ console.log('[startRecording] Voice.isRecognizing():', recognizing);
253
+ if (recognizing) {
254
+ console.log('[startRecording] Already recognizing, canceling first');
255
+ await Voice.cancel();
256
+ // Wait longer for cancel to take effect
257
+ await new Promise(r => setTimeout(r, 500));
258
+
259
+ // Double-check if still recognizing after cancel
260
+ const stillRecognizing = await Voice.isRecognizing();
261
+ console.log('[startRecording] After cancel, still recognizing:', stillRecognizing);
262
+ if (stillRecognizing) {
263
+ console.log('[startRecording] Still recognizing after cancel, stopping');
264
+ try {
265
+ await Voice.stop();
266
+ await new Promise(r => setTimeout(r, 300));
267
+ } catch (e) {
268
+ console.log('[startRecording] Error stopping:', e);
269
+ }
270
+ }
271
+ }
272
+
273
+ console.log('[startRecording] Calling Voice.start()');
289
274
  await Voice.start('en-US');
290
- isCurrentlyRecording = true;
275
+ console.log('[startRecording] Voice.start() completed, setting state to LISTENING');
276
+ state = State.LISTENING;
291
277
  return true;
292
278
  } catch (error) {
293
279
  console.error('Error starting voice recognition:', error);
294
- await cleanupVoiceSession();
280
+ cleanupVoiceSession();
295
281
  return false;
296
282
  }
297
283
  }
298
284
 
299
285
  export async function stopRecording() {
300
286
  try {
301
- if (!Voice) return;
302
-
303
- // Already stopped
304
- if (!isCurrentlyRecording) {
305
- // Still try to clean up any lingering session
306
- try {
307
- const isRecognizing = await Voice.isRecognizing();
308
- if (isRecognizing) {
309
- await Voice.stop();
310
- }
311
- } catch (e) {
312
- // Ignore errors checking recognition state
313
- }
287
+ console.log('[stopRecording] Called, current state:', state);
288
+ // Can be called from LISTENING or FINALIZING state
289
+ if ((state !== State.LISTENING && state !== State.FINALIZING) || !Voice) {
290
+ console.log('[stopRecording] Invalid state or no Voice, returning');
314
291
  return;
315
292
  }
316
-
317
- // Set this first to prevent race conditions
318
- isCurrentlyRecording = false;
319
-
320
- if (silenceTimer) {
321
- clearTimeout(silenceTimer);
322
- silenceTimer = null;
323
- }
324
-
325
- // For iOS, use a more careful approach
326
- if (Platform.OS === 'ios') {
327
- try {
328
- // First cancel any ongoing recognition
329
- await Voice.cancel();
330
- await new Promise(resolve => setTimeout(resolve, 50));
331
- } catch (error) {
332
- // Ignore cancel errors
333
- }
334
-
335
- try {
336
- // Then stop
337
- await Voice.stop();
338
- await new Promise(resolve => setTimeout(resolve, 100));
339
- } catch (error) {
340
- console.log('Error stopping Voice (expected on iOS):', error);
341
- }
342
-
343
- try {
344
- // Finally destroy
345
- await Voice.destroy();
346
- await new Promise(resolve => setTimeout(resolve, 200));
347
- } catch (error) {
348
- console.log('Error destroying Voice (expected on iOS):', error);
349
- }
350
- } else {
351
- // Android can handle the normal sequence
352
- try {
353
- await Voice.stop();
354
- await new Promise(resolve => setTimeout(resolve, 100));
355
- } catch (error) {
356
- console.error('Error stopping Voice:', error);
357
- }
358
-
359
- try {
360
- await Voice.destroy();
361
- await new Promise(resolve => setTimeout(resolve, 300));
362
- } catch (error) {
363
- console.error('Error destroying Voice:', error);
364
- }
293
+
294
+ // Only set to FINALIZING if not already there
295
+ if (state === State.LISTENING) {
296
+ console.log('[stopRecording] Setting state to FINALIZING');
297
+ state = State.FINALIZING;
365
298
  }
366
-
367
- // Final cleanup
368
- await cleanupVoiceSession();
299
+
300
+ clearSilenceTimer();
301
+ console.log('[stopRecording] Calling Voice.stop()');
302
+ await Voice.stop();
303
+ console.log('[stopRecording] Voice.stop() completed');
369
304
  } catch (error) {
370
305
  console.error('Error in stopRecording:', error);
371
- // Force cleanup on error
372
- await cleanupVoiceSession();
306
+ cleanupVoiceSession();
373
307
  }
374
308
  }
375
309
 
@@ -377,10 +311,10 @@ export async function cancelRecording() {
377
311
  try {
378
312
  if (!Voice) return;
379
313
  await Voice.cancel();
380
- await cleanupVoiceSession();
314
+ cleanupVoiceSession();
381
315
  } catch (error) {
382
316
  console.error('Error canceling voice recognition:', error);
383
- await cleanupVoiceSession();
317
+ cleanupVoiceSession();
384
318
  }
385
319
  }
386
320
 
@@ -415,8 +349,6 @@ export async function requestAudioPermission() {
415
349
 
416
350
  async function requestAndroidPermission() {
417
351
  try {
418
-
419
-
420
352
  // Request microphone permission
421
353
  const micPermission = await request(PERMISSIONS.ANDROID.RECORD_AUDIO);
422
354
  if (micPermission !== RESULTS.GRANTED) {
@@ -424,12 +356,18 @@ async function requestAndroidPermission() {
424
356
  return false;
425
357
  }
426
358
 
427
- // Skip checking speech recognition services which is causing errors
428
- const services = await Voice.getSpeechRecognitionServices();
429
- if (!services || services.length === 0) {
430
- console.error('No speech recognition services available');
431
- return false;
359
+ // Check speech recognition services
360
+ try {
361
+ const services = await Voice.getSpeechRecognitionServices();
362
+ if (!services || services.length === 0) {
363
+ console.error('No speech recognition services available');
364
+ return false;
365
+ }
366
+ } catch (e) {
367
+ console.log('Error checking speech services:', e);
368
+ // Continue anyway - some devices report error but work fine
432
369
  }
370
+
433
371
  return true;
434
372
  } catch (error) {
435
373
  console.error('Error requesting Android permission:', error);
@@ -469,20 +407,5 @@ export function resetStoredPermission() {
469
407
  }
470
408
 
471
409
  export function cleanup() {
472
- if (!Voice) {
473
- console.log('Voice module not available during cleanup');
474
- return;
475
- }
476
-
477
- Voice.destroy().then(() => {
478
- Voice.removeAllListeners();
479
- cleanupVoiceSession();
480
- }).catch(error => {
481
- console.error('Error in cleanup:', error);
482
- // Try one more time
483
- if (Voice) {
484
- Voice.destroy().catch(e => console.error('Final cleanup attempt failed:', e));
485
- }
486
- });
487
- }
488
-
410
+ cleanupVoiceSession();
411
+ }