@gmessier/nitro-speech 0.3.3 → 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.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +165 -148
  3. package/android/build.gradle +0 -1
  4. package/android/src/main/cpp/cpp-adapter.cpp +5 -1
  5. package/android/src/main/java/com/margelo/nitro/nitrospeech/HybridNitroSpeech.kt +2 -0
  6. package/android/src/main/java/com/margelo/nitro/nitrospeech/recognizer/AutoStopper.kt +80 -16
  7. package/android/src/main/java/com/margelo/nitro/nitrospeech/recognizer/HybridRecognizer.kt +93 -20
  8. package/android/src/main/java/com/margelo/nitro/nitrospeech/recognizer/RecognitionListenerSession.kt +27 -15
  9. package/ios/{BufferUtil.swift → Audio/AudioBufferConverter.swift} +3 -34
  10. package/ios/Audio/AudioLevelTracker.swift +66 -0
  11. package/ios/Coordinator.swift +105 -0
  12. package/ios/Engines/AnalyzerEngine.swift +241 -0
  13. package/ios/Engines/DictationRuntime.swift +67 -0
  14. package/ios/Engines/RecognizerEngine.swift +312 -0
  15. package/ios/Engines/SFSpeechEngine.swift +119 -0
  16. package/ios/Engines/SpeechRuntime.swift +58 -0
  17. package/ios/Engines/TranscriberRuntimeProtocol.swift +21 -0
  18. package/ios/HybridNitroSpeech.swift +1 -10
  19. package/ios/HybridRecognizer.swift +135 -192
  20. package/ios/LocaleManager.swift +73 -0
  21. package/ios/{AppStateObserver.swift → Shared/AppStateObserver.swift} +1 -2
  22. package/ios/Shared/AutoStopper.swift +147 -0
  23. package/ios/Shared/HapticImpact.swift +24 -0
  24. package/ios/Shared/Log.swift +41 -0
  25. package/ios/Shared/Permissions.swift +59 -0
  26. package/ios/Shared/Utils.swift +58 -0
  27. package/lib/NitroSpeech.d.ts +2 -0
  28. package/lib/NitroSpeech.js +2 -0
  29. package/lib/Recognizer/RecognizerRef.d.ts +5 -0
  30. package/lib/Recognizer/RecognizerRef.js +13 -0
  31. package/lib/Recognizer/SpeechRecognizer.d.ts +8 -0
  32. package/lib/Recognizer/SpeechRecognizer.js +9 -0
  33. package/lib/Recognizer/methods.d.ts +8 -0
  34. package/lib/Recognizer/methods.js +29 -0
  35. package/lib/Recognizer/types.d.ts +6 -0
  36. package/lib/Recognizer/types.js +1 -0
  37. package/lib/Recognizer/useRecognizer.d.ts +16 -0
  38. package/lib/Recognizer/useRecognizer.js +71 -0
  39. package/lib/Recognizer/useVoiceInputVolume.d.ts +25 -0
  40. package/lib/Recognizer/useVoiceInputVolume.js +52 -0
  41. package/lib/index.d.ts +6 -0
  42. package/lib/index.js +6 -0
  43. package/lib/specs/NitroSpeech.nitro.d.ts +8 -0
  44. package/lib/specs/NitroSpeech.nitro.js +1 -0
  45. package/lib/specs/Recognizer.nitro.d.ts +95 -0
  46. package/lib/specs/Recognizer.nitro.js +1 -0
  47. package/lib/specs/SpeechRecognitionConfig.d.ts +162 -0
  48. package/lib/specs/SpeechRecognitionConfig.js +1 -0
  49. package/lib/specs/VolumeChangeEvent.d.ts +31 -0
  50. package/lib/specs/VolumeChangeEvent.js +1 -0
  51. package/nitro.json +0 -4
  52. package/nitrogen/generated/android/NitroSpeech+autolinking.cmake +2 -2
  53. package/nitrogen/generated/android/NitroSpeechOnLoad.cpp +4 -2
  54. package/nitrogen/generated/android/c++/JFunc_void_VolumeChangeEvent.hpp +78 -0
  55. package/nitrogen/generated/android/c++/JFunc_void_std__vector_std__string_.hpp +14 -14
  56. package/nitrogen/generated/android/c++/JHybridRecognizerSpec.cpp +68 -19
  57. package/nitrogen/generated/android/c++/JHybridRecognizerSpec.hpp +7 -4
  58. package/nitrogen/generated/android/c++/JIosPreset.hpp +58 -0
  59. package/nitrogen/generated/android/c++/JMutableSpeechRecognitionConfig.hpp +79 -0
  60. package/nitrogen/generated/android/c++/{JSpeechToTextParams.hpp → JSpeechRecognitionConfig.hpp} +48 -30
  61. package/nitrogen/generated/android/c++/JVolumeChangeEvent.hpp +65 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/Func_void_VolumeChangeEvent.kt +80 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/HybridRecognizerSpec.kt +18 -5
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/IosPreset.kt +23 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/MutableSpeechRecognitionConfig.kt +76 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/SpeechRecognitionConfig.kt +121 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/VolumeChangeEvent.kt +61 -0
  68. package/nitrogen/generated/ios/NitroSpeech-Swift-Cxx-Bridge.cpp +46 -30
  69. package/nitrogen/generated/ios/NitroSpeech-Swift-Cxx-Bridge.hpp +203 -70
  70. package/nitrogen/generated/ios/NitroSpeech-Swift-Cxx-Umbrella.hpp +13 -3
  71. package/nitrogen/generated/ios/c++/HybridRecognizerSpecSwift.hpp +41 -9
  72. package/nitrogen/generated/ios/swift/Func_void_VolumeChangeEvent.swift +46 -0
  73. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  74. package/nitrogen/generated/ios/swift/HybridRecognizerSpec.swift +6 -3
  75. package/nitrogen/generated/ios/swift/HybridRecognizerSpec_cxx.swift +66 -18
  76. package/nitrogen/generated/ios/swift/IosPreset.swift +40 -0
  77. package/nitrogen/generated/ios/swift/MutableSpeechRecognitionConfig.swift +118 -0
  78. package/nitrogen/generated/ios/swift/{SpeechToTextParams.swift → SpeechRecognitionConfig.swift} +108 -43
  79. package/nitrogen/generated/ios/swift/VolumeChangeEvent.swift +52 -0
  80. package/nitrogen/generated/shared/c++/HybridRecognizerSpec.cpp +4 -1
  81. package/nitrogen/generated/shared/c++/HybridRecognizerSpec.hpp +17 -7
  82. package/nitrogen/generated/shared/c++/IosPreset.hpp +76 -0
  83. package/nitrogen/generated/shared/c++/MutableSpeechRecognitionConfig.hpp +105 -0
  84. package/nitrogen/generated/shared/c++/{SpeechToTextParams.hpp → SpeechRecognitionConfig.hpp} +39 -20
  85. package/nitrogen/generated/shared/c++/VolumeChangeEvent.hpp +91 -0
  86. package/package.json +15 -16
  87. package/src/NitroSpeech.ts +5 -0
  88. package/src/Recognizer/RecognizerRef.ts +23 -0
  89. package/src/Recognizer/SpeechRecognizer.ts +10 -0
  90. package/src/Recognizer/methods.ts +40 -0
  91. package/src/Recognizer/types.ts +33 -0
  92. package/src/Recognizer/useRecognizer.ts +85 -0
  93. package/src/Recognizer/useVoiceInputVolume.ts +65 -0
  94. package/src/index.ts +6 -182
  95. package/src/specs/NitroSpeech.nitro.ts +2 -163
  96. package/src/specs/Recognizer.nitro.ts +110 -0
  97. package/src/specs/SpeechRecognitionConfig.ts +167 -0
  98. package/src/specs/VolumeChangeEvent.ts +31 -0
  99. package/android/proguard-rules.pro +0 -1
  100. package/ios/AnylyzerTranscriber.swift +0 -331
  101. package/ios/AutoStopper.swift +0 -69
  102. package/ios/HapticImpact.swift +0 -32
  103. package/ios/LegacySpeechRecognizer.swift +0 -161
  104. package/lib/commonjs/index.js +0 -145
  105. package/lib/commonjs/index.js.map +0 -1
  106. package/lib/commonjs/package.json +0 -1
  107. package/lib/commonjs/specs/NitroSpeech.nitro.js +0 -6
  108. package/lib/commonjs/specs/NitroSpeech.nitro.js.map +0 -1
  109. package/lib/module/index.js +0 -138
  110. package/lib/module/index.js.map +0 -1
  111. package/lib/module/package.json +0 -1
  112. package/lib/module/specs/NitroSpeech.nitro.js +0 -4
  113. package/lib/module/specs/NitroSpeech.nitro.js.map +0 -1
  114. package/lib/tsconfig.tsbuildinfo +0 -1
  115. package/lib/typescript/index.d.ts +0 -50
  116. package/lib/typescript/index.d.ts.map +0 -1
  117. package/lib/typescript/specs/NitroSpeech.nitro.d.ts +0 -162
  118. package/lib/typescript/specs/NitroSpeech.nitro.d.ts.map +0 -1
  119. 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
  [![npm downloads](https://img.shields.io/npm/dm/@gmessier/nitro-speech.svg)](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 low-overhead native bridging
27
- - Uses newest advanced `SpeechAnalyzer` and `SpeechTranscriber` API for iOS 26+ (with fallback to legacy `SFSpeechRecognition` for older versions)
28
- - Configurable Timer for silence (default: 8 sec)
29
- - Callback `onAutoFinishProgress` for progress bars, etc...
30
- - Method `addAutoFinishTime` for single timer update
31
- - Method `updateAutoFinishTime` for constant timer update
32
- - Configurable Haptic Feedback on start and finish
33
- - Flexible `onVolumeChange` to display input volume in UI with built-in `useVoiceInputVolume` hook
34
- - Speech-quality configurations:
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 `disableRepeatingFilter` for consecutive duplicate-word filtering.
37
- - Param `androidDisableBatchHandling` for removing empty recognition result.
38
- - Embedded Permission handling
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: RecognizerSession](#unsafe-recognizersession)
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** | Get partial results as the user speaks, enabling live UI updates | ✅ | ✅ |
116
- | **Auto-stop on silence** | Automatically stops recognition after configurable inactivity period (default: 8s) | ✅ | ✅ |
117
- | **Auto-finish progress** | Progress callbacks showing countdown until auto-stop | ✅ | *(TODO)* |
118
- | **Haptic feedback** | Optional haptics on recording start/stop | ✅ | ✅ |
119
- | **Background handling** | Auto-stop when app loses focus/goes to background | ✅ | Not Safe *(TODO)* |
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
- | **Voice input volume** | Normalized voice input level for UI meters (`useVoiceInputVolume`) | ✅ | ✅ |
133
+ | **Background handling** | Stop when app loses focus/goes to background | ✅ | ✅ |
122
134
  | **Repeating word filter** | Removes consecutive duplicate words from artifacts | ✅ | ✅ |
123
- | **Locale support** | Configure speech recognizer for different languages | | ✅ |
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
- | **Offensive word masking** | Control whether offensive words are masked | Auto | ✅ |
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
- updateAutoFinishTime
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
- locale: 'en-US',
171
- disableRepeatingFilter: false,
172
- autoFinishRecognitionMs: 8000,
173
-
189
+ // Universal
190
+ locale: "en-US",
174
191
  contextualStrings: ['custom', 'words'],
175
- // Haptics (both platforms)
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
- // Android specific
181
- maskOffensiveWords: false,
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={() => updateAutoFinishTime(10000)}>
195
- <Text>Update Timer to 10s</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
- Use the handlers returned by this single hook instance inside that owner component.
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: `[useIsFocused` (React Navigation)](https://reactnavigation.org/docs/8.x/use-is-focused).
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.updateAutoFinishTime(10000, true);
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
- By default you have access to `useVoiceInputVolume` to read normalized voice input level (`0..1`) for UI meters.
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 volume = useVoiceInputVolume();
251
- return <Text>{volume.toFixed(2)}</Text>;
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: (normVolume) => {
334
+ onVolumeChange: (volumeEvent) => {
269
335
  "worklet";
270
- sharedVolume.value = normValue
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: RecognizerSession
345
+ ### Unsafe: SpeechRecognizer
280
346
 
281
- `RecognizerSession` 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.
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 { RecognizerSession, unsafe_onVolumeChange } from '@gmessier/nitro-speech';
350
+ import { SpeechRecognizer, speechRecognizerVolumeChangeHandler } from '@gmessier/nitro-speech';
285
351
 
286
352
  // Set up callbacks
287
- RecognizerSession.onReadyForSpeech = () => {
353
+ SpeechRecognizer.onReadyForSpeech = () => {
288
354
  console.log('Listening...');
289
355
  };
290
356
 
291
- RecognizerSession.onResult = (textBatches) => {
357
+ SpeechRecognizer.onResult = (textBatches) => {
292
358
  console.log('Result:', textBatches.join('\n'));
293
359
  };
294
360
 
295
- RecognizerSession.onRecordingStopped = () => {
361
+ SpeechRecognizer.onRecordingStopped = () => {
296
362
  console.log('Stopped');
297
363
  };
298
364
 
299
- RecognizerSession.onAutoFinishProgress = (timeLeftMs) => {
365
+ SpeechRecognizer.onAutoFinishProgress = (timeLeftMs) => {
300
366
  console.log('Auto-stop in:', timeLeftMs, 'ms');
301
367
  };
302
368
 
303
- RecognizerSession.onError = (error) => {
369
+ SpeechRecognizer.onError = (error) => {
304
370
  console.log('Error:', error);
305
371
  };
306
372
 
307
- RecognizerSession.onPermissionDenied = () => {
373
+ SpeechRecognizer.onPermissionDenied = () => {
308
374
  console.log('Permission denied');
309
375
  };
310
376
 
311
- RecognizerSession.onVolumeChange = (volume) => {
377
+ SpeechRecognizer.onVolumeChange = (volume) => {
312
378
  console.log('new volume: ', volume);
313
379
  };
314
- // OR use unsafe_onVolumeChange to enable useVoiceInputVolume hook manually
315
- RecognizerSession.onVolumeChange = unsafe_onVolumeChange
380
+ // OR use speechRecognizerVolumeChangeHandler to enable useVoiceInputVolume hook manually
381
+ SpeechRecognizer.onVolumeChange = speechRecognizerVolumeChangeHandler
316
382
 
317
383
 
318
384
  // Start listening
319
- RecognizerSession.startListening({
385
+ SpeechRecognizer.startListening({
320
386
  locale: 'en-US',
321
387
  });
322
388
 
323
389
  // Stop listening
324
- RecognizerSession.stopListening();
390
+ SpeechRecognizer.stopListening();
325
391
 
326
392
  // Manually add time to auto finish timer
327
- RecognizerSession.addAutoFinishTime(5000); // Add 5 seconds
328
- RecognizerSession.addAutoFinishTime(); // Reset to original time
329
-
330
- // Update auto finish time
331
- RecognizerSession.updateAutoFinishTime(10000); // Set to 10 seconds
332
- RecognizerSession.updateAutoFinishTime(10000, true); // Set to 10 seconds and refresh progress
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 `RecognizerSession.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.
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 core Nitro library:
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
@@ -63,7 +63,6 @@ android {
63
63
  }
64
64
  }
65
65
 
66
- consumerProguardFiles 'proguard-rules.pro'
67
66
  }
68
67
 
69
68
  externalNativeBuild {
@@ -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 margelo::nitro::nitrospeech::initialize(vm);
6
+ return facebook::jni::initialize(vm, []() {
7
+ margelo::nitro::nitrospeech::registerAllNatives();
8
+ });
6
9
  }
10
+
@@ -4,6 +4,8 @@ import androidx.annotation.Keep
4
4
  import com.facebook.proguard.annotations.DoNotStrip
5
5
  import com.margelo.nitro.nitrospeech.recognizer.HybridRecognizer
6
6
 
7
+ @DoNotStrip
8
+ @Keep
7
9
  class HybridNitroSpeech: HybridNitroSpeechSpec() {
8
10
 
9
11
  @DoNotStrip
@@ -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
- private var silenceThreshold: Long,
9
- val forceStopRecording: () -> Unit,
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 autoStopRecording = Runnable {
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 indicateRecordingActivity() {
25
- Log.d(TAG, "indicateRecordingActivity | isStopped: $isStopped | ms: ${System.currentTimeMillis()}")
26
- handler.removeCallbacks(autoStopRecording)
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
- handler.postDelayed(autoStopRecording, silenceThreshold)
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(autoStopRecording)
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 updateSilenceThreshold(newThreshold: Long) {
37
- silenceThreshold = newThreshold
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
+ }