react-native-voice-ts 1.0.4 → 1.0.6

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.
@@ -0,0 +1,384 @@
1
+ import React, { useEffect, useState, useCallback } from 'react';
2
+ import Voice from '../index';
3
+ import type { SpeechErrorEvent, SpeechResultsEvent } from '../VoiceModuleTypes';
4
+
5
+ export interface UseVoiceRecognitionOptions {
6
+ /**
7
+ * Language locale for speech recognition
8
+ * @default 'en-US'
9
+ */
10
+ locale?: string;
11
+
12
+ /**
13
+ * Whether to enable partial results (real-time transcription)
14
+ * @default true
15
+ */
16
+ enablePartialResults?: boolean;
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
+
32
+ /**
33
+ * Callback fired when speech is recognized
34
+ */
35
+ onResult?: (text: string) => void;
36
+
37
+ /**
38
+ * Callback fired when partial results are available (real-time)
39
+ */
40
+ onPartialResult?: (text: string) => void;
41
+
42
+ /**
43
+ * Callback fired when an error occurs
44
+ */
45
+ onError?: (error: string) => void;
46
+
47
+ /**
48
+ * Callback fired when recording starts
49
+ */
50
+ onStart?: () => void;
51
+
52
+ /**
53
+ * Callback fired when recording ends
54
+ */
55
+ onEnd?: () => void;
56
+
57
+ /**
58
+ * Callback fired when speech is recognized (before results)
59
+ */
60
+ onRecognized?: () => void;
61
+
62
+ /**
63
+ * Callback fired when volume changes (0-10)
64
+ */
65
+ onVolumeChanged?: (value: number) => void;
66
+ }
67
+
68
+ export interface UseVoiceRecognitionReturn {
69
+ /**
70
+ * Whether voice recognition is currently active
71
+ */
72
+ isRecording: boolean;
73
+
74
+ /**
75
+ * Final recognized text results
76
+ */
77
+ results: string[];
78
+
79
+ /**
80
+ * Partial results (real-time transcription)
81
+ */
82
+ partialResults: string[];
83
+
84
+ /**
85
+ * Error message if an error occurred
86
+ */
87
+ error: string | null;
88
+
89
+ /**
90
+ * Start voice recognition
91
+ */
92
+ start: () => Promise<void>;
93
+
94
+ /**
95
+ * Stop voice recognition and get final results
96
+ */
97
+ stop: () => Promise<void>;
98
+
99
+ /**
100
+ * Cancel voice recognition without getting results
101
+ */
102
+ cancel: () => Promise<void>;
103
+
104
+ /**
105
+ * Reset all state
106
+ */
107
+ reset: () => void;
108
+ }
109
+
110
+ /**
111
+ * Custom hook for voice recognition
112
+ *
113
+ * Provides a simple interface for speech-to-text functionality with automatic
114
+ * event listener setup and cleanup.
115
+ *
116
+ * @example
117
+ * ```tsx
118
+ * const { isRecording, results, start, stop } = useVoiceRecognition({
119
+ * locale: 'en-US',
120
+ * onResult: (text) => setSearchQuery(text),
121
+ * });
122
+ *
123
+ * // In your component
124
+ * <Button
125
+ * onPress={isRecording ? stop : start}
126
+ * title={isRecording ? 'Stop' : 'Start Recording'}
127
+ * />
128
+ * <Text>{results[0]}</Text>
129
+ * ```
130
+ */
131
+ export const useVoiceRecognition = (
132
+ options: UseVoiceRecognitionOptions = {},
133
+ ): UseVoiceRecognitionReturn => {
134
+ const {
135
+ locale = 'en-US',
136
+ enablePartialResults = true,
137
+ continuous = false,
138
+ maxSilenceDuration = 5000,
139
+ onResult,
140
+ onPartialResult,
141
+ onError,
142
+ onStart,
143
+ onEnd,
144
+ onRecognized,
145
+ onVolumeChanged,
146
+ } = options;
147
+
148
+ const [isRecording, setIsRecording] = useState(false);
149
+ const [results, setResults] = useState<string[]>([]);
150
+ const [partialResults, setPartialResults] = useState<string[]>([]);
151
+ const [error, setError] = useState<string | null>(null);
152
+ const shouldContinueRef = React.useRef(false);
153
+ const silenceTimerRef = React.useRef<NodeJS.Timeout | null>(null);
154
+ const accumulatedTextRef = React.useRef<string>('');
155
+
156
+ useEffect(() => {
157
+ // Clear any existing timers on cleanup
158
+ return () => {
159
+ if (silenceTimerRef.current) {
160
+ clearTimeout(silenceTimerRef.current);
161
+ }
162
+ };
163
+ }, []);
164
+
165
+ useEffect(() => {
166
+ // Set up event listeners
167
+ Voice.onSpeechStart = () => {
168
+ setIsRecording(true);
169
+ setError(null);
170
+ if (silenceTimerRef.current) {
171
+ clearTimeout(silenceTimerRef.current);
172
+ }
173
+ onStart?.();
174
+ };
175
+
176
+ Voice.onSpeechEnd = async () => {
177
+ setIsRecording(false);
178
+ onEnd?.();
179
+
180
+ // In continuous mode, restart listening after results
181
+ if (continuous && shouldContinueRef.current) {
182
+ setTimeout(async () => {
183
+ if (shouldContinueRef.current) {
184
+ try {
185
+ await Voice.start(locale, {
186
+ EXTRA_PARTIAL_RESULTS: enablePartialResults,
187
+ });
188
+ } catch (err) {
189
+ console.error('Failed to restart voice recognition:', err);
190
+ }
191
+ }
192
+ }, 100);
193
+ }
194
+ };
195
+
196
+ Voice.onSpeechRecognized = () => {
197
+ onRecognized?.();
198
+ };
199
+
200
+ Voice.onSpeechError = (e: SpeechErrorEvent) => {
201
+ const errorMessage = e.error?.message || 'Unknown error';
202
+ setError(errorMessage);
203
+ setIsRecording(false);
204
+ shouldContinueRef.current = false;
205
+ if (silenceTimerRef.current) {
206
+ clearTimeout(silenceTimerRef.current);
207
+ }
208
+ onError?.(errorMessage);
209
+ };
210
+
211
+ Voice.onSpeechResults = (e: SpeechResultsEvent) => {
212
+ if (e.value && e.value.length > 0) {
213
+ if (continuous) {
214
+ // Append new text to accumulated text
215
+ const newText = e.value[0];
216
+ accumulatedTextRef.current = accumulatedTextRef.current
217
+ ? accumulatedTextRef.current + ' ' + newText
218
+ : newText;
219
+ setResults([accumulatedTextRef.current, ...e.value.slice(1)]);
220
+ onResult?.(accumulatedTextRef.current);
221
+ } else {
222
+ setResults(e.value);
223
+ const firstResult = e.value[0];
224
+ if (firstResult) {
225
+ onResult?.(firstResult);
226
+ }
227
+ }
228
+ }
229
+ };
230
+
231
+ Voice.onSpeechPartialResults = (e: SpeechResultsEvent) => {
232
+ if (e.value && e.value.length > 0) {
233
+ setPartialResults(e.value);
234
+ const firstPartial = e.value[0];
235
+ if (firstPartial) {
236
+ onPartialResult?.(firstPartial);
237
+ }
238
+
239
+ // Reset silence timer on partial results (user is speaking)
240
+ if (continuous && silenceTimerRef.current) {
241
+ clearTimeout(silenceTimerRef.current);
242
+ silenceTimerRef.current = setTimeout(async () => {
243
+ if (shouldContinueRef.current) {
244
+ shouldContinueRef.current = false;
245
+ if (silenceTimerRef.current) {
246
+ clearTimeout(silenceTimerRef.current);
247
+ silenceTimerRef.current = null;
248
+ }
249
+ await Voice.stop();
250
+ }
251
+ }, maxSilenceDuration);
252
+ }
253
+ }
254
+ };
255
+
256
+ Voice.onSpeechVolumeChanged = (e: any) => {
257
+ if (e.value !== undefined) {
258
+ onVolumeChanged?.(e.value);
259
+ }
260
+ };
261
+
262
+ // Cleanup
263
+ return () => {
264
+ Voice.destroy().then(Voice.removeAllListeners);
265
+ };
266
+ }, [
267
+ enablePartialResults,
268
+ onResult,
269
+ onPartialResult,
270
+ onError,
271
+ onStart,
272
+ onEnd,
273
+ onRecognized,
274
+ onVolumeChanged,
275
+ continuous,
276
+ locale,
277
+ maxSilenceDuration,
278
+ ]);
279
+
280
+ const start = useCallback(async () => {
281
+ try {
282
+ setError(null);
283
+ if (!continuous) {
284
+ setResults([]);
285
+ setPartialResults([]);
286
+ accumulatedTextRef.current = '';
287
+ }
288
+ shouldContinueRef.current = true;
289
+
290
+ // Check permission (Android only)
291
+ const hasPermission = await Voice.checkMicrophonePermission();
292
+ if (!hasPermission) {
293
+ const granted = await Voice.requestMicrophonePermission();
294
+ if (!granted) {
295
+ setError('Microphone permission denied');
296
+ return;
297
+ }
298
+ }
299
+
300
+ await Voice.start(locale, {
301
+ EXTRA_PARTIAL_RESULTS: enablePartialResults,
302
+ });
303
+
304
+ // Start silence timer if in continuous mode
305
+ if (continuous) {
306
+ silenceTimerRef.current = setTimeout(async () => {
307
+ if (shouldContinueRef.current) {
308
+ shouldContinueRef.current = false;
309
+ if (silenceTimerRef.current) {
310
+ clearTimeout(silenceTimerRef.current);
311
+ silenceTimerRef.current = null;
312
+ }
313
+ await Voice.stop();
314
+ }
315
+ }, maxSilenceDuration);
316
+ }
317
+ } catch (e) {
318
+ const errorMessage =
319
+ e instanceof Error ? e.message : 'Failed to start recording';
320
+ setError(errorMessage);
321
+ shouldContinueRef.current = false;
322
+ onError?.(errorMessage);
323
+ }
324
+ }, [locale, enablePartialResults, onError, continuous, maxSilenceDuration]);
325
+
326
+ const stop = useCallback(async () => {
327
+ try {
328
+ shouldContinueRef.current = false;
329
+ if (silenceTimerRef.current) {
330
+ clearTimeout(silenceTimerRef.current);
331
+ silenceTimerRef.current = null;
332
+ }
333
+ await Voice.stop();
334
+ } catch (e) {
335
+ const errorMessage =
336
+ e instanceof Error ? e.message : 'Failed to stop recording';
337
+ setError(errorMessage);
338
+ onError?.(errorMessage);
339
+ }
340
+ }, [onError]);
341
+
342
+ const cancel = useCallback(async () => {
343
+ try {
344
+ shouldContinueRef.current = false;
345
+ if (silenceTimerRef.current) {
346
+ clearTimeout(silenceTimerRef.current);
347
+ silenceTimerRef.current = null;
348
+ }
349
+ await Voice.cancel();
350
+ setResults([]);
351
+ setPartialResults([]);
352
+ accumulatedTextRef.current = '';
353
+ } catch (e) {
354
+ const errorMessage =
355
+ e instanceof Error ? e.message : 'Failed to cancel recording';
356
+ setError(errorMessage);
357
+ onError?.(errorMessage);
358
+ }
359
+ }, [onError]);
360
+
361
+ const reset = useCallback(() => {
362
+ setResults([]);
363
+ setPartialResults([]);
364
+ setError(null);
365
+ setIsRecording(false);
366
+ accumulatedTextRef.current = '';
367
+ shouldContinueRef.current = false;
368
+ if (silenceTimerRef.current) {
369
+ clearTimeout(silenceTimerRef.current);
370
+ silenceTimerRef.current = null;
371
+ }
372
+ }, []);
373
+
374
+ return {
375
+ isRecording,
376
+ results,
377
+ partialResults,
378
+ error,
379
+ start,
380
+ stop,
381
+ cancel,
382
+ reset,
383
+ };
384
+ };