react-native-voice-ts 1.0.1 → 1.0.2
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/dist/components/VoiceMicrophone.d.ts +12 -0
- package/dist/components/VoiceMicrophone.d.ts.map +1 -1
- package/dist/components/VoiceMicrophone.js +97 -9
- package/dist/components/VoiceMicrophone.js.map +1 -1
- package/dist/hooks/useVoiceRecognition.d.ts +12 -0
- package/dist/hooks/useVoiceRecognition.d.ts.map +1 -1
- package/dist/hooks/useVoiceRecognition.js +109 -11
- package/dist/hooks/useVoiceRecognition.js.map +1 -1
- package/package.json +2 -2
- package/src/components/VoiceMicrophone.tsx +115 -8
- package/src/hooks/useVoiceRecognition.ts +126 -10
|
@@ -35,6 +35,18 @@ export interface VoiceMicrophoneProps {
|
|
|
35
35
|
* @default true
|
|
36
36
|
*/
|
|
37
37
|
enablePartialResults?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Whether to continue listening after getting results (continuous mode)
|
|
40
|
+
* When enabled, the microphone will automatically restart after getting results
|
|
41
|
+
* @default false
|
|
42
|
+
*/
|
|
43
|
+
continuous?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Maximum silence duration in milliseconds before stopping (continuous mode)
|
|
46
|
+
* Only applies when continuous mode is enabled
|
|
47
|
+
* @default 5000 (5 seconds)
|
|
48
|
+
*/
|
|
49
|
+
maxSilenceDuration?: number;
|
|
38
50
|
/**
|
|
39
51
|
* Custom render function for the component
|
|
40
52
|
* Receives isRecording state and control functions
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VoiceMicrophone.d.ts","sourceRoot":"","sources":["../../src/components/VoiceMicrophone.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2C,MAAM,OAAO,CAAC;AAIhE,MAAM,WAAW,oBAAoB;IACnC;;OAEG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAExC;;OAEG;IACH,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAEzC;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAErB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAElC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAE/B;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE;QACjB,WAAW,EAAE,OAAO,CAAC;QACrB,cAAc,EAAE,MAAM,CAAC;QACvB,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1B,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;KACtB,KAAK,KAAK,CAAC,SAAS,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,QAAA,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,
|
|
1
|
+
{"version":3,"file":"VoiceMicrophone.d.ts","sourceRoot":"","sources":["../../src/components/VoiceMicrophone.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2C,MAAM,OAAO,CAAC;AAIhE,MAAM,WAAW,oBAAoB;IACnC;;OAEG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAExC;;OAEG;IACH,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAEzC;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAErB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAElC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAE/B;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE;QACjB,WAAW,EAAE,OAAO,CAAC;QACrB,cAAc,EAAE,MAAM,CAAC;QACvB,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1B,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;KACtB,KAAK,KAAK,CAAC,SAAS,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,QAAA,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA8OnD,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
|
@@ -27,33 +27,77 @@ import Voice from '../index';
|
|
|
27
27
|
* </VoiceMicrophone>
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
|
-
const VoiceMicrophone = ({ onSpeechResult, onPartialResult, onStart, onStop, onError, locale = 'en-US', autoStart = false, enablePartialResults = true, children, }) => {
|
|
30
|
+
const VoiceMicrophone = ({ onSpeechResult, onPartialResult, onStart, onStop, onError, locale = 'en-US', autoStart = false, enablePartialResults = true, continuous = false, maxSilenceDuration = 5000, children, }) => {
|
|
31
31
|
const [isRecording, setIsRecording] = useState(false);
|
|
32
32
|
const [recognizedText, setRecognizedText] = useState('');
|
|
33
33
|
const [partialText, setPartialText] = useState('');
|
|
34
34
|
const [error, setError] = useState(null);
|
|
35
|
+
const [shouldContinue, setShouldContinue] = useState(false);
|
|
36
|
+
const silenceTimerRef = React.useRef(null);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
// Clear any existing timers on cleanup
|
|
39
|
+
return () => {
|
|
40
|
+
if (silenceTimerRef.current) {
|
|
41
|
+
clearTimeout(silenceTimerRef.current);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}, []);
|
|
35
45
|
useEffect(() => {
|
|
36
46
|
// Set up event listeners
|
|
37
47
|
Voice.onSpeechStart = () => {
|
|
38
48
|
setIsRecording(true);
|
|
39
49
|
setError(null);
|
|
50
|
+
if (silenceTimerRef.current) {
|
|
51
|
+
clearTimeout(silenceTimerRef.current);
|
|
52
|
+
}
|
|
40
53
|
onStart?.();
|
|
41
54
|
};
|
|
42
|
-
Voice.onSpeechEnd = () => {
|
|
55
|
+
Voice.onSpeechEnd = async () => {
|
|
43
56
|
setIsRecording(false);
|
|
44
|
-
|
|
57
|
+
// In continuous mode, restart listening after results
|
|
58
|
+
if (continuous && shouldContinue) {
|
|
59
|
+
// Small delay before restarting
|
|
60
|
+
setTimeout(async () => {
|
|
61
|
+
if (shouldContinue) {
|
|
62
|
+
try {
|
|
63
|
+
await Voice.start(locale, {
|
|
64
|
+
EXTRA_PARTIAL_RESULTS: enablePartialResults,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
console.error('Failed to restart voice recognition:', err);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}, 100);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
onStop?.();
|
|
75
|
+
}
|
|
45
76
|
};
|
|
46
77
|
Voice.onSpeechError = (e) => {
|
|
47
78
|
const errorMessage = e.error?.message || 'Unknown error';
|
|
48
79
|
setError(errorMessage);
|
|
49
80
|
setIsRecording(false);
|
|
81
|
+
setShouldContinue(false);
|
|
82
|
+
if (silenceTimerRef.current) {
|
|
83
|
+
clearTimeout(silenceTimerRef.current);
|
|
84
|
+
}
|
|
50
85
|
onError?.(errorMessage);
|
|
51
86
|
};
|
|
52
87
|
Voice.onSpeechResults = (e) => {
|
|
53
88
|
if (e.value && e.value.length > 0) {
|
|
54
89
|
const text = e.value[0];
|
|
55
|
-
|
|
56
|
-
|
|
90
|
+
// In continuous mode, append new text to existing
|
|
91
|
+
if (continuous && recognizedText) {
|
|
92
|
+
const updatedText = recognizedText + ' ' + text;
|
|
93
|
+
setRecognizedText(updatedText);
|
|
94
|
+
onSpeechResult?.(updatedText);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
setRecognizedText(text);
|
|
98
|
+
onSpeechResult?.(text);
|
|
99
|
+
}
|
|
100
|
+
setPartialText('');
|
|
57
101
|
}
|
|
58
102
|
};
|
|
59
103
|
if (enablePartialResults) {
|
|
@@ -62,6 +106,15 @@ const VoiceMicrophone = ({ onSpeechResult, onPartialResult, onStart, onStop, onE
|
|
|
62
106
|
const text = e.value[0];
|
|
63
107
|
setPartialText(text);
|
|
64
108
|
onPartialResult?.(text);
|
|
109
|
+
// Reset silence timer on partial results (user is speaking)
|
|
110
|
+
if (continuous && silenceTimerRef.current) {
|
|
111
|
+
clearTimeout(silenceTimerRef.current);
|
|
112
|
+
silenceTimerRef.current = setTimeout(() => {
|
|
113
|
+
if (shouldContinue) {
|
|
114
|
+
stop();
|
|
115
|
+
}
|
|
116
|
+
}, maxSilenceDuration);
|
|
117
|
+
}
|
|
65
118
|
}
|
|
66
119
|
};
|
|
67
120
|
}
|
|
@@ -76,6 +129,11 @@ const VoiceMicrophone = ({ onSpeechResult, onPartialResult, onStart, onStop, onE
|
|
|
76
129
|
onStop,
|
|
77
130
|
onError,
|
|
78
131
|
enablePartialResults,
|
|
132
|
+
continuous,
|
|
133
|
+
shouldContinue,
|
|
134
|
+
recognizedText,
|
|
135
|
+
locale,
|
|
136
|
+
maxSilenceDuration,
|
|
79
137
|
]);
|
|
80
138
|
// Auto-start if enabled
|
|
81
139
|
useEffect(() => {
|
|
@@ -87,8 +145,11 @@ const VoiceMicrophone = ({ onSpeechResult, onPartialResult, onStart, onStop, onE
|
|
|
87
145
|
const start = useCallback(async () => {
|
|
88
146
|
try {
|
|
89
147
|
setError(null);
|
|
90
|
-
|
|
91
|
-
|
|
148
|
+
if (!continuous) {
|
|
149
|
+
setRecognizedText('');
|
|
150
|
+
setPartialText('');
|
|
151
|
+
}
|
|
152
|
+
setShouldContinue(true);
|
|
92
153
|
// Check permission (Android only)
|
|
93
154
|
const hasPermission = await Voice.checkMicrophonePermission();
|
|
94
155
|
if (!hasPermission) {
|
|
@@ -101,25 +162,52 @@ const VoiceMicrophone = ({ onSpeechResult, onPartialResult, onStart, onStop, onE
|
|
|
101
162
|
await Voice.start(locale, {
|
|
102
163
|
EXTRA_PARTIAL_RESULTS: enablePartialResults,
|
|
103
164
|
});
|
|
165
|
+
// Start silence timer if in continuous mode
|
|
166
|
+
if (continuous) {
|
|
167
|
+
silenceTimerRef.current = setTimeout(() => {
|
|
168
|
+
if (shouldContinue) {
|
|
169
|
+
stop();
|
|
170
|
+
}
|
|
171
|
+
}, maxSilenceDuration);
|
|
172
|
+
}
|
|
104
173
|
}
|
|
105
174
|
catch (e) {
|
|
106
175
|
const errorMessage = e instanceof Error ? e.message : 'Failed to start recording';
|
|
107
176
|
setError(errorMessage);
|
|
177
|
+
setShouldContinue(false);
|
|
108
178
|
onError?.(errorMessage);
|
|
109
179
|
}
|
|
110
|
-
}, [
|
|
180
|
+
}, [
|
|
181
|
+
locale,
|
|
182
|
+
enablePartialResults,
|
|
183
|
+
onError,
|
|
184
|
+
continuous,
|
|
185
|
+
maxSilenceDuration,
|
|
186
|
+
shouldContinue,
|
|
187
|
+
]);
|
|
111
188
|
const stop = useCallback(async () => {
|
|
112
189
|
try {
|
|
190
|
+
setShouldContinue(false);
|
|
191
|
+
if (silenceTimerRef.current) {
|
|
192
|
+
clearTimeout(silenceTimerRef.current);
|
|
193
|
+
silenceTimerRef.current = null;
|
|
194
|
+
}
|
|
113
195
|
await Voice.stop();
|
|
196
|
+
onStop?.();
|
|
114
197
|
}
|
|
115
198
|
catch (e) {
|
|
116
199
|
const errorMessage = e instanceof Error ? e.message : 'Failed to stop recording';
|
|
117
200
|
setError(errorMessage);
|
|
118
201
|
onError?.(errorMessage);
|
|
119
202
|
}
|
|
120
|
-
}, [onError]);
|
|
203
|
+
}, [onError, onStop]);
|
|
121
204
|
const cancel = useCallback(async () => {
|
|
122
205
|
try {
|
|
206
|
+
setShouldContinue(false);
|
|
207
|
+
if (silenceTimerRef.current) {
|
|
208
|
+
clearTimeout(silenceTimerRef.current);
|
|
209
|
+
silenceTimerRef.current = null;
|
|
210
|
+
}
|
|
123
211
|
await Voice.cancel();
|
|
124
212
|
setRecognizedText('');
|
|
125
213
|
setPartialText('');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VoiceMicrophone.js","sourceRoot":"","sources":["../../src/components/VoiceMicrophone.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAChE,OAAO,KAAK,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"VoiceMicrophone.js","sourceRoot":"","sources":["../../src/components/VoiceMicrophone.tsx"],"names":[],"mappings":"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"}
|
|
@@ -9,6 +9,18 @@ export interface UseVoiceRecognitionOptions {
|
|
|
9
9
|
* @default true
|
|
10
10
|
*/
|
|
11
11
|
enablePartialResults?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Whether to continue listening after getting results (continuous mode)
|
|
14
|
+
* When enabled, the microphone will automatically restart after getting results
|
|
15
|
+
* @default false
|
|
16
|
+
*/
|
|
17
|
+
continuous?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Maximum silence duration in milliseconds before stopping (continuous mode)
|
|
20
|
+
* Only applies when continuous mode is enabled
|
|
21
|
+
* @default 5000 (5 seconds)
|
|
22
|
+
*/
|
|
23
|
+
maxSilenceDuration?: number;
|
|
12
24
|
/**
|
|
13
25
|
* Callback fired when speech is recognized
|
|
14
26
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useVoiceRecognition.d.ts","sourceRoot":"","sources":["../../src/hooks/useVoiceRecognition.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAE/B;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAElC;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,yBAAyB;IACxC;;OAEG;IACH,WAAW,EAAE,OAAO,CAAC;IAErB;;OAEG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;IAElB;;OAEG;IACH,cAAc,EAAE,MAAM,EAAE,CAAC;IAEzB;;OAEG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAErB;;OAEG;IACH,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3B;;OAEG;IACH,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1B;;OAEG;IACH,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5B;;OAEG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mBAAmB,GAC9B,UAAS,0BAA+B,KACvC,
|
|
1
|
+
{"version":3,"file":"useVoiceRecognition.d.ts","sourceRoot":"","sources":["../../src/hooks/useVoiceRecognition.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAE/B;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAElC;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,yBAAyB;IACxC;;OAEG;IACH,WAAW,EAAE,OAAO,CAAC;IAErB;;OAEG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;IAElB;;OAEG;IACH,cAAc,EAAE,MAAM,EAAE,CAAC;IAEzB;;OAEG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAErB;;OAEG;IACH,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3B;;OAEG;IACH,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1B;;OAEG;IACH,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5B;;OAEG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mBAAmB,GAC9B,UAAS,0BAA+B,KACvC,yBAiOF,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState, useCallback } from 'react';
|
|
1
|
+
import React, { useEffect, useState, useCallback } from 'react';
|
|
2
2
|
import Voice from '../index';
|
|
3
3
|
/**
|
|
4
4
|
* Custom hook for voice recognition
|
|
@@ -22,32 +22,76 @@ import Voice from '../index';
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
export const useVoiceRecognition = (options = {}) => {
|
|
25
|
-
const { locale = 'en-US', enablePartialResults = true, onResult, onError, } = options;
|
|
25
|
+
const { locale = 'en-US', enablePartialResults = true, continuous = false, maxSilenceDuration = 5000, onResult, onError, } = options;
|
|
26
26
|
const [isRecording, setIsRecording] = useState(false);
|
|
27
27
|
const [results, setResults] = useState([]);
|
|
28
28
|
const [partialResults, setPartialResults] = useState([]);
|
|
29
29
|
const [error, setError] = useState(null);
|
|
30
|
+
const [shouldContinue, setShouldContinue] = useState(false);
|
|
31
|
+
const silenceTimerRef = React.useRef(null);
|
|
32
|
+
const accumulatedTextRef = React.useRef('');
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
// Clear any existing timers on cleanup
|
|
35
|
+
return () => {
|
|
36
|
+
if (silenceTimerRef.current) {
|
|
37
|
+
clearTimeout(silenceTimerRef.current);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}, []);
|
|
30
41
|
useEffect(() => {
|
|
31
42
|
// Set up event listeners
|
|
32
43
|
Voice.onSpeechStart = () => {
|
|
33
44
|
setIsRecording(true);
|
|
34
45
|
setError(null);
|
|
46
|
+
if (silenceTimerRef.current) {
|
|
47
|
+
clearTimeout(silenceTimerRef.current);
|
|
48
|
+
}
|
|
35
49
|
};
|
|
36
|
-
Voice.onSpeechEnd = () => {
|
|
50
|
+
Voice.onSpeechEnd = async () => {
|
|
37
51
|
setIsRecording(false);
|
|
52
|
+
// In continuous mode, restart listening after results
|
|
53
|
+
if (continuous && shouldContinue) {
|
|
54
|
+
setTimeout(async () => {
|
|
55
|
+
if (shouldContinue) {
|
|
56
|
+
try {
|
|
57
|
+
await Voice.start(locale, {
|
|
58
|
+
EXTRA_PARTIAL_RESULTS: enablePartialResults,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
console.error('Failed to restart voice recognition:', err);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, 100);
|
|
66
|
+
}
|
|
38
67
|
};
|
|
39
68
|
Voice.onSpeechError = (e) => {
|
|
40
69
|
const errorMessage = e.error?.message || 'Unknown error';
|
|
41
70
|
setError(errorMessage);
|
|
42
71
|
setIsRecording(false);
|
|
72
|
+
setShouldContinue(false);
|
|
73
|
+
if (silenceTimerRef.current) {
|
|
74
|
+
clearTimeout(silenceTimerRef.current);
|
|
75
|
+
}
|
|
43
76
|
onError?.(errorMessage);
|
|
44
77
|
};
|
|
45
78
|
Voice.onSpeechResults = (e) => {
|
|
46
79
|
if (e.value && e.value.length > 0) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
80
|
+
if (continuous) {
|
|
81
|
+
// Append new text to accumulated text
|
|
82
|
+
const newText = e.value[0];
|
|
83
|
+
accumulatedTextRef.current = accumulatedTextRef.current
|
|
84
|
+
? accumulatedTextRef.current + ' ' + newText
|
|
85
|
+
: newText;
|
|
86
|
+
setResults([accumulatedTextRef.current, ...e.value.slice(1)]);
|
|
87
|
+
onResult?.(accumulatedTextRef.current);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
setResults(e.value);
|
|
91
|
+
const firstResult = e.value[0];
|
|
92
|
+
if (firstResult) {
|
|
93
|
+
onResult?.(firstResult);
|
|
94
|
+
}
|
|
51
95
|
}
|
|
52
96
|
}
|
|
53
97
|
};
|
|
@@ -55,6 +99,15 @@ export const useVoiceRecognition = (options = {}) => {
|
|
|
55
99
|
Voice.onSpeechPartialResults = (e) => {
|
|
56
100
|
if (e.value && e.value.length > 0) {
|
|
57
101
|
setPartialResults(e.value);
|
|
102
|
+
// Reset silence timer on partial results (user is speaking)
|
|
103
|
+
if (continuous && silenceTimerRef.current) {
|
|
104
|
+
clearTimeout(silenceTimerRef.current);
|
|
105
|
+
silenceTimerRef.current = setTimeout(() => {
|
|
106
|
+
if (shouldContinue) {
|
|
107
|
+
stop();
|
|
108
|
+
}
|
|
109
|
+
}, maxSilenceDuration);
|
|
110
|
+
}
|
|
58
111
|
}
|
|
59
112
|
};
|
|
60
113
|
}
|
|
@@ -62,12 +115,24 @@ export const useVoiceRecognition = (options = {}) => {
|
|
|
62
115
|
return () => {
|
|
63
116
|
Voice.destroy().then(Voice.removeAllListeners);
|
|
64
117
|
};
|
|
65
|
-
}, [
|
|
118
|
+
}, [
|
|
119
|
+
enablePartialResults,
|
|
120
|
+
onResult,
|
|
121
|
+
onError,
|
|
122
|
+
continuous,
|
|
123
|
+
shouldContinue,
|
|
124
|
+
locale,
|
|
125
|
+
maxSilenceDuration,
|
|
126
|
+
]);
|
|
66
127
|
const start = useCallback(async () => {
|
|
67
128
|
try {
|
|
68
129
|
setError(null);
|
|
69
|
-
|
|
70
|
-
|
|
130
|
+
if (!continuous) {
|
|
131
|
+
setResults([]);
|
|
132
|
+
setPartialResults([]);
|
|
133
|
+
accumulatedTextRef.current = '';
|
|
134
|
+
}
|
|
135
|
+
setShouldContinue(true);
|
|
71
136
|
// Check permission (Android only)
|
|
72
137
|
const hasPermission = await Voice.checkMicrophonePermission();
|
|
73
138
|
if (!hasPermission) {
|
|
@@ -80,15 +145,36 @@ export const useVoiceRecognition = (options = {}) => {
|
|
|
80
145
|
await Voice.start(locale, {
|
|
81
146
|
EXTRA_PARTIAL_RESULTS: enablePartialResults,
|
|
82
147
|
});
|
|
148
|
+
// Start silence timer if in continuous mode
|
|
149
|
+
if (continuous) {
|
|
150
|
+
silenceTimerRef.current = setTimeout(() => {
|
|
151
|
+
if (shouldContinue) {
|
|
152
|
+
stop();
|
|
153
|
+
}
|
|
154
|
+
}, maxSilenceDuration);
|
|
155
|
+
}
|
|
83
156
|
}
|
|
84
157
|
catch (e) {
|
|
85
158
|
const errorMessage = e instanceof Error ? e.message : 'Failed to start recording';
|
|
86
159
|
setError(errorMessage);
|
|
160
|
+
setShouldContinue(false);
|
|
87
161
|
onError?.(errorMessage);
|
|
88
162
|
}
|
|
89
|
-
}, [
|
|
163
|
+
}, [
|
|
164
|
+
locale,
|
|
165
|
+
enablePartialResults,
|
|
166
|
+
onError,
|
|
167
|
+
continuous,
|
|
168
|
+
maxSilenceDuration,
|
|
169
|
+
shouldContinue,
|
|
170
|
+
]);
|
|
90
171
|
const stop = useCallback(async () => {
|
|
91
172
|
try {
|
|
173
|
+
setShouldContinue(false);
|
|
174
|
+
if (silenceTimerRef.current) {
|
|
175
|
+
clearTimeout(silenceTimerRef.current);
|
|
176
|
+
silenceTimerRef.current = null;
|
|
177
|
+
}
|
|
92
178
|
await Voice.stop();
|
|
93
179
|
}
|
|
94
180
|
catch (e) {
|
|
@@ -99,9 +185,15 @@ export const useVoiceRecognition = (options = {}) => {
|
|
|
99
185
|
}, [onError]);
|
|
100
186
|
const cancel = useCallback(async () => {
|
|
101
187
|
try {
|
|
188
|
+
setShouldContinue(false);
|
|
189
|
+
if (silenceTimerRef.current) {
|
|
190
|
+
clearTimeout(silenceTimerRef.current);
|
|
191
|
+
silenceTimerRef.current = null;
|
|
192
|
+
}
|
|
102
193
|
await Voice.cancel();
|
|
103
194
|
setResults([]);
|
|
104
195
|
setPartialResults([]);
|
|
196
|
+
accumulatedTextRef.current = '';
|
|
105
197
|
}
|
|
106
198
|
catch (e) {
|
|
107
199
|
const errorMessage = e instanceof Error ? e.message : 'Failed to cancel recording';
|
|
@@ -114,6 +206,12 @@ export const useVoiceRecognition = (options = {}) => {
|
|
|
114
206
|
setPartialResults([]);
|
|
115
207
|
setError(null);
|
|
116
208
|
setIsRecording(false);
|
|
209
|
+
accumulatedTextRef.current = '';
|
|
210
|
+
setShouldContinue(false);
|
|
211
|
+
if (silenceTimerRef.current) {
|
|
212
|
+
clearTimeout(silenceTimerRef.current);
|
|
213
|
+
silenceTimerRef.current = null;
|
|
214
|
+
}
|
|
117
215
|
}, []);
|
|
118
216
|
return {
|
|
119
217
|
isRecording,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useVoiceRecognition.js","sourceRoot":"","sources":["../../src/hooks/useVoiceRecognition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useVoiceRecognition.js","sourceRoot":"","sources":["../../src/hooks/useVoiceRecognition.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-voice-ts",
|
|
3
3
|
"description": "Advanced Speech-to-Text library for React Native with TypeScript support. Features ready-to-use components (VoiceMicrophone), custom hooks (useVoiceRecognition), real-time transcription, multi-language support, and comprehensive voice recognition capabilities for iOS and Android.",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.2",
|
|
5
5
|
"author": "Noor Mohammad <noor.jsdivs@gmail.com>",
|
|
6
6
|
"private": false,
|
|
7
7
|
"homepage": "https://github.com/noorjsdivs/react-native-voice-ts",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"react": "^19.2.1",
|
|
23
23
|
"react-native": "^0.83.0",
|
|
24
24
|
"react-native-svg": "^15.15.1",
|
|
25
|
-
"semantic-release": "^
|
|
25
|
+
"semantic-release": "^23.0.0",
|
|
26
26
|
"typescript": "5.9.3"
|
|
27
27
|
},
|
|
28
28
|
"keywords": [
|
|
@@ -46,6 +46,20 @@ export interface VoiceMicrophoneProps {
|
|
|
46
46
|
*/
|
|
47
47
|
enablePartialResults?: boolean;
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Whether to continue listening after getting results (continuous mode)
|
|
51
|
+
* When enabled, the microphone will automatically restart after getting results
|
|
52
|
+
* @default false
|
|
53
|
+
*/
|
|
54
|
+
continuous?: boolean;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Maximum silence duration in milliseconds before stopping (continuous mode)
|
|
58
|
+
* Only applies when continuous mode is enabled
|
|
59
|
+
* @default 5000 (5 seconds)
|
|
60
|
+
*/
|
|
61
|
+
maxSilenceDuration?: number;
|
|
62
|
+
|
|
49
63
|
/**
|
|
50
64
|
* Custom render function for the component
|
|
51
65
|
* Receives isRecording state and control functions
|
|
@@ -97,38 +111,85 @@ const VoiceMicrophone: React.FC<VoiceMicrophoneProps> = ({
|
|
|
97
111
|
locale = 'en-US',
|
|
98
112
|
autoStart = false,
|
|
99
113
|
enablePartialResults = true,
|
|
114
|
+
continuous = false,
|
|
115
|
+
maxSilenceDuration = 5000,
|
|
100
116
|
children,
|
|
101
117
|
}) => {
|
|
102
118
|
const [isRecording, setIsRecording] = useState(false);
|
|
103
119
|
const [recognizedText, setRecognizedText] = useState('');
|
|
104
120
|
const [partialText, setPartialText] = useState('');
|
|
105
121
|
const [error, setError] = useState<string | null>(null);
|
|
122
|
+
const [shouldContinue, setShouldContinue] = useState(false);
|
|
123
|
+
const silenceTimerRef = React.useRef<NodeJS.Timeout | null>(null);
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
// Clear any existing timers on cleanup
|
|
127
|
+
return () => {
|
|
128
|
+
if (silenceTimerRef.current) {
|
|
129
|
+
clearTimeout(silenceTimerRef.current);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}, []);
|
|
106
133
|
|
|
107
134
|
useEffect(() => {
|
|
108
135
|
// Set up event listeners
|
|
109
136
|
Voice.onSpeechStart = () => {
|
|
110
137
|
setIsRecording(true);
|
|
111
138
|
setError(null);
|
|
139
|
+
if (silenceTimerRef.current) {
|
|
140
|
+
clearTimeout(silenceTimerRef.current);
|
|
141
|
+
}
|
|
112
142
|
onStart?.();
|
|
113
143
|
};
|
|
114
144
|
|
|
115
|
-
Voice.onSpeechEnd = () => {
|
|
145
|
+
Voice.onSpeechEnd = async () => {
|
|
116
146
|
setIsRecording(false);
|
|
117
|
-
|
|
147
|
+
|
|
148
|
+
// In continuous mode, restart listening after results
|
|
149
|
+
if (continuous && shouldContinue) {
|
|
150
|
+
// Small delay before restarting
|
|
151
|
+
setTimeout(async () => {
|
|
152
|
+
if (shouldContinue) {
|
|
153
|
+
try {
|
|
154
|
+
await Voice.start(locale, {
|
|
155
|
+
EXTRA_PARTIAL_RESULTS: enablePartialResults,
|
|
156
|
+
});
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error('Failed to restart voice recognition:', err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}, 100);
|
|
162
|
+
} else {
|
|
163
|
+
onStop?.();
|
|
164
|
+
}
|
|
118
165
|
};
|
|
119
166
|
|
|
120
167
|
Voice.onSpeechError = (e: SpeechErrorEvent) => {
|
|
121
168
|
const errorMessage = e.error?.message || 'Unknown error';
|
|
122
169
|
setError(errorMessage);
|
|
123
170
|
setIsRecording(false);
|
|
171
|
+
setShouldContinue(false);
|
|
172
|
+
if (silenceTimerRef.current) {
|
|
173
|
+
clearTimeout(silenceTimerRef.current);
|
|
174
|
+
}
|
|
124
175
|
onError?.(errorMessage);
|
|
125
176
|
};
|
|
126
177
|
|
|
127
178
|
Voice.onSpeechResults = (e: SpeechResultsEvent) => {
|
|
128
179
|
if (e.value && e.value.length > 0) {
|
|
129
180
|
const text = e.value[0];
|
|
130
|
-
|
|
131
|
-
|
|
181
|
+
|
|
182
|
+
// In continuous mode, append new text to existing
|
|
183
|
+
if (continuous && recognizedText) {
|
|
184
|
+
const updatedText = recognizedText + ' ' + text;
|
|
185
|
+
setRecognizedText(updatedText);
|
|
186
|
+
onSpeechResult?.(updatedText);
|
|
187
|
+
} else {
|
|
188
|
+
setRecognizedText(text);
|
|
189
|
+
onSpeechResult?.(text);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
setPartialText('');
|
|
132
193
|
}
|
|
133
194
|
};
|
|
134
195
|
|
|
@@ -138,6 +199,16 @@ const VoiceMicrophone: React.FC<VoiceMicrophoneProps> = ({
|
|
|
138
199
|
const text = e.value[0];
|
|
139
200
|
setPartialText(text);
|
|
140
201
|
onPartialResult?.(text);
|
|
202
|
+
|
|
203
|
+
// Reset silence timer on partial results (user is speaking)
|
|
204
|
+
if (continuous && silenceTimerRef.current) {
|
|
205
|
+
clearTimeout(silenceTimerRef.current);
|
|
206
|
+
silenceTimerRef.current = setTimeout(() => {
|
|
207
|
+
if (shouldContinue) {
|
|
208
|
+
stop();
|
|
209
|
+
}
|
|
210
|
+
}, maxSilenceDuration);
|
|
211
|
+
}
|
|
141
212
|
}
|
|
142
213
|
};
|
|
143
214
|
}
|
|
@@ -153,6 +224,11 @@ const VoiceMicrophone: React.FC<VoiceMicrophoneProps> = ({
|
|
|
153
224
|
onStop,
|
|
154
225
|
onError,
|
|
155
226
|
enablePartialResults,
|
|
227
|
+
continuous,
|
|
228
|
+
shouldContinue,
|
|
229
|
+
recognizedText,
|
|
230
|
+
locale,
|
|
231
|
+
maxSilenceDuration,
|
|
156
232
|
]);
|
|
157
233
|
|
|
158
234
|
// Auto-start if enabled
|
|
@@ -166,8 +242,11 @@ const VoiceMicrophone: React.FC<VoiceMicrophoneProps> = ({
|
|
|
166
242
|
const start = useCallback(async () => {
|
|
167
243
|
try {
|
|
168
244
|
setError(null);
|
|
169
|
-
|
|
170
|
-
|
|
245
|
+
if (!continuous) {
|
|
246
|
+
setRecognizedText('');
|
|
247
|
+
setPartialText('');
|
|
248
|
+
}
|
|
249
|
+
setShouldContinue(true);
|
|
171
250
|
|
|
172
251
|
// Check permission (Android only)
|
|
173
252
|
const hasPermission = await Voice.checkMicrophonePermission();
|
|
@@ -182,27 +261,55 @@ const VoiceMicrophone: React.FC<VoiceMicrophoneProps> = ({
|
|
|
182
261
|
await Voice.start(locale, {
|
|
183
262
|
EXTRA_PARTIAL_RESULTS: enablePartialResults,
|
|
184
263
|
});
|
|
264
|
+
|
|
265
|
+
// Start silence timer if in continuous mode
|
|
266
|
+
if (continuous) {
|
|
267
|
+
silenceTimerRef.current = setTimeout(() => {
|
|
268
|
+
if (shouldContinue) {
|
|
269
|
+
stop();
|
|
270
|
+
}
|
|
271
|
+
}, maxSilenceDuration);
|
|
272
|
+
}
|
|
185
273
|
} catch (e) {
|
|
186
274
|
const errorMessage =
|
|
187
275
|
e instanceof Error ? e.message : 'Failed to start recording';
|
|
188
276
|
setError(errorMessage);
|
|
277
|
+
setShouldContinue(false);
|
|
189
278
|
onError?.(errorMessage);
|
|
190
279
|
}
|
|
191
|
-
}, [
|
|
280
|
+
}, [
|
|
281
|
+
locale,
|
|
282
|
+
enablePartialResults,
|
|
283
|
+
onError,
|
|
284
|
+
continuous,
|
|
285
|
+
maxSilenceDuration,
|
|
286
|
+
shouldContinue,
|
|
287
|
+
]);
|
|
192
288
|
|
|
193
289
|
const stop = useCallback(async () => {
|
|
194
290
|
try {
|
|
291
|
+
setShouldContinue(false);
|
|
292
|
+
if (silenceTimerRef.current) {
|
|
293
|
+
clearTimeout(silenceTimerRef.current);
|
|
294
|
+
silenceTimerRef.current = null;
|
|
295
|
+
}
|
|
195
296
|
await Voice.stop();
|
|
297
|
+
onStop?.();
|
|
196
298
|
} catch (e) {
|
|
197
299
|
const errorMessage =
|
|
198
300
|
e instanceof Error ? e.message : 'Failed to stop recording';
|
|
199
301
|
setError(errorMessage);
|
|
200
302
|
onError?.(errorMessage);
|
|
201
303
|
}
|
|
202
|
-
}, [onError]);
|
|
304
|
+
}, [onError, onStop]);
|
|
203
305
|
|
|
204
306
|
const cancel = useCallback(async () => {
|
|
205
307
|
try {
|
|
308
|
+
setShouldContinue(false);
|
|
309
|
+
if (silenceTimerRef.current) {
|
|
310
|
+
clearTimeout(silenceTimerRef.current);
|
|
311
|
+
silenceTimerRef.current = null;
|
|
312
|
+
}
|
|
206
313
|
await Voice.cancel();
|
|
207
314
|
setRecognizedText('');
|
|
208
315
|
setPartialText('');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState, useCallback } from 'react';
|
|
1
|
+
import React, { useEffect, useState, useCallback } from 'react';
|
|
2
2
|
import Voice from '../index';
|
|
3
3
|
import type { SpeechErrorEvent, SpeechResultsEvent } from '../VoiceModuleTypes';
|
|
4
4
|
|
|
@@ -15,6 +15,20 @@ export interface UseVoiceRecognitionOptions {
|
|
|
15
15
|
*/
|
|
16
16
|
enablePartialResults?: boolean;
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Whether to continue listening after getting results (continuous mode)
|
|
20
|
+
* When enabled, the microphone will automatically restart after getting results
|
|
21
|
+
* @default false
|
|
22
|
+
*/
|
|
23
|
+
continuous?: boolean;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Maximum silence duration in milliseconds before stopping (continuous mode)
|
|
27
|
+
* Only applies when continuous mode is enabled
|
|
28
|
+
* @default 5000 (5 seconds)
|
|
29
|
+
*/
|
|
30
|
+
maxSilenceDuration?: number;
|
|
31
|
+
|
|
18
32
|
/**
|
|
19
33
|
* Callback fired when speech is recognized
|
|
20
34
|
*/
|
|
@@ -95,6 +109,8 @@ export const useVoiceRecognition = (
|
|
|
95
109
|
const {
|
|
96
110
|
locale = 'en-US',
|
|
97
111
|
enablePartialResults = true,
|
|
112
|
+
continuous = false,
|
|
113
|
+
maxSilenceDuration = 5000,
|
|
98
114
|
onResult,
|
|
99
115
|
onError,
|
|
100
116
|
} = options;
|
|
@@ -103,31 +119,75 @@ export const useVoiceRecognition = (
|
|
|
103
119
|
const [results, setResults] = useState<string[]>([]);
|
|
104
120
|
const [partialResults, setPartialResults] = useState<string[]>([]);
|
|
105
121
|
const [error, setError] = useState<string | null>(null);
|
|
122
|
+
const [shouldContinue, setShouldContinue] = useState(false);
|
|
123
|
+
const silenceTimerRef = React.useRef<NodeJS.Timeout | null>(null);
|
|
124
|
+
const accumulatedTextRef = React.useRef<string>('');
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
// Clear any existing timers on cleanup
|
|
128
|
+
return () => {
|
|
129
|
+
if (silenceTimerRef.current) {
|
|
130
|
+
clearTimeout(silenceTimerRef.current);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}, []);
|
|
106
134
|
|
|
107
135
|
useEffect(() => {
|
|
108
136
|
// Set up event listeners
|
|
109
137
|
Voice.onSpeechStart = () => {
|
|
110
138
|
setIsRecording(true);
|
|
111
139
|
setError(null);
|
|
140
|
+
if (silenceTimerRef.current) {
|
|
141
|
+
clearTimeout(silenceTimerRef.current);
|
|
142
|
+
}
|
|
112
143
|
};
|
|
113
144
|
|
|
114
|
-
Voice.onSpeechEnd = () => {
|
|
145
|
+
Voice.onSpeechEnd = async () => {
|
|
115
146
|
setIsRecording(false);
|
|
147
|
+
|
|
148
|
+
// In continuous mode, restart listening after results
|
|
149
|
+
if (continuous && shouldContinue) {
|
|
150
|
+
setTimeout(async () => {
|
|
151
|
+
if (shouldContinue) {
|
|
152
|
+
try {
|
|
153
|
+
await Voice.start(locale, {
|
|
154
|
+
EXTRA_PARTIAL_RESULTS: enablePartialResults,
|
|
155
|
+
});
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.error('Failed to restart voice recognition:', err);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}, 100);
|
|
161
|
+
}
|
|
116
162
|
};
|
|
117
163
|
|
|
118
164
|
Voice.onSpeechError = (e: SpeechErrorEvent) => {
|
|
119
165
|
const errorMessage = e.error?.message || 'Unknown error';
|
|
120
166
|
setError(errorMessage);
|
|
121
167
|
setIsRecording(false);
|
|
168
|
+
setShouldContinue(false);
|
|
169
|
+
if (silenceTimerRef.current) {
|
|
170
|
+
clearTimeout(silenceTimerRef.current);
|
|
171
|
+
}
|
|
122
172
|
onError?.(errorMessage);
|
|
123
173
|
};
|
|
124
174
|
|
|
125
175
|
Voice.onSpeechResults = (e: SpeechResultsEvent) => {
|
|
126
176
|
if (e.value && e.value.length > 0) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
177
|
+
if (continuous) {
|
|
178
|
+
// Append new text to accumulated text
|
|
179
|
+
const newText = e.value[0];
|
|
180
|
+
accumulatedTextRef.current = accumulatedTextRef.current
|
|
181
|
+
? accumulatedTextRef.current + ' ' + newText
|
|
182
|
+
: newText;
|
|
183
|
+
setResults([accumulatedTextRef.current, ...e.value.slice(1)]);
|
|
184
|
+
onResult?.(accumulatedTextRef.current);
|
|
185
|
+
} else {
|
|
186
|
+
setResults(e.value);
|
|
187
|
+
const firstResult = e.value[0];
|
|
188
|
+
if (firstResult) {
|
|
189
|
+
onResult?.(firstResult);
|
|
190
|
+
}
|
|
131
191
|
}
|
|
132
192
|
}
|
|
133
193
|
};
|
|
@@ -136,6 +196,16 @@ export const useVoiceRecognition = (
|
|
|
136
196
|
Voice.onSpeechPartialResults = (e: SpeechResultsEvent) => {
|
|
137
197
|
if (e.value && e.value.length > 0) {
|
|
138
198
|
setPartialResults(e.value);
|
|
199
|
+
|
|
200
|
+
// Reset silence timer on partial results (user is speaking)
|
|
201
|
+
if (continuous && silenceTimerRef.current) {
|
|
202
|
+
clearTimeout(silenceTimerRef.current);
|
|
203
|
+
silenceTimerRef.current = setTimeout(() => {
|
|
204
|
+
if (shouldContinue) {
|
|
205
|
+
stop();
|
|
206
|
+
}
|
|
207
|
+
}, maxSilenceDuration);
|
|
208
|
+
}
|
|
139
209
|
}
|
|
140
210
|
};
|
|
141
211
|
}
|
|
@@ -144,13 +214,25 @@ export const useVoiceRecognition = (
|
|
|
144
214
|
return () => {
|
|
145
215
|
Voice.destroy().then(Voice.removeAllListeners);
|
|
146
216
|
};
|
|
147
|
-
}, [
|
|
217
|
+
}, [
|
|
218
|
+
enablePartialResults,
|
|
219
|
+
onResult,
|
|
220
|
+
onError,
|
|
221
|
+
continuous,
|
|
222
|
+
shouldContinue,
|
|
223
|
+
locale,
|
|
224
|
+
maxSilenceDuration,
|
|
225
|
+
]);
|
|
148
226
|
|
|
149
227
|
const start = useCallback(async () => {
|
|
150
228
|
try {
|
|
151
229
|
setError(null);
|
|
152
|
-
|
|
153
|
-
|
|
230
|
+
if (!continuous) {
|
|
231
|
+
setResults([]);
|
|
232
|
+
setPartialResults([]);
|
|
233
|
+
accumulatedTextRef.current = '';
|
|
234
|
+
}
|
|
235
|
+
setShouldContinue(true);
|
|
154
236
|
|
|
155
237
|
// Check permission (Android only)
|
|
156
238
|
const hasPermission = await Voice.checkMicrophonePermission();
|
|
@@ -165,16 +247,38 @@ export const useVoiceRecognition = (
|
|
|
165
247
|
await Voice.start(locale, {
|
|
166
248
|
EXTRA_PARTIAL_RESULTS: enablePartialResults,
|
|
167
249
|
});
|
|
250
|
+
|
|
251
|
+
// Start silence timer if in continuous mode
|
|
252
|
+
if (continuous) {
|
|
253
|
+
silenceTimerRef.current = setTimeout(() => {
|
|
254
|
+
if (shouldContinue) {
|
|
255
|
+
stop();
|
|
256
|
+
}
|
|
257
|
+
}, maxSilenceDuration);
|
|
258
|
+
}
|
|
168
259
|
} catch (e) {
|
|
169
260
|
const errorMessage =
|
|
170
261
|
e instanceof Error ? e.message : 'Failed to start recording';
|
|
171
262
|
setError(errorMessage);
|
|
263
|
+
setShouldContinue(false);
|
|
172
264
|
onError?.(errorMessage);
|
|
173
265
|
}
|
|
174
|
-
}, [
|
|
266
|
+
}, [
|
|
267
|
+
locale,
|
|
268
|
+
enablePartialResults,
|
|
269
|
+
onError,
|
|
270
|
+
continuous,
|
|
271
|
+
maxSilenceDuration,
|
|
272
|
+
shouldContinue,
|
|
273
|
+
]);
|
|
175
274
|
|
|
176
275
|
const stop = useCallback(async () => {
|
|
177
276
|
try {
|
|
277
|
+
setShouldContinue(false);
|
|
278
|
+
if (silenceTimerRef.current) {
|
|
279
|
+
clearTimeout(silenceTimerRef.current);
|
|
280
|
+
silenceTimerRef.current = null;
|
|
281
|
+
}
|
|
178
282
|
await Voice.stop();
|
|
179
283
|
} catch (e) {
|
|
180
284
|
const errorMessage =
|
|
@@ -186,9 +290,15 @@ export const useVoiceRecognition = (
|
|
|
186
290
|
|
|
187
291
|
const cancel = useCallback(async () => {
|
|
188
292
|
try {
|
|
293
|
+
setShouldContinue(false);
|
|
294
|
+
if (silenceTimerRef.current) {
|
|
295
|
+
clearTimeout(silenceTimerRef.current);
|
|
296
|
+
silenceTimerRef.current = null;
|
|
297
|
+
}
|
|
189
298
|
await Voice.cancel();
|
|
190
299
|
setResults([]);
|
|
191
300
|
setPartialResults([]);
|
|
301
|
+
accumulatedTextRef.current = '';
|
|
192
302
|
} catch (e) {
|
|
193
303
|
const errorMessage =
|
|
194
304
|
e instanceof Error ? e.message : 'Failed to cancel recording';
|
|
@@ -202,6 +312,12 @@ export const useVoiceRecognition = (
|
|
|
202
312
|
setPartialResults([]);
|
|
203
313
|
setError(null);
|
|
204
314
|
setIsRecording(false);
|
|
315
|
+
accumulatedTextRef.current = '';
|
|
316
|
+
setShouldContinue(false);
|
|
317
|
+
if (silenceTimerRef.current) {
|
|
318
|
+
clearTimeout(silenceTimerRef.current);
|
|
319
|
+
silenceTimerRef.current = null;
|
|
320
|
+
}
|
|
205
321
|
}, []);
|
|
206
322
|
|
|
207
323
|
return {
|