@gmessier/nitro-speech 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +165 -148
- package/android/build.gradle +0 -1
- package/android/src/main/cpp/cpp-adapter.cpp +5 -1
- package/android/src/main/java/com/margelo/nitro/nitrospeech/HybridNitroSpeech.kt +2 -0
- package/android/src/main/java/com/margelo/nitro/nitrospeech/recognizer/AutoStopper.kt +80 -16
- package/android/src/main/java/com/margelo/nitro/nitrospeech/recognizer/HybridRecognizer.kt +93 -20
- package/android/src/main/java/com/margelo/nitro/nitrospeech/recognizer/RecognitionListenerSession.kt +27 -15
- package/ios/{BufferUtil.swift → Audio/AudioBufferConverter.swift} +3 -34
- package/ios/Audio/AudioLevelTracker.swift +66 -0
- package/ios/Coordinator.swift +105 -0
- package/ios/Engines/AnalyzerEngine.swift +241 -0
- package/ios/Engines/DictationRuntime.swift +67 -0
- package/ios/Engines/RecognizerEngine.swift +312 -0
- package/ios/Engines/SFSpeechEngine.swift +119 -0
- package/ios/Engines/SpeechRuntime.swift +58 -0
- package/ios/Engines/TranscriberRuntimeProtocol.swift +21 -0
- package/ios/HybridNitroSpeech.swift +1 -10
- package/ios/HybridRecognizer.swift +135 -192
- package/ios/LocaleManager.swift +73 -0
- package/ios/{AppStateObserver.swift → Shared/AppStateObserver.swift} +1 -2
- package/ios/Shared/AutoStopper.swift +147 -0
- package/ios/Shared/HapticImpact.swift +24 -0
- package/ios/Shared/Log.swift +41 -0
- package/ios/Shared/Permissions.swift +59 -0
- package/ios/Shared/Utils.swift +58 -0
- package/lib/NitroSpeech.d.ts +2 -0
- package/lib/NitroSpeech.js +2 -0
- package/lib/Recognizer/RecognizerRef.d.ts +5 -0
- package/lib/Recognizer/RecognizerRef.js +13 -0
- package/lib/Recognizer/SpeechRecognizer.d.ts +8 -0
- package/lib/Recognizer/SpeechRecognizer.js +9 -0
- package/lib/Recognizer/methods.d.ts +8 -0
- package/lib/Recognizer/methods.js +29 -0
- package/lib/Recognizer/types.d.ts +6 -0
- package/lib/Recognizer/types.js +1 -0
- package/lib/Recognizer/useRecognizer.d.ts +16 -0
- package/lib/Recognizer/useRecognizer.js +71 -0
- package/lib/Recognizer/useVoiceInputVolume.d.ts +25 -0
- package/lib/Recognizer/useVoiceInputVolume.js +52 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/specs/NitroSpeech.nitro.d.ts +8 -0
- package/lib/specs/NitroSpeech.nitro.js +1 -0
- package/lib/specs/Recognizer.nitro.d.ts +95 -0
- package/lib/specs/Recognizer.nitro.js +1 -0
- package/lib/specs/SpeechRecognitionConfig.d.ts +162 -0
- package/lib/specs/SpeechRecognitionConfig.js +1 -0
- package/lib/specs/VolumeChangeEvent.d.ts +31 -0
- package/lib/specs/VolumeChangeEvent.js +1 -0
- package/nitro.json +2 -6
- package/nitrogen/generated/android/NitroSpeech+autolinking.cmake +2 -2
- package/nitrogen/generated/android/NitroSpeechOnLoad.cpp +5 -3
- package/nitrogen/generated/android/c++/JFunc_void_VolumeChangeEvent.hpp +78 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_std__string_.hpp +14 -14
- package/nitrogen/generated/android/c++/JHybridRecognizerSpec.cpp +68 -19
- package/nitrogen/generated/android/c++/JHybridRecognizerSpec.hpp +7 -4
- package/nitrogen/generated/android/c++/JIosPreset.hpp +58 -0
- package/nitrogen/generated/android/c++/JMutableSpeechRecognitionConfig.hpp +79 -0
- package/nitrogen/generated/android/c++/{JSpeechToTextParams.hpp → JSpeechRecognitionConfig.hpp} +48 -30
- package/nitrogen/generated/android/c++/JVolumeChangeEvent.hpp +65 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/Func_void_VolumeChangeEvent.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/HybridRecognizerSpec.kt +18 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/IosPreset.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/MutableSpeechRecognitionConfig.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/SpeechRecognitionConfig.kt +121 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/VolumeChangeEvent.kt +61 -0
- package/nitrogen/generated/ios/NitroSpeech-Swift-Cxx-Bridge.cpp +46 -30
- package/nitrogen/generated/ios/NitroSpeech-Swift-Cxx-Bridge.hpp +203 -70
- package/nitrogen/generated/ios/NitroSpeech-Swift-Cxx-Umbrella.hpp +13 -3
- package/nitrogen/generated/ios/NitroSpeechAutolinking.swift +2 -2
- package/nitrogen/generated/ios/c++/HybridRecognizerSpecSwift.hpp +41 -9
- package/nitrogen/generated/ios/swift/Func_void_VolumeChangeEvent.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridRecognizerSpec.swift +6 -3
- package/nitrogen/generated/ios/swift/HybridRecognizerSpec_cxx.swift +66 -18
- package/nitrogen/generated/ios/swift/IosPreset.swift +40 -0
- package/nitrogen/generated/ios/swift/MutableSpeechRecognitionConfig.swift +118 -0
- package/nitrogen/generated/ios/swift/{SpeechToTextParams.swift → SpeechRecognitionConfig.swift} +108 -43
- package/nitrogen/generated/ios/swift/VolumeChangeEvent.swift +52 -0
- package/nitrogen/generated/shared/c++/HybridRecognizerSpec.cpp +4 -1
- package/nitrogen/generated/shared/c++/HybridRecognizerSpec.hpp +17 -7
- package/nitrogen/generated/shared/c++/IosPreset.hpp +76 -0
- package/nitrogen/generated/shared/c++/MutableSpeechRecognitionConfig.hpp +105 -0
- package/nitrogen/generated/shared/c++/{SpeechToTextParams.hpp → SpeechRecognitionConfig.hpp} +39 -20
- package/nitrogen/generated/shared/c++/VolumeChangeEvent.hpp +91 -0
- package/package.json +15 -16
- package/src/NitroSpeech.ts +5 -0
- package/src/Recognizer/RecognizerRef.ts +23 -0
- package/src/Recognizer/SpeechRecognizer.ts +10 -0
- package/src/Recognizer/methods.ts +40 -0
- package/src/Recognizer/types.ts +33 -0
- package/src/Recognizer/useRecognizer.ts +85 -0
- package/src/Recognizer/useVoiceInputVolume.ts +65 -0
- package/src/index.ts +6 -182
- package/src/specs/NitroSpeech.nitro.ts +2 -163
- package/src/specs/Recognizer.nitro.ts +110 -0
- package/src/specs/SpeechRecognitionConfig.ts +167 -0
- package/src/specs/VolumeChangeEvent.ts +31 -0
- package/android/proguard-rules.pro +0 -1
- package/ios/AnylyzerTranscriber.swift +0 -331
- package/ios/AutoStopper.swift +0 -69
- package/ios/HapticImpact.swift +0 -32
- package/ios/LegacySpeechRecognizer.swift +0 -161
- package/lib/commonjs/index.js +0 -145
- package/lib/commonjs/index.js.map +0 -1
- package/lib/commonjs/package.json +0 -1
- package/lib/commonjs/specs/NitroSpeech.nitro.js +0 -6
- package/lib/commonjs/specs/NitroSpeech.nitro.js.map +0 -1
- package/lib/module/index.js +0 -138
- package/lib/module/index.js.map +0 -1
- package/lib/module/package.json +0 -1
- package/lib/module/specs/NitroSpeech.nitro.js +0 -4
- package/lib/module/specs/NitroSpeech.nitro.js.map +0 -1
- package/lib/tsconfig.tsbuildinfo +0 -1
- package/lib/typescript/index.d.ts +0 -50
- package/lib/typescript/index.d.ts.map +0 -1
- package/lib/typescript/specs/NitroSpeech.nitro.d.ts +0 -162
- package/lib/typescript/specs/NitroSpeech.nitro.d.ts.map +0 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/SpeechToTextParams.kt +0 -68
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 George Messier
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -5,39 +5,42 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@gmessier/nitro-speech)
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
> If you hit an issue, please open a GitHub issue or reach out to me on Discord / Twitter (X) — response is guaranteed.
|
|
8
|
+
> If you hit an issue or want to request a feature, please open a GitHub issue or reach out to me on Discord / Twitter (X) — response is guaranteed.
|
|
9
9
|
>
|
|
10
10
|
> - GitHub Issues: [NotGeorgeMessier/nitro-speech/issues](https://github.com/NotGeorgeMessier/nitro-speech/issues)
|
|
11
11
|
> - Discord: `gmessier`
|
|
12
12
|
> - Twitter (X): `SufferingGeorge`
|
|
13
13
|
|
|
14
|
-
React Native Real-Time Speech Recognition Library, powered by [Nitro Modules](https://github.com/mrousavy/nitro).
|
|
15
|
-
|
|
16
|
-
#### Compatibility:
|
|
17
|
-
‼️ Newest versions of `@gmessier/nitro-speech` requires [react-native-nitro-modules 0.35.0 or higher](https://github.com/mrousavy/nitro/releases/tag/v0.35.0).
|
|
18
|
-
|
|
19
|
-
| Compatibility | Supported versions |
|
|
20
|
-
|---|---|
|
|
21
|
-
| `react-native-nitro-modules <= 0.34.*` | `@gmessier/nitro-speech <= 0.2.*` |
|
|
22
|
-
| `react-native-nitro-modules >= 0.35.*` | `@gmessier/nitro-speech >= 0.3.*` |
|
|
23
|
-
|
|
24
14
|
#### Key Features:
|
|
25
15
|
|
|
26
|
-
- Built on Nitro Modules for
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
- Configurable
|
|
33
|
-
-
|
|
34
|
-
|
|
16
|
+
- ⚡ Built on Nitro Modules for zero-overhead native bridging
|
|
17
|
+
- 🌎 Supports 60+ languages
|
|
18
|
+
- 🍎 The only library that uses new `SpeechAnalyzer` with `SpeechTranscriber` or `DictationTranscriber` API for iOS 26+ (with fallback to legacy `SFSpeechRecognition` for older versions)
|
|
19
|
+
- ⏱️ Timer for silence
|
|
20
|
+
- Configurable `autoFinishRecognitionMs` value (default: 8 sec)
|
|
21
|
+
- Callback `onAutoFinishProgress` fires periodically with interval
|
|
22
|
+
- Configurable interval `autoFinishProgressIntervalMs` value (default: 1 sec)
|
|
23
|
+
- Method `updateConfig` with `autoFinishRecognitionMs` and `autoFinishProgressIntervalMs`
|
|
24
|
+
allows to change value on the fly
|
|
25
|
+
- Method `resetAutoFinishTime` resets the Timer to the threshold
|
|
26
|
+
- Method `addAutoFinishTime` adds ms once without changing threshold
|
|
27
|
+
- Configurable volume-based sensitivity `resetAutoFinishVoiceSensitivity` for the timer from 0 to 1
|
|
28
|
+
- 🎤 Rich user voice input management
|
|
29
|
+
- Hook `useVoiceInputVolume()` for `raw` or `smoothed` normalized
|
|
30
|
+
volume level from 0 to 1 -> easy to use for UI animations;
|
|
31
|
+
And `db` as human-friendly value
|
|
32
|
+
- Flexible callback `onVolumeChange` for custom behavior
|
|
33
|
+
- 🧩 Lifecycle methods: `prewarm` | `updateConfig` | `getIsActive` | `getSupportedLocalesIOS`
|
|
34
|
+
- 👆 Configurable Haptic Feedback on start and finish
|
|
35
|
+
- 🎚️ Speech-quality configurations:
|
|
35
36
|
- Result is grouped by speech segments into Batches.
|
|
36
|
-
- Param `
|
|
37
|
-
- Param `
|
|
38
|
-
-
|
|
37
|
+
- Param `iosPreset` - `shortForm` or `general` enables best transcriber for your situation
|
|
38
|
+
- Param `disableRepeatingFilter` - filters out consecutive duplicate words.
|
|
39
|
+
- Param `androidDisableBatchHandling` - disables empty partial results
|
|
40
|
+
- Many more, see `SpeechRecognitionConfig`
|
|
41
|
+
- 🔓 Embedded Permission handling
|
|
39
42
|
- Callback `onPermissionDenied` - if user denied the request
|
|
40
|
-
- Everything else that could be found in Expo or other libraries
|
|
43
|
+
- 📦 Everything else that could be found in Expo or other libraries
|
|
41
44
|
|
|
42
45
|
## Table of Contents
|
|
43
46
|
|
|
@@ -48,10 +51,11 @@ React Native Real-Time Speech Recognition Library, powered by [Nitro Modules](ht
|
|
|
48
51
|
- [Recommended: useRecognizer Hook](#recommended-userecognizer-hook)
|
|
49
52
|
- [With React Navigation (important)](#with-react-navigation-important)
|
|
50
53
|
- [Cross-component control: RecognizerRef](#cross-component-control-recognizerref)
|
|
54
|
+
- [Multithreading (react-native-worklets)](#multithreading-react-native-worklets)
|
|
51
55
|
- [Voice input volume](#voice-input-volume)
|
|
52
|
-
- [Unsafe:
|
|
53
|
-
- [API Reference](#api-reference)
|
|
56
|
+
- [Unsafe: SpeechRecognizer](#unsafe-speechrecognizer)
|
|
54
57
|
- [Requirements](#requirements)
|
|
58
|
+
- [Compatibility](#compatibility)
|
|
55
59
|
- [Troubleshooting](#troubleshooting)
|
|
56
60
|
|
|
57
61
|
## Installation
|
|
@@ -88,6 +92,7 @@ No additional setup required.
|
|
|
88
92
|
|
|
89
93
|
### Android
|
|
90
94
|
|
|
95
|
+
No actions required.
|
|
91
96
|
The library declares the required permission in its `AndroidManifest.xml` (merged automatically):
|
|
92
97
|
|
|
93
98
|
```xml
|
|
@@ -112,20 +117,31 @@ Both permissions are required for speech recognition to work on iOS.
|
|
|
112
117
|
|
|
113
118
|
| Feature | Description | iOS | Android |
|
|
114
119
|
|---------|-------------|-----|---------|
|
|
115
|
-
| **Real-time transcription** |
|
|
116
|
-
| **
|
|
117
|
-
| **Auto-finish
|
|
118
|
-
| **
|
|
119
|
-
| **
|
|
120
|
+
| **Real-time transcription** | Gets partial results as the user speaks | ✅ | ✅ |
|
|
121
|
+
| **Locale support** | 60+ Supported locales | ✅ | ✅ |
|
|
122
|
+
| **Auto-finish on silence** | Automatically stops recognition after configurable inactivity period | ✅ | ✅ |
|
|
123
|
+
| **Auto-finish progress** | Callback `onAutoFinishProgress` with countdown until auto-stop | ✅ | ✅ |
|
|
124
|
+
| **Add Auto-finish Time** | Adds time to the auto finish timer once without changing the timer threshold | ✅ | ✅ |
|
|
125
|
+
| **Reset Auto-finish Time** | Resets the Timer to the threshold | ✅ | ✅ |
|
|
126
|
+
| **Voice input volume** | Hook `useVoiceInputVolume` and `onVolumeChange` callback | ✅ | ✅ |
|
|
127
|
+
| **Reset Auto-finish Sensitivity** | The voice detector sensitivity to reset the Auto-finish time | ✅ | ✅ |
|
|
128
|
+
| **Prewarm** | Prepares resources, downloads assets, confirms locale availability | ✅ | ✅ |
|
|
129
|
+
| **Update config** | Static method `updateConfig` allows update config on the fly | ✅ | ✅ |
|
|
130
|
+
| **isActive** | Static method `getIsActive()` | ✅ | ✅ |
|
|
131
|
+
| **Haptic feedback** | Haptic feedback on recording start/stop | ✅ | ✅ |
|
|
120
132
|
| **Permission handling** | Dedicated `onPermissionDenied` callback | ✅ | ✅ |
|
|
121
|
-
| **
|
|
133
|
+
| **Background handling** | Stop when app loses focus/goes to background | ✅ | ✅ |
|
|
122
134
|
| **Repeating word filter** | Removes consecutive duplicate words from artifacts | ✅ | ✅ |
|
|
123
|
-
| **
|
|
135
|
+
| **Offensive word masking** | Control whether offensive words are masked with * | iOS 26+ | ✅ |
|
|
124
136
|
| **Contextual strings** | Domain-specific vocabulary for improved accuracy | ✅ | ✅ |
|
|
125
|
-
| **Automatic punctuation** | Adds punctuation to transcription (iOS 16+) | ✅ | Auto |
|
|
126
137
|
| **Language model selection** | Choose between web search vs free-form models | Auto | ✅ |
|
|
127
|
-
| **
|
|
138
|
+
| **Batch handling** | Filters out empty or repeated results | Auto | ✅ |
|
|
128
139
|
| **Formatting quality** | Prefer quality vs speed in formatting | Auto | ✅ |
|
|
140
|
+
| **Transcription preset** | `iosPreset` adapts for short phrases (`shortForm`) or `general` conversation | ✅ | Auto |
|
|
141
|
+
| **Automatic punctuation** | Adds punctuation to transcription (iOS 16+) | ✅ | Auto |
|
|
142
|
+
| **Atypical speech hint** | Hint iOS that speech may include accent, lisp, or other confounding traits | ✅ | Auto |
|
|
143
|
+
| **getSupportedLocalesIOS** | Supported locales for iOS (No available API for Android) | ✅ | X |
|
|
144
|
+
|
|
129
145
|
|
|
130
146
|
## Usage
|
|
131
147
|
|
|
@@ -141,8 +157,11 @@ function MyComponent() {
|
|
|
141
157
|
const {
|
|
142
158
|
startListening,
|
|
143
159
|
stopListening,
|
|
160
|
+
resetAutoFinishTime,
|
|
144
161
|
addAutoFinishTime,
|
|
145
|
-
|
|
162
|
+
updateConfig,
|
|
163
|
+
getSupportedLocalesIOS,
|
|
164
|
+
getIsActive,
|
|
146
165
|
} = useRecognizer({
|
|
147
166
|
onReadyForSpeech: () => {
|
|
148
167
|
console.log('Listening...');
|
|
@@ -167,18 +186,22 @@ function MyComponent() {
|
|
|
167
186
|
return (
|
|
168
187
|
<View>
|
|
169
188
|
<TouchableOpacity onPress={() => startListening({
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
autoFinishRecognitionMs: 8000,
|
|
173
|
-
|
|
189
|
+
// Universal
|
|
190
|
+
locale: "en-US",
|
|
174
191
|
contextualStrings: ['custom', 'words'],
|
|
175
|
-
|
|
192
|
+
maskOffensiveWords: false,
|
|
193
|
+
// Mutable properties
|
|
194
|
+
autoFinishRecognitionMs: 12000,
|
|
195
|
+
autoFinishProgressIntervalMs: 1000,
|
|
196
|
+
resetAutoFinishVoiceSensitivity: 0.4,
|
|
197
|
+
disableRepeatingFilter: false,
|
|
176
198
|
startHapticFeedbackStyle: 'medium',
|
|
177
199
|
stopHapticFeedbackStyle: 'light',
|
|
178
|
-
// iOS specific
|
|
200
|
+
// iOS specific, non-mutable
|
|
179
201
|
iosAddPunctuation: true,
|
|
180
|
-
|
|
181
|
-
|
|
202
|
+
iosPreset: 'general',
|
|
203
|
+
iosAtypicalSpeech: false,
|
|
204
|
+
// Android specific, non-mutable
|
|
182
205
|
androidFormattingPreferQuality: false,
|
|
183
206
|
androidUseWebSearchModel: false,
|
|
184
207
|
androidDisableBatchHandling: false,
|
|
@@ -191,22 +214,31 @@ function MyComponent() {
|
|
|
191
214
|
<TouchableOpacity onPress={() => addAutoFinishTime(5000)}>
|
|
192
215
|
<Text>Add 5s to Timer</Text>
|
|
193
216
|
</TouchableOpacity>
|
|
194
|
-
<TouchableOpacity onPress={() =>
|
|
195
|
-
<Text>
|
|
217
|
+
<TouchableOpacity onPress={() => resetAutoFinishTime()}>
|
|
218
|
+
<Text>Reset Timer</Text>
|
|
219
|
+
</TouchableOpacity>
|
|
220
|
+
<TouchableOpacity onPress={() => updateConfig(
|
|
221
|
+
{
|
|
222
|
+
autoFinishRecognitionMs: 12000,
|
|
223
|
+
autoFinishProgressIntervalMs: 500,
|
|
224
|
+
resetAutoFinishVoiceSensitivity: 0.65,
|
|
225
|
+
},
|
|
226
|
+
true
|
|
227
|
+
)>
|
|
228
|
+
<Text>Update Timer to 12s, 500ms interval, 0.65 sensitivity, with reset</Text>
|
|
196
229
|
</TouchableOpacity>
|
|
197
230
|
</View>
|
|
198
231
|
);
|
|
199
232
|
}
|
|
200
233
|
```
|
|
201
234
|
|
|
202
|
-
|
|
203
|
-
For other components, avoid creating another `useRecognizer` instance for the same session.
|
|
235
|
+
On iOS 26+, the recognizer prefers the newer `SpeechTranscriber` path for general cases. Setting `iosPreset: 'shortForm'`, `iosAddPunctuation: false`, or `iosAtypicalSpeech: true` switches priority to `DictationTranscriber` that is better suited for short utterances or non-standard speech patterns.
|
|
204
236
|
|
|
205
237
|
### With React Navigation (important)
|
|
206
238
|
|
|
207
239
|
React Navigation **doesn’t unmount screens** when you navigate — the screen can stay mounted in the background and come back without remounting. See: [Navigation lifecycle (React Navigation)](https://reactnavigation.org/docs/8.x/navigation-lifecycle/#summary).
|
|
208
240
|
|
|
209
|
-
Because of that, prefer tying recognition cleanup to **focus state**, not just component unmount. A simple approach is `useIsFocused()` and passing it into `useRecognizer`’s `destroyDeps` so recognition stops when the screen blurs. See: `
|
|
241
|
+
Because of that, prefer tying recognition cleanup to **focus state**, not just component unmount. A simple approach is `useIsFocused()` and passing it into `useRecognizer`’s `destroyDeps` so recognition stops when the screen blurs. See: [`useIsFocused` (React Navigation)](https://reactnavigation.org/docs/8.x/use-is-focused).
|
|
210
242
|
|
|
211
243
|
```typescript
|
|
212
244
|
const isFocused = useIsFocused();
|
|
@@ -229,26 +261,58 @@ import { RecognizerRef } from '@gmessier/nitro-speech';
|
|
|
229
261
|
|
|
230
262
|
RecognizerRef.startListening({ locale: 'en-US' });
|
|
231
263
|
RecognizerRef.addAutoFinishTime(5000);
|
|
232
|
-
RecognizerRef.
|
|
264
|
+
RecognizerRef.resetAutoFinishTime();
|
|
265
|
+
RecognizerRef.updateConfig(
|
|
266
|
+
{
|
|
267
|
+
autoFinishRecognitionMs: 12000,
|
|
268
|
+
autoFinishProgressIntervalMs: 500,
|
|
269
|
+
resetAutoFinishVoiceSensitivity: 0.65,
|
|
270
|
+
},
|
|
271
|
+
true
|
|
272
|
+
);
|
|
233
273
|
RecognizerRef.getIsActive();
|
|
234
274
|
RecognizerRef.stopListening();
|
|
275
|
+
// iOS only
|
|
276
|
+
RecognizerRef.getSupportedLocalesIOS();
|
|
235
277
|
```
|
|
236
278
|
|
|
237
279
|
`RecognizerRef` exposes only method handlers and is safe for cross-component method access.
|
|
238
280
|
|
|
281
|
+
### Multithreading (react-native-worklets)
|
|
282
|
+
All methods are thread-safe and can be called from UI thread or custom worklets
|
|
283
|
+
```typescript
|
|
284
|
+
import { createWorkletRuntime, scheduleOnRuntime } from 'react-native-worklets';
|
|
285
|
+
const workletRuntime = createWorkletRuntime({ name: 'background' });
|
|
286
|
+
|
|
287
|
+
onPress={() => {
|
|
288
|
+
scheduleOnRuntime(workletRuntime, () => {
|
|
289
|
+
// or SpeechRecognizer
|
|
290
|
+
// or just updateConfig from useRecognizer
|
|
291
|
+
RecognizerRef.updateConfig({
|
|
292
|
+
autoFinishRecognitionMs: 10000,
|
|
293
|
+
autoFinishProgressIntervalMs: 200,
|
|
294
|
+
resetAutoFinishVoiceSensitivity: 0.10,
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}}
|
|
298
|
+
```
|
|
299
|
+
|
|
239
300
|
### Voice input volume
|
|
240
301
|
|
|
241
302
|
#### useVoiceInputVolume
|
|
242
303
|
|
|
243
|
-
|
|
244
|
-
⚠️ **Technical limitation**: this approach re-renders component a lot.
|
|
304
|
+
⚠️ **Technical limitation**: this hook re-renders component a lot.
|
|
245
305
|
|
|
246
306
|
```typescript
|
|
247
307
|
import { useVoiceInputVolume } from '@gmessier/nitro-speech';
|
|
248
308
|
|
|
249
309
|
function VoiceMeter() {
|
|
250
|
-
const
|
|
251
|
-
return
|
|
310
|
+
const volumeEvent = useVoiceInputVolume();
|
|
311
|
+
return <>
|
|
312
|
+
<Text>{volumeEvent.smoothedVolume.toFixed(2)}</Text>
|
|
313
|
+
<Text>{volumeEvent.rawVolume.toFixed(2)}</Text>
|
|
314
|
+
<Text>{volumeEvent.db}</Text>
|
|
315
|
+
</>;
|
|
252
316
|
}
|
|
253
317
|
```
|
|
254
318
|
|
|
@@ -257,6 +321,8 @@ function VoiceMeter() {
|
|
|
257
321
|
As a better alternative you can control volume via SharedValue and apply it only on UI thread with Reanimated.
|
|
258
322
|
This way you will avoid re-renders since the volume will be stored on UI thread
|
|
259
323
|
|
|
324
|
+
Warning: this approach will disable the built-in `useVoiceInputVolume` hook.
|
|
325
|
+
|
|
260
326
|
```typescript
|
|
261
327
|
function VoiceMeter() {
|
|
262
328
|
const sharedVolume = useSharedValue(0)
|
|
@@ -265,9 +331,9 @@ function VoiceMeter() {
|
|
|
265
331
|
} = useRecognizer(
|
|
266
332
|
{
|
|
267
333
|
// ...
|
|
268
|
-
onVolumeChange: (
|
|
334
|
+
onVolumeChange: (volumeEvent) => {
|
|
269
335
|
"worklet";
|
|
270
|
-
sharedVolume.value =
|
|
336
|
+
sharedVolume.value = volumeEvent.smoothedVolume
|
|
271
337
|
},
|
|
272
338
|
// ...
|
|
273
339
|
}
|
|
@@ -276,154 +342,105 @@ function VoiceMeter() {
|
|
|
276
342
|
```
|
|
277
343
|
|
|
278
344
|
|
|
279
|
-
### Unsafe:
|
|
345
|
+
### Unsafe: SpeechRecognizer
|
|
280
346
|
|
|
281
|
-
`
|
|
347
|
+
`SpeechRecognizer` is the hybrid object. It gives direct access to callbacks and control methods, but it is unsafe to orchestrate the full session directly from it.
|
|
282
348
|
|
|
283
349
|
```typescript
|
|
284
|
-
import {
|
|
350
|
+
import { SpeechRecognizer, speechRecognizerVolumeChangeHandler } from '@gmessier/nitro-speech';
|
|
285
351
|
|
|
286
352
|
// Set up callbacks
|
|
287
|
-
|
|
353
|
+
SpeechRecognizer.onReadyForSpeech = () => {
|
|
288
354
|
console.log('Listening...');
|
|
289
355
|
};
|
|
290
356
|
|
|
291
|
-
|
|
357
|
+
SpeechRecognizer.onResult = (textBatches) => {
|
|
292
358
|
console.log('Result:', textBatches.join('\n'));
|
|
293
359
|
};
|
|
294
360
|
|
|
295
|
-
|
|
361
|
+
SpeechRecognizer.onRecordingStopped = () => {
|
|
296
362
|
console.log('Stopped');
|
|
297
363
|
};
|
|
298
364
|
|
|
299
|
-
|
|
365
|
+
SpeechRecognizer.onAutoFinishProgress = (timeLeftMs) => {
|
|
300
366
|
console.log('Auto-stop in:', timeLeftMs, 'ms');
|
|
301
367
|
};
|
|
302
368
|
|
|
303
|
-
|
|
369
|
+
SpeechRecognizer.onError = (error) => {
|
|
304
370
|
console.log('Error:', error);
|
|
305
371
|
};
|
|
306
372
|
|
|
307
|
-
|
|
373
|
+
SpeechRecognizer.onPermissionDenied = () => {
|
|
308
374
|
console.log('Permission denied');
|
|
309
375
|
};
|
|
310
376
|
|
|
311
|
-
|
|
377
|
+
SpeechRecognizer.onVolumeChange = (volume) => {
|
|
312
378
|
console.log('new volume: ', volume);
|
|
313
379
|
};
|
|
314
|
-
// OR use
|
|
315
|
-
|
|
380
|
+
// OR use speechRecognizerVolumeChangeHandler to enable useVoiceInputVolume hook manually
|
|
381
|
+
SpeechRecognizer.onVolumeChange = speechRecognizerVolumeChangeHandler
|
|
316
382
|
|
|
317
383
|
|
|
318
384
|
// Start listening
|
|
319
|
-
|
|
385
|
+
SpeechRecognizer.startListening({
|
|
320
386
|
locale: 'en-US',
|
|
321
387
|
});
|
|
322
388
|
|
|
323
389
|
// Stop listening
|
|
324
|
-
|
|
390
|
+
SpeechRecognizer.stopListening();
|
|
325
391
|
|
|
326
392
|
// Manually add time to auto finish timer
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
// Update
|
|
331
|
-
|
|
332
|
-
|
|
393
|
+
SpeechRecognizer.addAutoFinishTime(5000); // Add 5 seconds
|
|
394
|
+
SpeechRecognizer.addAutoFinishTime(); // Reset to original time
|
|
395
|
+
|
|
396
|
+
// Update config
|
|
397
|
+
SpeechRecognizer.updateConfig({
|
|
398
|
+
autoFinishRecognitionMs: 10000,
|
|
399
|
+
autoFinishProgressIntervalMs: 200,
|
|
400
|
+
resetAutoFinishVoiceSensitivity: 0.10,
|
|
401
|
+
}, true); // Set to 10 seconds, 200ms interval, 0.10 sensitivity, with reset
|
|
333
402
|
```
|
|
334
403
|
|
|
335
404
|
### ⚠️ About dispose()
|
|
336
405
|
|
|
337
|
-
The `
|
|
406
|
+
The `SpeechRecognizer.dispose()` method is **NOT SAFE** and should rarely be used. Hybrid Objects in Nitro are typically managed by the JS garbage collector automatically. Only call `dispose()` in performance-critical scenarios where you need to eagerly destroy objects.
|
|
338
407
|
|
|
339
408
|
**See:** [Nitro dispose() documentation](https://nitro.margelo.com/docs/hybrid-objects#dispose)
|
|
340
409
|
|
|
341
|
-
## API Reference
|
|
342
|
-
|
|
343
|
-
### `useRecognizer(callbacks, destroyDeps?)`
|
|
344
|
-
|
|
345
|
-
#### Usage notes
|
|
346
|
-
|
|
347
|
-
- Use `useRecognizer` once per session/screen as the session setup owner.
|
|
348
|
-
- Cleanup stops recognition, so mounting multiple instances can unexpectedly end an active session.
|
|
349
|
-
- For method access in non-owner components, use `RecognizerRef`.
|
|
350
|
-
|
|
351
|
-
#### Parameters
|
|
352
|
-
|
|
353
|
-
- `callbacks` (object):
|
|
354
|
-
- `onReadyForSpeech?: () => void` - Called when speech recognition starts
|
|
355
|
-
- `onResult?: (textBatches: string[]) => void` - Called every time when partial result is ready (array of text batches)
|
|
356
|
-
- `onRecordingStopped?: () => void` - Called when recording stops
|
|
357
|
-
- `onAutoFinishProgress?: (timeLeftMs: number) => void` - Called each second during auto-finish countdown
|
|
358
|
-
- `onError?: (message: string) => void` - Called when an error occurs
|
|
359
|
-
- `onPermissionDenied?: () => void` - Called if microphone permission is denied
|
|
360
|
-
- `destroyDeps` (array, optional) - Additional dependencies for the cleanup effect. When any of these change (or the component unmounts), recognition is stopped.
|
|
361
|
-
|
|
362
|
-
#### Returns
|
|
363
|
-
|
|
364
|
-
- `startListening(params: SpeechToTextParams)` - Start speech recognition with the given parameters
|
|
365
|
-
- `stopListening()` - Stop speech recognition
|
|
366
|
-
- `addAutoFinishTime(additionalTimeMs?: number)` - Add time to the auto-finish timer (or reset to original if no parameter)
|
|
367
|
-
- `updateAutoFinishTime(newTimeMs: number, withRefresh?: boolean)` - Update the auto-finish timer
|
|
368
|
-
- `getIsActive()` - Returns true if the speech recognition is active
|
|
369
|
-
|
|
370
|
-
### `RecognizerRef`
|
|
371
|
-
|
|
372
|
-
- `startListening(params: SpeechToTextParams)`
|
|
373
|
-
- `stopListening()`
|
|
374
|
-
- `addAutoFinishTime(additionalTimeMs?: number)`
|
|
375
|
-
- `updateAutoFinishTime(newTimeMs: number, withRefresh?: boolean)`
|
|
376
|
-
- `getIsActive()`
|
|
377
|
-
|
|
378
|
-
### `useVoiceInputVolume`
|
|
379
|
-
|
|
380
|
-
- `useVoiceInputVolume(): number`
|
|
381
|
-
|
|
382
|
-
### `RecognizerSession`
|
|
383
|
-
|
|
384
|
-
- Exposes callbacks (`onReadyForSpeech`, `onResult`, etc.) and control methods.
|
|
385
|
-
- Prefer `useRecognizer` (single owner) + `RecognizerRef` for app-level usage.
|
|
386
|
-
|
|
387
|
-
### `SpeechToTextParams`
|
|
388
|
-
|
|
389
|
-
Configuration object for speech recognition.
|
|
390
|
-
|
|
391
|
-
#### Common Parameters
|
|
392
|
-
|
|
393
|
-
- `locale?: string` - Language locale (default: `"en-US"`)
|
|
394
|
-
- `autoFinishRecognitionMs?: number` - Auto-stop timeout in milliseconds (default: `8000`)
|
|
395
|
-
- `contextualStrings?: string[]` - Array of domain-specific words for better recognition
|
|
396
|
-
- `disableRepeatingFilter?: boolean` - Disable filter that removes consecutive duplicate words (default: `false`)
|
|
397
|
-
- `startHapticFeedbackStyle?: 'light' | 'medium' | 'heavy' | 'none'` - Haptic feedback style when microphone starts recording (default: `"medium"`)
|
|
398
|
-
- `stopHapticFeedbackStyle?: 'light' | 'medium' | 'heavy' | 'none'` - Haptic feedback style when microphone stops recording (default: `"medium"`)
|
|
399
|
-
- `maskOffensiveWords?: boolean` - Mask offensive words with asterisks. (Android 13+, iOS 26+, default: `false`. iOS <26: always `false`)
|
|
400
|
-
|
|
401
|
-
#### iOS-Specific Parameters
|
|
402
|
-
|
|
403
|
-
- `iosAddPunctuation?: boolean` - Add punctuation to results (iOS 16+, default: `true`)
|
|
404
|
-
|
|
405
|
-
#### Android-Specific Parameters
|
|
406
|
-
|
|
407
|
-
- `androidFormattingPreferQuality?: boolean` - Prefer quality over latency (Android 13+, default: `false`)
|
|
408
|
-
- `androidUseWebSearchModel?: boolean` - Use web search language model instead of free-form (default: `false`)
|
|
409
|
-
- `androidDisableBatchHandling?: boolean` - Disable default batch handling (may add many empty batches, default: `false`)
|
|
410
|
-
|
|
411
410
|
## Requirements
|
|
412
411
|
|
|
413
412
|
- React Native >= 0.76
|
|
414
413
|
- New Arch Only
|
|
415
414
|
- react-native-nitro-modules
|
|
416
415
|
|
|
416
|
+
## Compatibility
|
|
417
|
+
|
|
418
|
+
Latest versions of `@gmessier/nitro-speech` requires [react-native-nitro-modules 0.35.0 or higher](https://github.com/mrousavy/nitro/releases/tag/v0.35.0).
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
| Compatibility | Supported versions |
|
|
422
|
+
| -------------------------------------- | --------------------------------- |
|
|
423
|
+
| `react-native-nitro-modules <= 0.34.*` | `@gmessier/nitro-speech <= 0.2.*` |
|
|
424
|
+
| `react-native-nitro-modules >= 0.35.*` | `@gmessier/nitro-speech >= 0.3.*` |
|
|
425
|
+
|
|
417
426
|
## Troubleshooting
|
|
418
427
|
|
|
419
428
|
### Android Gradle sync issues
|
|
420
429
|
|
|
421
|
-
If you're having issues with Android Gradle sync, try running the prebuild for the
|
|
430
|
+
If you're having issues with Android Gradle sync, try running the prebuild for the library, that causes the issue:
|
|
431
|
+
|
|
432
|
+
e.g. failed in `react-native-nitro-modules`:
|
|
422
433
|
|
|
423
434
|
```bash
|
|
424
435
|
cd android && ./gradlew :react-native-nitro-modules:preBuild
|
|
425
436
|
```
|
|
426
437
|
|
|
438
|
+
e.g. failed in `react-native-worklets`:
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
cd android && ./gradlew :react-native-worklets:preBuild
|
|
442
|
+
```
|
|
443
|
+
|
|
427
444
|
## License
|
|
428
445
|
|
|
429
446
|
MIT
|
package/android/build.gradle
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#include <jni.h>
|
|
2
|
+
#include <fbjni/fbjni.h>
|
|
2
3
|
#include "NitroSpeechOnLoad.hpp"
|
|
3
4
|
|
|
4
5
|
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
|
|
5
|
-
return
|
|
6
|
+
return facebook::jni::initialize(vm, []() {
|
|
7
|
+
margelo::nitro::nitrospeech::registerAllNatives();
|
|
8
|
+
});
|
|
6
9
|
}
|
|
10
|
+
|
|
@@ -3,37 +3,101 @@ package com.margelo.nitro.nitrospeech.recognizer
|
|
|
3
3
|
import android.os.Handler
|
|
4
4
|
import android.os.Looper
|
|
5
5
|
import android.util.Log
|
|
6
|
+
import kotlin.math.max
|
|
6
7
|
|
|
7
|
-
class AutoStopper
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
class AutoStopper(
|
|
9
|
+
silenceThresholdMs: Double?,
|
|
10
|
+
progressIntervalMs: Double?,
|
|
11
|
+
private val onProgress: (Double) -> Unit,
|
|
12
|
+
val onTimeout: () -> Unit,
|
|
10
13
|
) {
|
|
11
14
|
companion object {
|
|
12
15
|
private const val TAG = "HybridRecognizer"
|
|
16
|
+
private const val DEFAULT_SILENCE_THRESHOLD_MS = 8000.0
|
|
17
|
+
private const val DEFAULT_PROGRESS_INTERVAL_MS = 1000.0
|
|
18
|
+
private const val MIN_PROGRESS_INTERVAL_MS = 50.0
|
|
13
19
|
}
|
|
14
20
|
|
|
21
|
+
private var silenceThresholdMs: Double = clampMs(silenceThresholdMs ?: DEFAULT_SILENCE_THRESHOLD_MS)
|
|
22
|
+
private var progressIntervalMs: Double = clampMs(progressIntervalMs ?: DEFAULT_PROGRESS_INTERVAL_MS)
|
|
23
|
+
|
|
24
|
+
private var timeLeftMs: Double = this.silenceThresholdMs
|
|
15
25
|
private var isStopped = false
|
|
26
|
+
private var didTimeout = false
|
|
27
|
+
private var isTimerScheduled = false
|
|
28
|
+
|
|
16
29
|
private val handler = Handler(Looper.getMainLooper())
|
|
17
30
|
|
|
18
|
-
private val
|
|
19
|
-
if (isStopped) return@Runnable
|
|
20
|
-
Log.d(TAG, "forceStopRecording, ms: ${System.currentTimeMillis()}")
|
|
21
|
-
forceStopRecording()
|
|
22
|
-
}
|
|
31
|
+
private val tickRunnable = Runnable { tick() }
|
|
23
32
|
|
|
24
|
-
fun
|
|
25
|
-
Log.d(TAG, "
|
|
26
|
-
handler.removeCallbacks(
|
|
33
|
+
fun resetTimer() {
|
|
34
|
+
Log.d(TAG, "resetTimer | isStopped: $isStopped | ms: ${System.currentTimeMillis()}")
|
|
35
|
+
handler.removeCallbacks(tickRunnable)
|
|
36
|
+
isTimerScheduled = false
|
|
27
37
|
if (isStopped) return
|
|
28
|
-
|
|
38
|
+
didTimeout = false
|
|
39
|
+
timeLeftMs = silenceThresholdMs
|
|
40
|
+
if (timeLeftMs > 0) {
|
|
41
|
+
onProgress(timeLeftMs)
|
|
42
|
+
}
|
|
43
|
+
scheduleNextTickLocked()
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
fun stop() {
|
|
32
47
|
isStopped = true
|
|
33
|
-
handler.removeCallbacks(
|
|
48
|
+
handler.removeCallbacks(tickRunnable)
|
|
49
|
+
isTimerScheduled = false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fun updateSilenceThreshold(newThresholdMs: Double) {
|
|
53
|
+
silenceThresholdMs = clampMs(newThresholdMs)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fun addMsOnce(extraMs: Double) {
|
|
57
|
+
if (isStopped || !extraMs.isFinite()) return
|
|
58
|
+
Log.d(TAG, "addMsOnce | extraMs: $extraMs")
|
|
59
|
+
timeLeftMs += extraMs
|
|
60
|
+
didTimeout = false
|
|
61
|
+
if (timeLeftMs > 0 && isTimerScheduled) {
|
|
62
|
+
onProgress(timeLeftMs)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fun updateProgressInterval(newIntervalMs: Double) {
|
|
67
|
+
if (isStopped) return
|
|
68
|
+
Log.d(TAG, "updateProgressInterval | newIntervalMs: $newIntervalMs")
|
|
69
|
+
progressIntervalMs = clampMs(newIntervalMs)
|
|
70
|
+
if (isTimerScheduled) {
|
|
71
|
+
scheduleNextTickLocked()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private fun scheduleNextTickLocked() {
|
|
76
|
+
handler.removeCallbacks(tickRunnable)
|
|
77
|
+
val delayMs = progressIntervalMs.toLong().coerceAtLeast(MIN_PROGRESS_INTERVAL_MS.toLong())
|
|
78
|
+
handler.postDelayed(tickRunnable, delayMs)
|
|
79
|
+
isTimerScheduled = true
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private fun tick() {
|
|
83
|
+
if (isStopped || didTimeout) return
|
|
84
|
+
timeLeftMs -= progressIntervalMs
|
|
85
|
+
if (timeLeftMs > 0) {
|
|
86
|
+
Log.d(TAG, "onProgress | timeLeftMs: $timeLeftMs")
|
|
87
|
+
onProgress(timeLeftMs)
|
|
88
|
+
scheduleNextTickLocked()
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
timeLeftMs = 0.0
|
|
92
|
+
didTimeout = true
|
|
93
|
+
handler.removeCallbacks(tickRunnable)
|
|
94
|
+
isTimerScheduled = false
|
|
95
|
+
Log.d(TAG, "onTimeout | ms: ${System.currentTimeMillis()}")
|
|
96
|
+
onTimeout()
|
|
34
97
|
}
|
|
35
98
|
|
|
36
|
-
fun
|
|
37
|
-
|
|
99
|
+
private fun clampMs(value: Double): Double {
|
|
100
|
+
if (!value.isFinite()) return MIN_PROGRESS_INTERVAL_MS
|
|
101
|
+
return max(MIN_PROGRESS_INTERVAL_MS, value)
|
|
38
102
|
}
|
|
39
|
-
}
|
|
103
|
+
}
|