react-native-srschat 0.1.60 → 0.1.61
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/lib/commonjs/components/voice.js +14 -17
- package/lib/commonjs/components/voice.js.map +1 -1
- package/lib/commonjs/utils/audioRecorder.js +182 -244
- package/lib/commonjs/utils/audioRecorder.js.map +1 -1
- package/lib/module/components/voice.js +14 -17
- package/lib/module/components/voice.js.map +1 -1
- package/lib/module/utils/audioRecorder.js +181 -243
- package/lib/module/utils/audioRecorder.js.map +1 -1
- package/lib/typescript/components/voice.d.ts.map +1 -1
- package/lib/typescript/utils/audioRecorder.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/voice.js +16 -17
- package/src/utils/audioRecorder.js +187 -264
package/src/components/voice.js
CHANGED
|
@@ -49,40 +49,38 @@ export const VoiceButton = ({ setInput }) => {
|
|
|
49
49
|
|
|
50
50
|
if (permissionResult) {
|
|
51
51
|
const initialized = await initVoice(
|
|
52
|
-
// Final result callback
|
|
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
|
-
//
|
|
70
|
+
// For live transcription mode: just update input, don't auto-send
|
|
67
71
|
setInput(result);
|
|
68
72
|
} else {
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
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
|
-
|
|
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 = (
|
|
64
|
-
console.log('onSpeechStart
|
|
65
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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.
|
|
86
|
-
console.log('
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
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.
|
|
106
|
-
console.
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
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
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
//
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
137
|
-
console.log('onSpeechResults
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
//
|
|
210
|
-
if (
|
|
211
|
-
|
|
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
|
-
//
|
|
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 =
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
234
|
-
|
|
235
|
-
if (isRecognizing) {
|
|
225
|
+
// On Android, destroy any lingering instance before starting
|
|
226
|
+
if (Platform.OS === 'android') {
|
|
236
227
|
try {
|
|
237
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
280
|
+
cleanupVoiceSession();
|
|
295
281
|
return false;
|
|
296
282
|
}
|
|
297
283
|
}
|
|
298
284
|
|
|
299
285
|
export async function stopRecording() {
|
|
300
286
|
try {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
368
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
+
cleanupVoiceSession();
|
|
381
315
|
} catch (error) {
|
|
382
316
|
console.error('Error canceling voice recognition:', error);
|
|
383
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
473
|
-
|
|
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
|
+
}
|