react-native-nitro-onnx 0.1.0 → 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 (47) hide show
  1. package/NitroOnnxSpeech.podspec +3 -1
  2. package/README.md +146 -41
  3. package/android/CMakeLists.txt +54 -1
  4. package/android/build.gradle +6 -0
  5. package/android/src/main/AndroidManifest.xml +0 -2
  6. package/android/src/main/cpp/cpp-adapter.cpp +2 -2
  7. package/android/src/main/java/com/margelo/nitro/onnx/speech/OnnxSpeechPackage.kt +26 -17
  8. package/cpp/AsrEngine.cpp +154 -59
  9. package/cpp/AsrEngine.hpp +19 -6
  10. package/cpp/AudioFileReader.cpp +4 -0
  11. package/cpp/ModelSingleton.hpp +14 -0
  12. package/cpp/NitroOnnxSpeech.cpp +37 -18
  13. package/cpp/NitroOnnxSpeech.hpp +1 -7
  14. package/cpp/OfflineAsr.cpp +17 -8
  15. package/cpp/OfflineAsr.hpp +1 -1
  16. package/cpp/ResourceDir.cpp +5 -5
  17. package/cpp/ResourceDir.hpp +5 -4
  18. package/cpp/SpeakerEngine.cpp +38 -28
  19. package/cpp/SpeakerEngine.hpp +14 -13
  20. package/cpp/SpeakerManager.cpp +16 -14
  21. package/cpp/SpeakerManager.hpp +2 -1
  22. package/cpp/SpeakerRecord.cpp +125 -0
  23. package/cpp/SpeakerRecord.hpp +42 -0
  24. package/cpp/StreamingAsr.cpp +20 -9
  25. package/cpp/StreamingAsr.hpp +1 -1
  26. package/cpp/Tts.cpp +44 -10
  27. package/cpp/Tts.hpp +2 -1
  28. package/cpp/TtsEngine.cpp +17 -9
  29. package/cpp/TtsEngine.hpp +24 -5
  30. package/cpp/Vad.cpp +12 -11
  31. package/cpp/Vad.hpp +1 -1
  32. package/cpp/VadEngine.cpp +27 -33
  33. package/cpp/VadEngine.hpp +9 -10
  34. package/cpp/Version.hpp +7 -0
  35. package/ios/OnnxSpeechInitializer.mm +18 -6
  36. package/lib/specs/OnnxSpeech.nitro.d.ts +43 -6
  37. package/lib/specs/OnnxSpeech.nitro.d.ts.map +1 -1
  38. package/nitrogen/generated/shared/c++/AsrModelConfig.hpp +10 -2
  39. package/nitrogen/generated/shared/c++/HybridOnnxSpeechSpec.cpp +1 -1
  40. package/nitrogen/generated/shared/c++/HybridOnnxSpeechSpec.hpp +1 -1
  41. package/nitrogen/generated/shared/c++/TtsModelConfig.hpp +10 -2
  42. package/nitrogen/generated/shared/c++/VadConfig.hpp +6 -2
  43. package/package.json +4 -3
  44. package/scripts/generate-version.js +22 -0
  45. package/src/specs/OnnxSpeech.nitro.ts +43 -6
  46. package/cpp/ThreadPool.cpp +0 -41
  47. package/cpp/ThreadPool.hpp +0 -62
package/cpp/AsrEngine.cpp CHANGED
@@ -6,8 +6,8 @@
6
6
  #include "AudioFileReader.hpp"
7
7
  #include "sherpa-onnx/c-api/c-api.h"
8
8
 
9
+ #include <cmath>
9
10
  #include <cstring>
10
- #include <fstream>
11
11
  #include <stdexcept>
12
12
 
13
13
  namespace margelo::nitro::onnx::speech {
@@ -33,25 +33,74 @@ std::string joinPath(const std::string& dir, const std::string& file) {
33
33
  ModelSingleton<const SherpaOnnxOfflineRecognizer> gOfflineRecognizerCache;
34
34
  ModelSingleton<const SherpaOnnxOnlineRecognizer> gOnlineRecognizerCache;
35
35
 
36
+ float meanTokenConfidence(const float* logProbs, int32_t count) {
37
+ if (logProbs == nullptr || count <= 0) {
38
+ return 0.0f;
39
+ }
40
+ double sum = 0.0;
41
+ for (int32_t i = 0; i < count; ++i) {
42
+ sum += logProbs[i];
43
+ }
44
+ return static_cast<float>(std::exp(sum / count));
45
+ }
46
+
47
+ void fillTimestampsMs(AsrEngineResult& result, const float* timestamps, int32_t count) {
48
+ if (timestamps == nullptr || count <= 0) {
49
+ return;
50
+ }
51
+ result.timestamps.assign(timestamps, timestamps + count);
52
+ for (float& t : result.timestamps) {
53
+ t *= 1000.0f;
54
+ }
55
+ if (!result.timestamps.empty()) {
56
+ result.startMs = result.timestamps.front();
57
+ }
58
+ }
59
+
60
+ AsrEngineResult fromOfflineResult(const SherpaOnnxOfflineRecognizerResult& r, float endMs) {
61
+ AsrEngineResult result;
62
+ result.text = r.text ? r.text : "";
63
+ result.endMs = endMs;
64
+ result.score = meanTokenConfidence(r.ys_log_probs, r.count);
65
+ fillTimestampsMs(result, r.timestamps, r.count);
66
+ if (r.json) {
67
+ result.json = r.json;
68
+ }
69
+ return result;
70
+ }
71
+
72
+ AsrEngineResult fromOnlineResult(const SherpaOnnxOnlineRecognizerResult& r) {
73
+ AsrEngineResult result;
74
+ result.text = r.text ? r.text : "";
75
+ fillTimestampsMs(result, r.timestamps, r.count);
76
+ if (r.json) {
77
+ result.json = r.json;
78
+ }
79
+ return result;
80
+ }
81
+
36
82
  } // namespace
37
83
 
84
+ std::string AsrEngineConfig::cacheSignature() const {
85
+ return modelDir + "|" + std::to_string(static_cast<int>(type)) + "|" + provider + "|" +
86
+ std::to_string(numThreads) + "|" + language + "|" + decodingMethod + "|" +
87
+ std::to_string(maxActivePaths) + "|" + (useItn ? "1" : "0");
88
+ }
89
+
38
90
  // ------------------------------------------------------------------------------
39
91
  // Offline ASR
40
92
  // ------------------------------------------------------------------------------
41
93
 
42
- OfflineAsrEngine::OfflineAsrEngine(std::shared_ptr<ThreadPool> threadPool)
43
- : threadPool_(std::move(threadPool)) {}
44
-
45
94
  OfflineAsrEngine::~OfflineAsrEngine() {
46
95
  unload();
47
96
  }
48
97
 
49
98
  void OfflineAsrEngine::load(const AsrEngineConfig& config) {
50
99
  unload();
100
+ std::lock_guard<std::mutex> lock(mutex_);
51
101
  config_ = config;
52
102
 
53
- const std::string key = config_.modelDir + "|" + std::to_string(static_cast<int>(config_.type));
54
- auto cached = gOfflineRecognizerCache.getOrCreate(key, [this](const std::string&) {
103
+ auto cached = gOfflineRecognizerCache.getOrCreate(config_.cacheSignature(), [this](const std::string&) {
55
104
  SherpaOnnxOfflineRecognizerConfig c;
56
105
  std::memset(&c, 0, sizeof(c));
57
106
 
@@ -70,6 +119,7 @@ void OfflineAsrEngine::load(const AsrEngineConfig& config) {
70
119
  c.model_config.whisper.decoder = whisperDecoder.c_str();
71
120
  c.model_config.whisper.language = config_.language.c_str();
72
121
  c.model_config.whisper.tail_paddings = 2;
122
+ c.model_config.model_type = "whisper";
73
123
  break;
74
124
  case AsrModelType::TRANSDUCER:
75
125
  case AsrModelType::ZIPFORMER:
@@ -79,24 +129,51 @@ void OfflineAsrEngine::load(const AsrEngineConfig& config) {
79
129
  c.model_config.transducer.joiner = joiner.c_str();
80
130
  break;
81
131
  case AsrModelType::PARAFORMER:
132
+ c.model_config.paraformer.model = model.c_str();
133
+ c.model_config.model_type = "paraformer";
134
+ break;
82
135
  case AsrModelType::WENET:
136
+ c.model_config.wenet_ctc.model = model.c_str();
137
+ c.model_config.model_type = "wenet_ctc";
138
+ break;
83
139
  case AsrModelType::TELESPEECH:
140
+ c.model_config.telespeech_ctc = model.c_str();
141
+ c.model_config.model_type = "telespeech_ctc";
142
+ break;
84
143
  case AsrModelType::SENSE_VOICE:
85
- c.model_config.paraformer.model = model.c_str();
144
+ c.model_config.sense_voice.model = model.c_str();
145
+ c.model_config.sense_voice.language = config_.language.c_str();
146
+ c.model_config.sense_voice.use_itn = config_.useItn ? 1 : 0;
147
+ c.model_config.model_type = "sense_voice";
86
148
  break;
87
149
  case AsrModelType::MOONSHINE:
150
+ // Moonshine layout: model=preprocessor, encoder=encoder,
151
+ // decoder=uncached_decoder (or merged_decoder when joiner is empty),
152
+ // joiner=cached_decoder.
153
+ c.model_config.moonshine.preprocessor = model.c_str();
154
+ c.model_config.moonshine.encoder = encoder.c_str();
155
+ if (joiner.empty()) {
156
+ c.model_config.moonshine.merged_decoder = decoder.c_str();
157
+ } else {
158
+ c.model_config.moonshine.uncached_decoder = decoder.c_str();
159
+ c.model_config.moonshine.cached_decoder = joiner.c_str();
160
+ }
161
+ c.model_config.model_type = "moonshine";
162
+ break;
88
163
  case AsrModelType::DOLPHIN:
164
+ c.model_config.dolphin.model = model.c_str();
165
+ c.model_config.model_type = "dolphin";
166
+ break;
89
167
  case AsrModelType::NEMO:
90
168
  c.model_config.nemo_ctc.model = model.c_str();
91
- if (config_.type == AsrModelType::NEMO) {
92
- c.model_config.model_type = "nemo";
93
- }
169
+ c.model_config.model_type = "nemo";
94
170
  break;
95
171
  }
96
172
 
97
173
  c.model_config.tokens = tokens.c_str();
98
174
  c.model_config.num_threads = config_.numThreads;
99
- c.model_config.debug = 0;
175
+ c.model_config.debug = config_.debug ? 1 : 0;
176
+ c.model_config.provider = config_.provider.c_str();
100
177
  c.decoding_method = config_.decodingMethod.c_str();
101
178
  c.max_active_paths = config_.maxActivePaths;
102
179
 
@@ -112,34 +189,34 @@ void OfflineAsrEngine::load(const AsrEngineConfig& config) {
112
189
  }
113
190
 
114
191
  bool OfflineAsrEngine::isLoaded() const {
192
+ std::lock_guard<std::mutex> lock(mutex_);
115
193
  return recognizer_ != nullptr;
116
194
  }
117
195
 
118
196
  AsrEngineResult OfflineAsrEngine::recognize(const std::vector<float>& samples) {
119
- if (recognizer_ == nullptr) {
197
+ // Copy the shared_ptr under the lock, then decode without holding it so
198
+ // concurrent recognize() calls can share the same const recognizer.
199
+ std::shared_ptr<const SherpaOnnxOfflineRecognizer> recognizer;
200
+ {
201
+ std::lock_guard<std::mutex> lock(mutex_);
202
+ recognizer = recognizer_;
203
+ }
204
+ if (recognizer == nullptr) {
120
205
  throw std::runtime_error("Offline ASR not loaded");
121
206
  }
122
207
 
123
- const SherpaOnnxOfflineStream* stream = SherpaOnnxCreateOfflineStream(recognizer_.get());
208
+ const SherpaOnnxOfflineStream* stream = SherpaOnnxCreateOfflineStream(recognizer.get());
124
209
  SherpaOnnxAcceptWaveformOffline(stream, 16000, samples.data(), static_cast<int32_t>(samples.size()));
125
- SherpaOnnxDecodeOfflineStream(recognizer_.get(), stream);
210
+ SherpaOnnxDecodeOfflineStream(recognizer.get(), stream);
126
211
 
127
- const char* json = SherpaOnnxGetOfflineStreamResultAsJson(stream);
212
+ const SherpaOnnxOfflineRecognizerResult* raw = SherpaOnnxGetOfflineStreamResult(stream);
128
213
  AsrEngineResult result;
129
- result.json = json ? json : "";
130
-
131
- // Parse the JSON to extract text. In a full implementation, use a JSON
132
- // library to also populate timestamps and score.
133
- const char* textKey = "\"text\":\"";
134
- const char* textStart = std::strstr(result.json.c_str(), textKey);
135
- if (textStart != nullptr) {
136
- textStart += std::strlen(textKey);
137
- const char* textEnd = std::strstr(textStart, "\"");
138
- if (textEnd != nullptr) {
139
- result.text = std::string(textStart, textEnd);
140
- }
214
+ if (raw != nullptr) {
215
+ result = fromOfflineResult(*raw, samplesToMs(static_cast<int32_t>(samples.size())));
216
+ SherpaOnnxDestroyOfflineRecognizerResult(raw);
217
+ } else {
218
+ result.endMs = samplesToMs(static_cast<int32_t>(samples.size()));
141
219
  }
142
- result.endMs = samplesToMs(static_cast<int32_t>(samples.size()));
143
220
 
144
221
  SherpaOnnxDestroyOfflineStream(stream);
145
222
  return result;
@@ -156,6 +233,7 @@ AsrEngineResult OfflineAsrEngine::recognizeFile(const std::string& path) {
156
233
  }
157
234
 
158
235
  void OfflineAsrEngine::unload() {
236
+ std::lock_guard<std::mutex> lock(mutex_);
159
237
  recognizer_.reset();
160
238
  }
161
239
 
@@ -163,9 +241,6 @@ void OfflineAsrEngine::unload() {
163
241
  // Streaming ASR
164
242
  // ------------------------------------------------------------------------------
165
243
 
166
- StreamingAsrEngine::StreamingAsrEngine(std::shared_ptr<ThreadPool> threadPool)
167
- : threadPool_(std::move(threadPool)) {}
168
-
169
244
  StreamingAsrEngine::~StreamingAsrEngine() {
170
245
  unload();
171
246
  }
@@ -174,10 +249,11 @@ void StreamingAsrEngine::load(
174
249
  const AsrEngineConfig& config,
175
250
  std::shared_ptr<StreamingAsrListener> listener) {
176
251
  unload();
252
+ std::lock_guard<std::mutex> lock(mutex_);
177
253
  config_ = config;
178
254
  listener_ = std::move(listener);
179
255
 
180
- const std::string key = config_.modelDir + "|streaming|" + std::to_string(static_cast<int>(config_.type));
256
+ const std::string key = config_.cacheSignature() + "|streaming";
181
257
  auto cached = gOnlineRecognizerCache.getOrCreate(key, [this](const std::string&) {
182
258
  SherpaOnnxOnlineRecognizerConfig c;
183
259
  std::memset(&c, 0, sizeof(c));
@@ -196,11 +272,13 @@ void StreamingAsrEngine::load(
196
272
  c.model_config.transducer.joiner = joiner.c_str();
197
273
  break;
198
274
  default:
199
- throw std::runtime_error("Streaming ASR does not support this model type in the scaffold");
275
+ throw std::runtime_error("Streaming ASR only supports transducer / zipformer / conformer models");
200
276
  }
201
277
 
202
278
  c.model_config.tokens = tokens.c_str();
203
279
  c.model_config.num_threads = config_.numThreads;
280
+ c.model_config.debug = config_.debug ? 1 : 0;
281
+ c.model_config.provider = config_.provider.c_str();
204
282
  c.decoding_method = config_.decodingMethod.c_str();
205
283
  c.max_active_paths = config_.maxActivePaths;
206
284
  c.enable_endpoint = 1;
@@ -219,48 +297,64 @@ void StreamingAsrEngine::load(
219
297
  }
220
298
 
221
299
  bool StreamingAsrEngine::isLoaded() const {
300
+ std::lock_guard<std::mutex> lock(mutex_);
222
301
  return recognizer_ != nullptr && stream_ != nullptr;
223
302
  }
224
303
 
225
304
  void StreamingAsrEngine::acceptWaveform(const std::vector<float>& samples) {
226
- if (recognizer_ == nullptr || stream_ == nullptr) {
227
- throw std::runtime_error("Streaming ASR not loaded");
228
- }
229
- SherpaOnnxOnlineStreamAcceptWaveform(stream_.get(), 16000, samples.data(), static_cast<int32_t>(samples.size()));
230
-
231
- if (listener_ && SherpaOnnxIsOnlineStreamReady(recognizer_.get(), stream_.get())) {
232
- SherpaOnnxDecodeOnlineStream(recognizer_.get(), stream_.get());
233
- const char* json = SherpaOnnxGetOnlineStreamResultAsJson(recognizer_.get(), stream_.get());
234
- if (json != nullptr) {
235
- AsrEngineResult result;
236
- result.json = json;
237
- // TODO: parse text from JSON.
238
- listener_->onPartialResult(result);
239
- SherpaOnnxDestroyOnlineStreamResultJson(json);
305
+ std::shared_ptr<StreamingAsrListener> listener;
306
+ AsrEngineResult result;
307
+ bool hasResult = false;
308
+ {
309
+ std::lock_guard<std::mutex> lock(mutex_);
310
+ if (recognizer_ == nullptr || stream_ == nullptr) {
311
+ throw std::runtime_error("Streaming ASR not loaded");
312
+ }
313
+ SherpaOnnxOnlineStreamAcceptWaveform(stream_.get(), 16000, samples.data(), static_cast<int32_t>(samples.size()));
314
+
315
+ if (SherpaOnnxIsOnlineStreamReady(recognizer_.get(), stream_.get())) {
316
+ SherpaOnnxDecodeOnlineStream(recognizer_.get(), stream_.get());
317
+ const SherpaOnnxOnlineRecognizerResult* raw =
318
+ SherpaOnnxGetOnlineStreamResult(recognizer_.get(), stream_.get());
319
+ if (raw != nullptr) {
320
+ result = fromOnlineResult(*raw);
321
+ SherpaOnnxDestroyOnlineRecognizerResult(raw);
322
+ hasResult = true;
323
+ }
240
324
  }
325
+ listener = listener_.lock();
326
+ }
327
+ if (hasResult && listener) {
328
+ listener->onPartialResult(result);
241
329
  }
242
330
  }
243
331
 
244
332
  AsrEngineResult StreamingAsrEngine::finalize() {
245
- if (recognizer_ == nullptr || stream_ == nullptr) {
246
- throw std::runtime_error("Streaming ASR not loaded");
247
- }
248
- SherpaOnnxOnlineStreamInputFinished(stream_.get());
249
- SherpaOnnxDecodeOnlineStream(recognizer_.get(), stream_.get());
250
- const char* json = SherpaOnnxGetOnlineStreamResultAsJson(recognizer_.get(), stream_.get());
333
+ std::shared_ptr<StreamingAsrListener> listener;
251
334
  AsrEngineResult result;
252
- if (json != nullptr) {
253
- result.json = json;
254
- // TODO: parse text from JSON.
255
- SherpaOnnxDestroyOnlineStreamResultJson(json);
335
+ {
336
+ std::lock_guard<std::mutex> lock(mutex_);
337
+ if (recognizer_ == nullptr || stream_ == nullptr) {
338
+ throw std::runtime_error("Streaming ASR not loaded");
339
+ }
340
+ SherpaOnnxOnlineStreamInputFinished(stream_.get());
341
+ SherpaOnnxDecodeOnlineStream(recognizer_.get(), stream_.get());
342
+ const SherpaOnnxOnlineRecognizerResult* raw =
343
+ SherpaOnnxGetOnlineStreamResult(recognizer_.get(), stream_.get());
344
+ if (raw != nullptr) {
345
+ result = fromOnlineResult(*raw);
346
+ SherpaOnnxDestroyOnlineRecognizerResult(raw);
347
+ }
348
+ listener = listener_.lock();
256
349
  }
257
- if (listener_) {
258
- listener_->onFinalResult(result);
350
+ if (listener) {
351
+ listener->onFinalResult(result);
259
352
  }
260
353
  return result;
261
354
  }
262
355
 
263
356
  void StreamingAsrEngine::reset() {
357
+ std::lock_guard<std::mutex> lock(mutex_);
264
358
  if (recognizer_ != nullptr) {
265
359
  stream_ = std::shared_ptr<const SherpaOnnxOnlineStream>(
266
360
  SherpaOnnxCreateOnlineStream(recognizer_.get()),
@@ -269,6 +363,7 @@ void StreamingAsrEngine::reset() {
269
363
  }
270
364
 
271
365
  void StreamingAsrEngine::unload() {
366
+ std::lock_guard<std::mutex> lock(mutex_);
272
367
  stream_.reset();
273
368
  recognizer_.reset();
274
369
  listener_.reset();
package/cpp/AsrEngine.hpp CHANGED
@@ -9,11 +9,11 @@
9
9
 
10
10
  #include "AudioUtils.hpp"
11
11
  #include "ModelSingleton.hpp"
12
- #include "ThreadPool.hpp"
13
12
 
14
13
  #include "AsrModelType.hpp"
15
14
 
16
15
  #include <memory>
16
+ #include <mutex>
17
17
  #include <string>
18
18
  #include <vector>
19
19
 
@@ -41,6 +41,19 @@ struct AsrEngineConfig {
41
41
  int32_t maxActivePaths = 4;
42
42
  std::string language = "en";
43
43
  bool useItn = true;
44
+ bool debug = false;
45
+ #if defined(__ANDROID__) && defined(SHERPA_ONNX_ENABLE_QNN)
46
+ std::string provider = "qnn";
47
+ #elif defined(__ANDROID__)
48
+ std::string provider = "nnapi";
49
+ #elif defined(__APPLE__)
50
+ std::string provider = "coreml";
51
+ #else
52
+ std::string provider = "cpu";
53
+ #endif
54
+
55
+ /** Cache-key signature covering options that change recognizer identity. */
56
+ std::string cacheSignature() const;
44
57
  };
45
58
 
46
59
  /** Native recognition result before conversion to the generated AsrResult. */
@@ -65,7 +78,7 @@ class StreamingAsrListener {
65
78
  /** Offline ASR engine. */
66
79
  class OfflineAsrEngine final {
67
80
  public:
68
- explicit OfflineAsrEngine(std::shared_ptr<ThreadPool> threadPool);
81
+ OfflineAsrEngine() = default;
69
82
  ~OfflineAsrEngine();
70
83
 
71
84
  OfflineAsrEngine(const OfflineAsrEngine&) = delete;
@@ -78,7 +91,7 @@ class OfflineAsrEngine final {
78
91
  void unload();
79
92
 
80
93
  private:
81
- std::shared_ptr<ThreadPool> threadPool_;
94
+ mutable std::mutex mutex_;
82
95
  AsrEngineConfig config_;
83
96
  std::shared_ptr<const SherpaOnnxOfflineRecognizer> recognizer_;
84
97
  };
@@ -86,7 +99,7 @@ class OfflineAsrEngine final {
86
99
  /** Streaming ASR engine. */
87
100
  class StreamingAsrEngine final {
88
101
  public:
89
- explicit StreamingAsrEngine(std::shared_ptr<ThreadPool> threadPool);
102
+ StreamingAsrEngine() = default;
90
103
  ~StreamingAsrEngine();
91
104
 
92
105
  StreamingAsrEngine(const StreamingAsrEngine&) = delete;
@@ -100,9 +113,9 @@ class StreamingAsrEngine final {
100
113
  void unload();
101
114
 
102
115
  private:
103
- std::shared_ptr<ThreadPool> threadPool_;
116
+ mutable std::mutex mutex_;
104
117
  AsrEngineConfig config_;
105
- std::shared_ptr<StreamingAsrListener> listener_;
118
+ std::weak_ptr<StreamingAsrListener> listener_;
106
119
  std::shared_ptr<const SherpaOnnxOnlineRecognizer> recognizer_;
107
120
  std::shared_ptr<const SherpaOnnxOnlineStream> stream_;
108
121
  };
@@ -3,6 +3,7 @@
3
3
  // ------------------------------------------------------------------------------
4
4
  #include "AudioFileReader.hpp"
5
5
 
6
+ #include <bit>
6
7
  #include <cmath>
7
8
  #include <cstring>
8
9
  #include <fstream>
@@ -12,6 +13,9 @@ namespace margelo::nitro::onnx::speech {
12
13
 
13
14
  namespace {
14
15
 
16
+ static_assert(std::endian::native == std::endian::little,
17
+ "WAV reader assumes little-endian targets");
18
+
15
19
  constexpr int32_t kTargetSampleRate = 16000;
16
20
 
17
21
  struct WavHeader {
@@ -16,6 +16,9 @@ namespace margelo::nitro::onnx::speech {
16
16
 
17
17
  /**
18
18
  * Thread-safe cache for heavy model instances.
19
+ * The cache key must cover every config field that changes model identity
20
+ * (paths, provider, thread count, language, ...), otherwise a later load with
21
+ * different options would incorrectly reuse the first instance.
19
22
  * @tparam T The native model type (e.g. SherpaOnnxOfflineRecognizer).
20
23
  */
21
24
  template <typename T>
@@ -26,6 +29,7 @@ class ModelSingleton final {
26
29
  /** Return a cached instance or create one using factory. */
27
30
  std::shared_ptr<T> getOrCreate(const std::string& key, const Factory& factory) {
28
31
  std::lock_guard<std::mutex> lock(mutex_);
32
+ pruneExpiredLocked();
29
33
  auto it = instances_.find(key);
30
34
  if (it != instances_.end()) {
31
35
  if (auto alive = it->second.lock()) {
@@ -44,6 +48,16 @@ class ModelSingleton final {
44
48
  }
45
49
 
46
50
  private:
51
+ void pruneExpiredLocked() {
52
+ for (auto it = instances_.begin(); it != instances_.end();) {
53
+ if (it->second.expired()) {
54
+ it = instances_.erase(it);
55
+ } else {
56
+ ++it;
57
+ }
58
+ }
59
+ }
60
+
47
61
  std::mutex mutex_;
48
62
  std::unordered_map<std::string, std::weak_ptr<T>> instances_;
49
63
  };
@@ -9,60 +9,79 @@
9
9
  #include "StreamingAsr.hpp"
10
10
  #include "Tts.hpp"
11
11
  #include "Vad.hpp"
12
+ #include "Version.hpp"
12
13
 
13
14
  #include <cstdio>
14
15
  #include <cstring>
15
16
 
17
+ #ifdef __ANDROID__
18
+ #include <sys/system_properties.h>
19
+ #endif
20
+
16
21
  namespace margelo::nitro::onnx::speech {
17
22
 
18
23
  NitroOnnxSpeech::NitroOnnxSpeech()
19
- : HybridObject(TAG),
20
- threadPool_(std::make_shared<ThreadPool>()),
21
- cacheDir_(getCacheDir()) {}
24
+ : HybridObject(TAG) {}
22
25
 
23
26
  NitroOnnxSpeech::~NitroOnnxSpeech() = default;
24
27
 
25
28
  std::shared_ptr<HybridVadSpec> NitroOnnxSpeech::createVad() {
26
- return std::make_shared<Vad>(threadPool_);
29
+ return std::make_shared<Vad>();
27
30
  }
28
31
 
29
32
  std::shared_ptr<HybridOfflineAsrSpec> NitroOnnxSpeech::createOfflineAsr() {
30
- return std::make_shared<OfflineAsr>(threadPool_);
33
+ return std::make_shared<OfflineAsr>();
31
34
  }
32
35
 
33
36
  std::shared_ptr<HybridStreamingAsrSpec> NitroOnnxSpeech::createStreamingAsr() {
34
- return std::make_shared<StreamingAsr>(threadPool_);
37
+ return std::make_shared<StreamingAsr>();
35
38
  }
36
39
 
37
40
  std::shared_ptr<HybridTtsSpec> NitroOnnxSpeech::createTts() {
38
- return std::make_shared<Tts>(threadPool_);
41
+ return std::make_shared<Tts>();
39
42
  }
40
43
 
41
44
  std::shared_ptr<HybridSpeakerManagerSpec> NitroOnnxSpeech::createSpeakerManager() {
42
- return std::make_shared<SpeakerManager>(threadPool_, cacheDir_);
45
+ return std::make_shared<SpeakerManager>();
43
46
  }
44
47
 
45
48
  std::string NitroOnnxSpeech::getVersion() {
46
- return "0.1.0";
49
+ return NITRO_ONNX_SPEECH_VERSION;
47
50
  }
48
51
 
49
- bool NitroOnnxSpeech::isQualcommCpu() {
52
+ std::string NitroOnnxSpeech::getQualcommSoc() {
50
53
  #ifdef __ANDROID__
54
+ // 1) Try ro.soc.model first (available on most modern Android devices).
55
+ char prop[PROP_VALUE_MAX] = {0};
56
+ if (__system_property_get("ro.soc.model", prop) > 0 && prop[0] != '\0') {
57
+ return std::string(prop);
58
+ }
59
+
60
+ // 2) Fall back to parsing /proc/cpuinfo Hardware line.
51
61
  FILE* f = fopen("/proc/cpuinfo", "r");
52
- if (!f) return false;
62
+ if (!f) return "";
53
63
  char line[256];
54
- bool found = false;
55
64
  while (fgets(line, sizeof(line), f)) {
56
- if (strstr(line, "Qualcomm") != nullptr) {
57
- found = true;
58
- break;
65
+ if (strncmp(line, "Hardware", 8) == 0) {
66
+ char* colon = strchr(line, ':');
67
+ if (!colon) break;
68
+ char* val = colon + 1;
69
+ while (*val == ' ' || *val == '\t') ++val;
70
+ char* nl = strchr(val, '\n');
71
+ if (nl) *nl = '\0';
72
+ if (strstr(val, "Qualcomm") == nullptr) break;
73
+ const char* last = strrchr(val, ' ');
74
+ if (last && *(last + 1) != '\0') {
75
+ fclose(f);
76
+ return std::string(last + 1);
77
+ }
78
+ fclose(f);
79
+ return std::string(val);
59
80
  }
60
81
  }
61
82
  fclose(f);
62
- return found;
63
- #else
64
- return false;
65
83
  #endif
84
+ return "";
66
85
  }
67
86
 
68
87
  } // namespace margelo::nitro::onnx::speech
@@ -4,8 +4,6 @@
4
4
  // ------------------------------------------------------------------------------
5
5
  #pragma once
6
6
 
7
- #include "ThreadPool.hpp"
8
-
9
7
  #include <NitroModules/HybridObject.hpp>
10
8
  #include <HybridOnnxSpeechSpec.hpp>
11
9
  #include <HybridOfflineAsrSpec.hpp>
@@ -32,11 +30,7 @@ class NitroOnnxSpeech : public HybridOnnxSpeechSpec {
32
30
  std::shared_ptr<HybridTtsSpec> createTts() override;
33
31
  std::shared_ptr<HybridSpeakerManagerSpec> createSpeakerManager() override;
34
32
  std::string getVersion() override;
35
- bool isQualcommCpu() override;
36
-
37
- private:
38
- std::shared_ptr<ThreadPool> threadPool_;
39
- std::string cacheDir_;
33
+ std::string getQualcommSoc() override;
40
34
  };
41
35
 
42
36
  } // namespace margelo::nitro::onnx::speech
@@ -28,15 +28,14 @@ AsrResult toAsrResult(const AsrEngineResult& native) {
28
28
 
29
29
  } // namespace
30
30
 
31
- OfflineAsr::OfflineAsr(std::shared_ptr<ThreadPool> threadPool)
32
- : HybridObject(TAG), engine_(std::move(threadPool)) {}
31
+ OfflineAsr::OfflineAsr() : HybridObject(TAG) {}
33
32
 
34
33
  OfflineAsr::~OfflineAsr() {
35
34
  engine_.unload();
36
35
  }
37
36
 
38
37
  std::shared_ptr<Promise<void>> OfflineAsr::load(const AsrModelConfig& config) {
39
- return Promise<void>::async([this, config]() {
38
+ return Promise<void>::async([self = shared_cast<OfflineAsr>(), config]() {
40
39
  AsrEngineConfig native;
41
40
  native.type = config.type;
42
41
  native.modelDir = config.modelDir;
@@ -53,7 +52,17 @@ std::shared_ptr<Promise<void>> OfflineAsr::load(const AsrModelConfig& config) {
53
52
  native.maxActivePaths = static_cast<int32_t>(config.maxActivePaths.value_or(4));
54
53
  native.language = config.language.value_or("en");
55
54
  native.useItn = config.useItn.value_or(true);
56
- engine_.load(native);
55
+ native.debug = config.debug.value_or(false);
56
+ #if defined(__ANDROID__) && defined(SHERPA_ONNX_ENABLE_QNN)
57
+ native.provider = config.provider.value_or("qnn");
58
+ #elif defined(__ANDROID__)
59
+ native.provider = config.provider.value_or("nnapi");
60
+ #elif defined(__APPLE__)
61
+ native.provider = config.provider.value_or("coreml");
62
+ #else
63
+ native.provider = config.provider.value_or("cpu");
64
+ #endif
65
+ self->engine_.load(native);
57
66
  });
58
67
  }
59
68
 
@@ -63,19 +72,19 @@ bool OfflineAsr::isLoaded() {
63
72
 
64
73
  std::shared_ptr<Promise<AsrResult>> OfflineAsr::recognize(
65
74
  const std::shared_ptr<ArrayBuffer>& samples) {
66
- return Promise<AsrResult>::async([this, samples]() {
75
+ return Promise<AsrResult>::async([self = shared_cast<OfflineAsr>(), samples]() {
67
76
  auto floatSamples = bytesToFloatVector(samples->data(), samples->size());
68
- return toAsrResult(engine_.recognize(floatSamples));
77
+ return toAsrResult(self->engine_.recognize(floatSamples));
69
78
  });
70
79
  }
71
80
 
72
81
  std::shared_ptr<Promise<AsrResult>> OfflineAsr::recognizeFile(const std::string& path) {
73
82
  return Promise<AsrResult>::async(
74
- [this, path]() { return toAsrResult(engine_.recognizeFile(path)); });
83
+ [self = shared_cast<OfflineAsr>(), path]() { return toAsrResult(self->engine_.recognizeFile(path)); });
75
84
  }
76
85
 
77
86
  std::shared_ptr<Promise<void>> OfflineAsr::unload() {
78
- return Promise<void>::async([this]() { engine_.unload(); });
87
+ return Promise<void>::async([self = shared_cast<OfflineAsr>()]() { self->engine_.unload(); });
79
88
  }
80
89
 
81
90
  } // namespace margelo::nitro::onnx::speech
@@ -17,7 +17,7 @@ class OfflineAsr : public HybridOfflineAsrSpec {
17
17
  public:
18
18
  static constexpr auto TAG = "OfflineAsr";
19
19
 
20
- explicit OfflineAsr(std::shared_ptr<ThreadPool> threadPool);
20
+ OfflineAsr();
21
21
  ~OfflineAsr() override;
22
22
 
23
23
  std::shared_ptr<Promise<void>> load(const AsrModelConfig& config) override;