react-native-nitro-onnx 0.1.1 β†’ 0.1.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.
Files changed (40) hide show
  1. package/README.md +101 -31
  2. package/android/CMakeLists.txt +5 -3
  3. package/android/src/main/AndroidManifest.xml +0 -2
  4. package/android/src/main/cpp/cpp-adapter.cpp +2 -2
  5. package/android/src/main/java/com/margelo/nitro/onnx/speech/OnnxSpeechPackage.kt +24 -15
  6. package/cpp/AsrEngine.cpp +150 -58
  7. package/cpp/AsrEngine.hpp +12 -7
  8. package/cpp/AudioFileReader.cpp +4 -0
  9. package/cpp/ModelSingleton.hpp +14 -0
  10. package/cpp/NitroOnnxSpeech.cpp +8 -9
  11. package/cpp/NitroOnnxSpeech.hpp +0 -6
  12. package/cpp/OfflineAsr.cpp +9 -12
  13. package/cpp/OfflineAsr.hpp +1 -1
  14. package/cpp/ResourceDir.cpp +5 -5
  15. package/cpp/ResourceDir.hpp +5 -4
  16. package/cpp/SpeakerEngine.cpp +38 -28
  17. package/cpp/SpeakerEngine.hpp +14 -13
  18. package/cpp/SpeakerManager.cpp +16 -14
  19. package/cpp/SpeakerManager.hpp +2 -1
  20. package/cpp/SpeakerRecord.cpp +125 -0
  21. package/cpp/SpeakerRecord.hpp +42 -0
  22. package/cpp/StreamingAsr.cpp +13 -10
  23. package/cpp/StreamingAsr.hpp +1 -1
  24. package/cpp/Tts.cpp +38 -10
  25. package/cpp/Tts.hpp +2 -1
  26. package/cpp/TtsEngine.cpp +15 -7
  27. package/cpp/TtsEngine.hpp +18 -5
  28. package/cpp/Vad.cpp +11 -11
  29. package/cpp/Vad.hpp +1 -1
  30. package/cpp/VadEngine.cpp +26 -33
  31. package/cpp/VadEngine.hpp +8 -10
  32. package/cpp/Version.hpp +7 -0
  33. package/ios/OnnxSpeechInitializer.mm +18 -6
  34. package/lib/specs/OnnxSpeech.nitro.d.ts +43 -6
  35. package/lib/specs/OnnxSpeech.nitro.d.ts.map +1 -1
  36. package/package.json +4 -3
  37. package/scripts/generate-version.js +22 -0
  38. package/src/specs/OnnxSpeech.nitro.ts +25 -8
  39. package/cpp/ThreadPool.cpp +0 -41
  40. package/cpp/ThreadPool.hpp +0 -62
package/README.md CHANGED
@@ -9,7 +9,22 @@ A React Native [Nitro Module](https://nitro.margelo.com) that wraps [sherpa-onnx
9
9
 
10
10
  All audio I/O uses zero-copy `ArrayBuffer` with **16 kHz mono f32 PCM**.
11
11
 
12
- > **Note:** This repository is a structural scaffold. Every model family has a typed config slot and a singleton-backed engine. Only one model per category is fully wired in the reference implementation; the remaining model types map to the correct sherpa-onnx C API fields and are ready for incremental completion.
12
+ > **Note:** Every ASR / TTS model type is wired to the corresponding sherpa-onnx C API config segment. Coverage is complete at the binding layer; per-model quality still depends on the downloaded sherpa-onnx model files.
13
+
14
+ > πŸ“’ **Important note about scope & package name**
15
+ >
16
+ > At the present time, this binding is built exclusively for **speech‑related workloads via sherpa‑onnx**:
17
+ > ASR, TTS, VAD and speaker embedding only.
18
+ > It is **NOT a general‑purpose ONNX Runtime binding** for arbitrary ONNX models (YOLO, LLM etc).
19
+ >
20
+ > The package name `react-native-nitro‑onnx` may appear to imply general‑purpose ONNX support,
21
+ > but that is not the current goal of this repository.
22
+ >
23
+ > If you are an open‑source developer and would like to take over this npm package name
24
+ > to build a truly general‑purpose Nitro ONNX binding supporting LLM / CV workloads,
25
+ > feel free to open a GitHub issue to contact me for discussion about npm ownership transfer.
26
+ >
27
+ > For now this repo will continue focusing on the speech‑only sherpa‑onnx use‑case.
13
28
 
14
29
  ## Table of Contents
15
30
 
@@ -34,12 +49,14 @@ All audio I/O uses zero-copy `ArrayBuffer` with **16 kHz mono f32 PCM**.
34
49
  ```
35
50
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
36
51
  β”‚ JS / TS β”‚
37
- β”‚ getOnnxSpeech() β†’ createVad() / createTts() / createAsr() β”‚
52
+ β”‚ getOnnxSpeech() β†’ createVad() / createTts() / β”‚
53
+ β”‚ createOfflineAsr() / createStreamingAsr() β”‚
38
54
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
39
55
  β”‚ react-native-nitro-modules (zero-copy ArrayBuffer)
40
56
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
41
57
  β”‚ C++ β”‚
42
- β”‚ OnnxSpeechImpl β†’ VadEngine / AsrEngine / TtsEngine / ... β”‚
58
+ β”‚ NitroOnnxSpeech β†’ Vad / OfflineAsr / StreamingAsr / Tts / β”‚
59
+ β”‚ SpeakerManager β”‚
43
60
  β”‚ ModelSingleton caches heavy recognizer / TTS instances β”‚
44
61
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
45
62
  β”‚ sherpa-onnx C API
@@ -50,10 +67,11 @@ All audio I/O uses zero-copy `ArrayBuffer` with **16 kHz mono f32 PCM**.
50
67
 
51
68
  Key design decisions:
52
69
 
53
- - **Singleton preloading:** `OfflineAsrEngine`, `StreamingAsrEngine`, `TtsEngine` and `SpeakerEngine` use `ModelSingleton` keyed by model directory and type. Loading the same model twice returns the same native instance.
54
- - **Background inference:** Every heavy operation runs on a fixed `ThreadPool` so the JS thread never blocks.
70
+ - **Singleton preloading:** `OfflineAsrEngine`, `StreamingAsrEngine`, `TtsEngine` and `SpeakerEngine` use `ModelSingleton`. The cache key covers model dir, type, provider, thread count and other identity-relevant options, so loading the same configuration twice returns the same native instance while a changed option creates a new one.
71
+ - **Background inference:** Every heavy operation runs on a background task pool provided by `react-native-nitro-modules` (`Promise::async`) so the JS thread never blocks. VAD additionally uses a dedicated processor thread for streaming segmentation.
55
72
  - **External model download:** The module does not bundle an internal downloader. Download model files in the background with a library such as [`@kesha-antonov/react-native-background-downloader`](https://github.com/kesha-antonov/react-native-background-downloader), then pass the local file paths to `load()` / `initialize()`.
56
73
  - **Zero-copy audio:** `ArrayBuffer` is the only audio transport format; samples are expected to be 16 kHz mono little-endian f32 PCM.
74
+ - **Storage:** Bundled assets (e.g. `silero_vad.onnx`) live in the platform resource dir. Registered speakers are written under the app document dir (`Application Support` on iOS, excluded from iCloud backup; `filesDir` on Android) at `<documentDir>/speakers/`.
57
75
 
58
76
  ## Supported Models
59
77
 
@@ -68,9 +86,9 @@ Key design decisions:
68
86
  | Conformer | `conformer` | attention-convolution | Streaming accuracy | Yes | Transducer triple |
69
87
  | Wenet | `wenet` | U2++ / CTC | Chinese industrial | No | Single `model.onnx` |
70
88
  | Telespeech | `telespeech` | telephony ASR | 8 kHz telco audio | No | Single `model.onnx` |
71
- | Moonshine | `moonshine` | lightweight | Edge devices | No | Single `model.onnx` |
89
+ | Moonshine | `moonshine` | lightweight encoder-decoder | Edge devices | No | `preprocessor.onnx` + `encoder.onnx` + decoder pair (see below) |
72
90
  | Dolphin | `dolphin` | CTC | English | No | Single `model.onnx` |
73
- | NeMo | `nemo` | CTC / RNNT | NVIDIA NeMo exported models | No | `model.onnx` + config |
91
+ | NeMo | `nemo` | CTC | NVIDIA NeMo exported models | No | `model.onnx` + tokens |
74
92
  | SenseVoice | `sense_voice` | multilingual | Alibaba SenseVoice | No | Single `model.onnx` |
75
93
 
76
94
  ### TTS
@@ -80,8 +98,8 @@ Key design decisions:
80
98
  | Kokoro | `kokoro` | internal | High, multi-speaker | Medium | Needs `model.onnx`, `voices.bin`, `tokens.txt`, `lexicon.txt` |
81
99
  | VITS | `vits` | internal | High quality | Medium | Needs `model.onnx`, `tokens.txt`, optional lexicon |
82
100
  | Matcha | `matcha` | external (e.g. Hifigan) | Fast, natural | Fast | Needs acoustic model + vocoder ONNX |
83
- | Pocket | `pocket` | internal | Lightweight zero-shot | Very fast | Needs `model.onnx` + config JSON |
84
- | ZipVoice | `zipvoice` | internal | Placeholder type | - | Maps to VITS-like config until sherpa-onnx exposes dedicated ZipVoice support |
101
+ | Pocket | `pocket` | internal | Lightweight zero-shot | Very fast | Multi-file (lm / encoder / decoder, see below) |
102
+ | ZipVoice | `zipvoice` | internal | Dedicated encoder/decoder | Fast | Needs `zipvoiceEncoder`, `zipvoiceDecoder`, vocoder, tokens |
85
103
 
86
104
  ### VAD
87
105
 
@@ -119,7 +137,7 @@ transducer/
119
137
  tokens.txt
120
138
  ```
121
139
 
122
- ### Paraformer / Wenet / Telespeech / Moonshine / Dolphin / SenseVoice (offline ASR)
140
+ ### Paraformer / Telespeech / Dolphin / SenseVoice (offline ASR)
123
141
 
124
142
  ```
125
143
  model/
@@ -127,12 +145,32 @@ model/
127
145
  tokens.txt
128
146
  ```
129
147
 
148
+ ### Wenet (offline ASR)
149
+
150
+ ```
151
+ wenet/
152
+ model.onnx
153
+ tokens.txt
154
+ ```
155
+
156
+ ### Moonshine (offline ASR)
157
+
158
+ ```
159
+ moonshine/
160
+ preprocessor.onnx β†’ model
161
+ encoder.onnx β†’ encoder
162
+ uncached_decoder.onnx β†’ decoder
163
+ cached_decoder.onnx β†’ joiner
164
+ tokens.txt
165
+ ```
166
+
167
+ Alternatively, pass `merged_decoder.onnx` as `decoder` and leave `joiner` unset.
168
+
130
169
  ### NeMo (offline ASR)
131
170
 
132
171
  ```
133
172
  nemo/
134
173
  model.onnx
135
- config.yaml
136
174
  tokens.txt
137
175
  ```
138
176
 
@@ -169,8 +207,27 @@ matcha/
169
207
 
170
208
  ```
171
209
  pocket/
172
- model.onnx
173
- config.json
210
+ lm_main.onnx
211
+ lm_flow.onnx
212
+ encoder.onnx
213
+ decoder.onnx
214
+ text_conditioner.onnx
215
+ vocab.json
216
+ token_scores.json
217
+ ```
218
+
219
+ Map these to `lmMain`, `lmFlow`, `pocketEncoder`, `pocketDecoder`, `textConditioner`, `vocabJson`, `tokenScoresJson`.
220
+
221
+ ### ZipVoice (TTS)
222
+
223
+ ```
224
+ zipvoice/
225
+ encoder.onnx β†’ zipvoiceEncoder
226
+ decoder.onnx β†’ zipvoiceDecoder
227
+ vocoder.onnx β†’ vocoder
228
+ tokens.txt
229
+ lexicon.txt
230
+ espeak-ng-data/ (optional)
174
231
  ```
175
232
 
176
233
  ### Silero VAD
@@ -197,7 +254,7 @@ Build requirements:
197
254
  - React Native >= 0.78
198
255
  - react-native-nitro-modules >= 0.35.8
199
256
  - Xcode 15 / Android NDK 26
200
- - The `prepare-sherpa-onnx.js` postinstall script downloads the sherpa-onnx
257
+ - The `postinstall` script runs `generate-version.js` (keeps `cpp/Version.hpp` in sync with `package.json`) and `prepare-sherpa-onnx.js`, which downloads the sherpa-onnx
201
258
  prebuilt tree (host static libraries, Android shared libraries, iOS
202
259
  xcframework, and C API headers) into `cpp/sherpa-onnx-prebuilt`.
203
260
 
@@ -306,10 +363,11 @@ await tts.saveWav(audio, "/path/to/output.wav");
306
363
 
307
364
  ## Execution Providers
308
365
 
309
- By default, the module automatically selects the best execution provider for your platform:
366
+ By default, the module selects a platform-appropriate execution provider:
310
367
 
311
- - **Android:** `qnn` β€” uses Qualcomm NPU via QNN; unsupported operators fall back to NNAPI/CPU.
312
- - **iOS:** `coreml` β€” uses Apple Neural Engine via CoreML; unsupported operators fall back to CPU.
368
+ - **Android (no QNN SDK):** `nnapi` β€” NNAPI with CPU fallback.
369
+ - **Android (built with `QNN_ROOT`):** `qnn` β€” Qualcomm HTP via QNN; unsupported operators fall back to CPU.
370
+ - **iOS:** `coreml` β€” Apple Neural Engine via CoreML; unsupported operators fall back to CPU.
313
371
 
314
372
  To disable NPU acceleration and force CPU-only inference, pass `provider: "cpu"` explicitly:
315
373
 
@@ -333,7 +391,7 @@ const soc = speech.getQualcommSoc();
333
391
 
334
392
  if (soc) {
335
393
  console.log(`Qualcomm SoC: ${soc}`);
336
- // QNN is the default provider on Android β€” no need to specify it
394
+ // Use QNN when the app was built with -DQNN_ROOT=...; otherwise NNAPI.
337
395
  await asr.load({ type: "whisper", /* ... */ });
338
396
  } else {
339
397
  await asr.load({ type: "whisper", /* ... */ provider: "cpu" });
@@ -344,15 +402,15 @@ if (soc) {
344
402
 
345
403
  ### Building with QNN Support
346
404
 
347
- QNN is enabled by default on Android (the prebuilt sherpa-onnx libraries include QNN support). You do **not** need `QNN_ROOT` for normal usage.
405
+ QNN is **opt-in**. Without `QNN_ROOT`, Android builds do not define `SHERPA_ONNX_ENABLE_QNN`, the default provider is `nnapi`, and no QNN runtime libraries are linked.
348
406
 
349
- `QNN_ROOT` is **only** required when you need to bundle additional QNN Binary backend libraries from the Qualcomm AI Runtime (QAIRT) SDK. If you don't need the binary backend, simply omit `QNN_ROOT` β€” the default QNN execution provider works out of the box.
407
+ `QNN_ROOT` is required to enable the QNN execution provider and bundle QNN Binary backend libraries from the Qualcomm AI Runtime (QAIRT) SDK.
350
408
 
351
- **Download QAIRT SDK (only if you need QNN Binary):**
409
+ **Download QAIRT SDK:**
352
410
 
353
411
  Visit [Qualcomm Software Center](https://softwarecenter.qualcomm.com/api/download/software/sdks/Qualcomm_AI_Runtime_Community/All/2.40.0.251030/v2.40.0.251030.zip) to download the SDK (v2.40.0).
354
412
 
355
- **Specify QNN_ROOT (optional):**
413
+ **Specify QNN_ROOT:**
356
414
 
357
415
  ```bash
358
416
  # Via environment variable
@@ -362,7 +420,7 @@ QNN_ROOT=/path/to/qnn/sdk ./gradlew assembleRelease
362
420
  QNN_ROOT=/path/to/qnn/sdk
363
421
  ```
364
422
 
365
- When `QNN_ROOT` is set, the build system will link the QNN core library (`QnnHtp`) and all available HTP version libraries (`QnnHtpV73Stub`/`HtpV73`, `QnnHtpV75Stub`/`HtpV75`, etc.) from the SDK.
423
+ When `QNN_ROOT` is set, the build defines `SHERPA_ONNX_ENABLE_QNN` (so the default provider becomes `qnn`) and links the QNN core library (`QnnHtp`) plus all available HTP version libraries (`QnnHtpV73Stub`/`HtpV73`, `QnnHtpV75Stub`/`HtpV75`, etc.) from the SDK.
366
424
 
367
425
  > **Note:** QNN support is Android-only. On iOS, CoreML is used by default.
368
426
 
@@ -382,27 +440,37 @@ The result is that no speech frames are lost between detection and JS delivery.
382
440
 
383
441
  ## Voice Cloning
384
442
 
443
+ Registered speakers are stored under the platform document directory (`Application Support` on iOS β€” excluded from iCloud backup β€” and `filesDir` on Android) as `<documentDir>/speakers/<id>.bin`. The record holds the embedding and, for `registerSpeakerFromFile`, the reference audio used by zero-shot TTS.
444
+
385
445
  Two voice-cloning paths are exposed:
386
446
 
387
- 1. **Speaker embedding registration** - compute an embedding from reference audio, store it locally, and pass the speaker ID to TTS models that accept a speaker index (e.g. Kokoro multi-speaker, VITS multi-speaker).
388
- 2. **Reference-audio TTS** - models that support prompt-based or zero-shot synthesis (e.g. Pocket) receive the reference embedding directly during synthesis.
447
+ 1. **Speaker embedding registration** β€” compute an embedding from reference audio, store it locally. Embeddings are kept for identity / search; they are not a model speaker index.
448
+ 2. **Reference-audio TTS** β€” models that support prompt-based or zero-shot synthesis (e.g. Pocket) receive stored reference audio during synthesis. Register with `registerSpeakerFromFile` so the reference audio is kept alongside the embedding.
449
+
450
+ `synthesizeWithSpeaker` accepts **two kinds of `speakerId`**:
451
+
452
+ | `speakerId` | Meaning | Typical models |
453
+ |---|---|---|
454
+ | Numeric string, e.g. `"0"` | Model-internal speaker index | Kokoro / VITS multi-speaker |
455
+ | Registered ID, e.g. `"speaker-1"` | Voice-cloning record (needs reference audio) | Pocket (zero-shot) |
389
456
 
390
457
  ```typescript
391
458
  const speaker = speech.createSpeakerManager();
392
459
  await speaker.load({ modelDir: "/path/to/speaker", model: "model.onnx", numThreads: 4 });
393
460
 
394
- const embedding = await speaker.computeEmbedding(referenceAudio);
395
- const registered = await speaker.registerSpeaker("speaker-1", "Alice", embedding);
396
-
397
- // Use the registered speaker with TTS (optional speed override)
461
+ // Zero-shot clone (Pocket): keep the reference audio for synthesis
462
+ const registered = await speaker.registerSpeakerFromFile("speaker-1", "Alice", "/path/to/alice.wav");
398
463
  const cloned = await tts.synthesizeWithSpeaker("Hello, I am Alice.", registered.id, 1.1);
464
+
465
+ // Multi-speaker model index (Kokoro / VITS)
466
+ const voice0 = await tts.synthesizeWithSpeaker("Hello there.", "0");
399
467
  ```
400
468
 
401
469
  ## Threading
402
470
 
403
- Every native inference task runs on a background thread pool:
471
+ Every native inference task runs on a background thread pool (`Promise::async` from react-native-nitro-modules):
404
472
 
405
- - VAD processing
473
+ - VAD processing (plus a dedicated processor thread for streaming segmentation)
406
474
  - Offline / streaming ASR decode
407
475
  - TTS synthesis
408
476
  - Speaker embedding extraction
@@ -427,6 +495,8 @@ The C++ test suite covers:
427
495
 
428
496
  - Audio sample / millisecond conversions
429
497
  - Float vector / byte buffer round-trip
498
+ - Speaker record write/read round-trip (embedding + reference audio)
499
+ - `tryParseSpeakerIndex` edge cases (empty, negative, non-numeric, oversized)
430
500
 
431
501
  ## License
432
502
 
@@ -14,8 +14,8 @@ if(BUILD_ONNX_SPEECH_TESTS)
14
14
  enable_testing()
15
15
  add_executable(NitroOnnxSpeechTests
16
16
  ../cpp/tests/test_core.cpp
17
- ../cpp/ThreadPool.cpp
18
17
  ../cpp/AudioUtils.cpp
18
+ ../cpp/SpeakerRecord.cpp
19
19
  )
20
20
  target_include_directories(NitroOnnxSpeechTests PRIVATE ../cpp)
21
21
  find_package(Threads REQUIRED)
@@ -163,6 +163,8 @@ if(QNN_ROOT)
163
163
  endif()
164
164
  endforeach()
165
165
  else()
166
- message(NOTICE "πŸ”§ QNN execution provider enabled (without external SDK)")
167
- target_compile_definitions(${PROJECT_NAME} PRIVATE SHERPA_ONNX_ENABLE_QNN)
166
+ # QNN runtime libraries (libQnnHtp.so etc.) are only linked when QNN_ROOT is
167
+ # set. Do not enable the QNN compile-time path without the SDK, otherwise the
168
+ # default provider ("qnn") would fail or silently fall back at runtime.
169
+ message(NOTICE "πŸ”§ QNN execution provider disabled (set -DQNN_ROOT=... to enable)")
168
170
  endif()
@@ -1,6 +1,4 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
2
  <uses-permission android:name="android.permission.INTERNET" />
3
3
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
4
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
5
- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
6
4
  </manifest>
@@ -17,8 +17,8 @@ Java_com_margelo_nitro_onnx_speech_OnnxSpeechPackage_setResourceDir(JNIEnv* env,
17
17
  }
18
18
 
19
19
  extern "C" JNIEXPORT void JNICALL
20
- Java_com_margelo_nitro_onnx_speech_OnnxSpeechPackage_setCacheDir(JNIEnv* env, jclass, jstring dir) {
20
+ Java_com_margelo_nitro_onnx_speech_OnnxSpeechPackage_setDocumentDir(JNIEnv* env, jclass, jstring dir) {
21
21
  const char* utf = env->GetStringUTFChars(dir, nullptr);
22
- margelo::nitro::onnx::speech::setCacheDir(utf);
22
+ margelo::nitro::onnx::speech::setDocumentDir(utf);
23
23
  env->ReleaseStringUTFChars(dir, utf);
24
24
  }
@@ -12,6 +12,7 @@ import java.io.FileOutputStream
12
12
  class OnnxSpeechPackage : ReactPackage {
13
13
  companion object {
14
14
  private const val TAG = "OnnxSpeechPackage"
15
+ private const val RESOURCE_NAME = "silero_vad.onnx"
15
16
 
16
17
  init {
17
18
  NitroOnnxSpeechOnLoad.initializeNative()
@@ -21,34 +22,42 @@ class OnnxSpeechPackage : ReactPackage {
21
22
  private external fun setResourceDir(dir: String)
22
23
 
23
24
  @JvmStatic
24
- private external fun setCacheDir(dir: String)
25
+ private external fun setDocumentDir(dir: String)
25
26
 
26
27
  /**
27
28
  * Copies bundled model files from APK assets to internal storage so that
28
29
  * the C++ layer can access them via real file paths (fopen-compatible).
30
+ * Re-copies when the app version changes or a file is missing.
29
31
  * Called lazily when createNativeModules is first invoked.
30
32
  */
31
33
  internal fun ensureResources(context: Context) {
32
34
  val filesDir = context.filesDir
33
- val marker = File(filesDir, ".resources_extracted")
34
- if (marker.exists()) {
35
- setResourceDir(filesDir.absolutePath)
36
- setCacheDir(filesDir.absolutePath)
37
- return
38
- }
39
- try {
40
- copyAsset(context, "silero_vad.onnx", filesDir)
41
- marker.createNewFile()
42
- setResourceDir(filesDir.absolutePath)
43
- setCacheDir(filesDir.absolutePath)
35
+
36
+ val version = try {
37
+ context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "0"
44
38
  } catch (e: Exception) {
45
- Log.e(TAG, "Failed to extract bundled resources", e)
39
+ "0"
40
+ }
41
+ val marker = File(filesDir, ".resources_extracted_$version")
42
+ val resourceFile = File(filesDir, RESOURCE_NAME)
43
+ if (!marker.exists() || !resourceFile.exists()) {
44
+ try {
45
+ copyAsset(context, RESOURCE_NAME, filesDir, overwrite = true)
46
+ filesDir.listFiles()
47
+ ?.filter { it.name.startsWith(".resources_extracted") }
48
+ ?.forEach { it.delete() }
49
+ marker.createNewFile()
50
+ } catch (e: Exception) {
51
+ Log.e(TAG, "Failed to extract bundled resources", e)
52
+ }
46
53
  }
54
+ setResourceDir(filesDir.absolutePath)
55
+ setDocumentDir(context.filesDir.absolutePath)
47
56
  }
48
57
 
49
- private fun copyAsset(context: Context, name: String, destDir: File) {
58
+ private fun copyAsset(context: Context, name: String, destDir: File, overwrite: Boolean = false) {
50
59
  val dest = File(destDir, name)
51
- if (dest.exists()) return
60
+ if (dest.exists() && !overwrite) return
52
61
  context.assets.open(name).use { input ->
53
62
  FileOutputStream(dest).use { output ->
54
63
  input.copyTo(output)