react-native-nitro-onnx 0.1.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 (91) hide show
  1. package/NOTICE +241 -0
  2. package/NitroOnnxSpeech.podspec +51 -0
  3. package/Package.swift +52 -0
  4. package/README.md +398 -0
  5. package/android/CMakeLists.txt +117 -0
  6. package/android/build.gradle +65 -0
  7. package/android/src/main/AndroidManifest.xml +6 -0
  8. package/android/src/main/assets/silero_vad.onnx +0 -0
  9. package/android/src/main/cpp/cpp-adapter.cpp +24 -0
  10. package/android/src/main/java/com/margelo/nitro/onnx/speech/OnnxSpeechPackage.kt +67 -0
  11. package/assets/silero_vad.onnx +0 -0
  12. package/cpp/AndroidPthreadCompat.cpp +16 -0
  13. package/cpp/AsrEngine.cpp +277 -0
  14. package/cpp/AsrEngine.hpp +110 -0
  15. package/cpp/AudioFileReader.cpp +177 -0
  16. package/cpp/AudioFileReader.hpp +26 -0
  17. package/cpp/AudioUtils.cpp +28 -0
  18. package/cpp/AudioUtils.hpp +38 -0
  19. package/cpp/ModelSingleton.hpp +51 -0
  20. package/cpp/NitroOnnxSpeech.cpp +68 -0
  21. package/cpp/NitroOnnxSpeech.hpp +42 -0
  22. package/cpp/OfflineAsr.cpp +81 -0
  23. package/cpp/OfflineAsr.hpp +33 -0
  24. package/cpp/ResourceDir.cpp +29 -0
  25. package/cpp/ResourceDir.hpp +18 -0
  26. package/cpp/SpeakerEngine.cpp +170 -0
  27. package/cpp/SpeakerEngine.hpp +70 -0
  28. package/cpp/SpeakerManager.cpp +93 -0
  29. package/cpp/SpeakerManager.hpp +43 -0
  30. package/cpp/StreamingAsr.cpp +123 -0
  31. package/cpp/StreamingAsr.hpp +55 -0
  32. package/cpp/ThreadPool.cpp +41 -0
  33. package/cpp/ThreadPool.hpp +62 -0
  34. package/cpp/Tts.cpp +132 -0
  35. package/cpp/Tts.hpp +38 -0
  36. package/cpp/TtsEngine.cpp +222 -0
  37. package/cpp/TtsEngine.hpp +77 -0
  38. package/cpp/Vad.cpp +126 -0
  39. package/cpp/Vad.hpp +62 -0
  40. package/cpp/VadEngine.cpp +217 -0
  41. package/cpp/VadEngine.hpp +126 -0
  42. package/ios/OnnxSpeechInitializer.mm +37 -0
  43. package/ios/PrivacyInfo.xcprivacy +14 -0
  44. package/lib/index.d.ts +11 -0
  45. package/lib/index.d.ts.map +1 -0
  46. package/lib/index.js +19 -0
  47. package/lib/index.js.map +1 -0
  48. package/lib/specs/OnnxSpeech.nitro.d.ts +264 -0
  49. package/lib/specs/OnnxSpeech.nitro.d.ts.map +1 -0
  50. package/lib/specs/OnnxSpeech.nitro.js +6 -0
  51. package/lib/specs/OnnxSpeech.nitro.js.map +1 -0
  52. package/nitro.json +19 -0
  53. package/nitrogen/generated/.gitattributes +1 -0
  54. package/nitrogen/generated/android/NitroOnnxSpeech+autolinking.cmake +86 -0
  55. package/nitrogen/generated/android/NitroOnnxSpeech+autolinking.gradle +27 -0
  56. package/nitrogen/generated/android/NitroOnnxSpeechOnLoad.cpp +49 -0
  57. package/nitrogen/generated/android/NitroOnnxSpeechOnLoad.hpp +34 -0
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onnx/speech/NitroOnnxSpeechOnLoad.kt +35 -0
  59. package/nitrogen/generated/ios/NitroOnnxSpeech+autolinking.rb +62 -0
  60. package/nitrogen/generated/ios/NitroOnnxSpeech-Swift-Cxx-Bridge.cpp +17 -0
  61. package/nitrogen/generated/ios/NitroOnnxSpeech-Swift-Cxx-Bridge.hpp +27 -0
  62. package/nitrogen/generated/ios/NitroOnnxSpeech-Swift-Cxx-Umbrella.hpp +38 -0
  63. package/nitrogen/generated/ios/NitroOnnxSpeechAutolinking.mm +35 -0
  64. package/nitrogen/generated/ios/NitroOnnxSpeechAutolinking.swift +16 -0
  65. package/nitrogen/generated/shared/c++/AsrModelConfig.hpp +142 -0
  66. package/nitrogen/generated/shared/c++/AsrModelType.hpp +112 -0
  67. package/nitrogen/generated/shared/c++/AsrResult.hpp +105 -0
  68. package/nitrogen/generated/shared/c++/HybridOfflineAsrSpec.cpp +25 -0
  69. package/nitrogen/generated/shared/c++/HybridOfflineAsrSpec.hpp +73 -0
  70. package/nitrogen/generated/shared/c++/HybridOnnxSpeechSpec.cpp +27 -0
  71. package/nitrogen/generated/shared/c++/HybridOnnxSpeechSpec.hpp +82 -0
  72. package/nitrogen/generated/shared/c++/HybridSpeakerManagerSpec.cpp +28 -0
  73. package/nitrogen/generated/shared/c++/HybridSpeakerManagerSpec.hpp +77 -0
  74. package/nitrogen/generated/shared/c++/HybridStreamingAsrSpec.cpp +32 -0
  75. package/nitrogen/generated/shared/c++/HybridStreamingAsrSpec.hpp +81 -0
  76. package/nitrogen/generated/shared/c++/HybridTtsSpec.cpp +26 -0
  77. package/nitrogen/generated/shared/c++/HybridTtsSpec.hpp +74 -0
  78. package/nitrogen/generated/shared/c++/HybridVadSpec.cpp +31 -0
  79. package/nitrogen/generated/shared/c++/HybridVadSpec.hpp +81 -0
  80. package/nitrogen/generated/shared/c++/RegisteredSpeaker.hpp +91 -0
  81. package/nitrogen/generated/shared/c++/SpeakerEmbeddingConfig.hpp +91 -0
  82. package/nitrogen/generated/shared/c++/TtsModelConfig.hpp +178 -0
  83. package/nitrogen/generated/shared/c++/TtsModelType.hpp +88 -0
  84. package/nitrogen/generated/shared/c++/TtsResult.hpp +91 -0
  85. package/nitrogen/generated/shared/c++/VadConfig.hpp +100 -0
  86. package/nitrogen/generated/shared/c++/VadSegment.hpp +91 -0
  87. package/package.json +62 -0
  88. package/scripts/prepare-sherpa-onnx.js +236 -0
  89. package/scripts/test-cpp.js +24 -0
  90. package/src/index.ts +45 -0
  91. package/src/specs/OnnxSpeech.nitro.ts +326 -0
@@ -0,0 +1,222 @@
1
+ // ------------------------------------------------------------------------------
2
+ // TtsEngine.cpp
3
+ // ------------------------------------------------------------------------------
4
+ #include "TtsEngine.hpp"
5
+
6
+ #include "sherpa-onnx/c-api/c-api.h"
7
+
8
+ #include <cstring>
9
+ #include <stdexcept>
10
+ #include <string>
11
+ #include <cstdio>
12
+
13
+ #ifdef __ANDROID__
14
+ #include <android/log.h>
15
+ #define TTS_LOG(fmt, ...) __android_log_print(ANDROID_LOG_ERROR, "TtsEngine", fmt, ##__VA_ARGS__)
16
+ #else
17
+ #define TTS_LOG(fmt, ...) fprintf(stderr, "[TtsEngine] " fmt "\n", ##__VA_ARGS__)
18
+ #endif
19
+
20
+ namespace margelo::nitro::onnx::speech {
21
+
22
+ namespace {
23
+
24
+ std::string joinPath(const std::string& dir, const std::string& file) {
25
+ if (file.empty()) {
26
+ return dir;
27
+ }
28
+ if (file[0] == '/') {
29
+ return file;
30
+ }
31
+ if (dir.empty()) {
32
+ return file;
33
+ }
34
+ if (dir.back() == '/') {
35
+ return dir + file;
36
+ }
37
+ return dir + "/" + file;
38
+ }
39
+
40
+ void checkFile(const std::string& path, const std::string& name) {
41
+ if (path.empty()) return;
42
+ FILE* f = fopen(path.c_str(), "rb");
43
+ if (!f) {
44
+ TTS_LOG("MISSING %s: %s", name.c_str(), path.c_str());
45
+ throw std::runtime_error("TTS model file not found: " + name + " -> " + path);
46
+ }
47
+ fclose(f);
48
+ }
49
+
50
+ ModelSingleton<const SherpaOnnxOfflineTts> gTtsCache;
51
+
52
+ } // namespace
53
+
54
+ TtsEngine::TtsEngine(std::shared_ptr<ThreadPool> threadPool)
55
+ : threadPool_(std::move(threadPool)) {}
56
+
57
+ TtsEngine::~TtsEngine() {
58
+ unload();
59
+ }
60
+
61
+ void TtsEngine::load(const TtsEngineConfig& config) {
62
+ unload();
63
+ config_ = config;
64
+
65
+ const std::string key = config_.modelDir + "|" + std::to_string(static_cast<int>(config_.type));
66
+ auto cached = gTtsCache.getOrCreate(key, [this](const std::string&) {
67
+ SherpaOnnxOfflineTtsConfig c;
68
+ std::memset(&c, 0, sizeof(c));
69
+
70
+ std::string model = joinPath(config_.modelDir, config_.model);
71
+ std::string acoustic = joinPath(config_.modelDir, config_.acousticModel);
72
+ std::string vocoder = joinPath(config_.modelDir, config_.vocoder);
73
+ std::string tokens = joinPath(config_.modelDir, config_.tokens);
74
+ std::string lexicon = joinPath(config_.modelDir, config_.lexicon);
75
+ std::string voices = joinPath(config_.modelDir, config_.voices);
76
+ std::string espeakNgData = joinPath(config_.modelDir, config_.espeakNgData);
77
+ std::string dictDir = joinPath(config_.modelDir, config_.dictDir);
78
+ std::string configPath = joinPath(config_.modelDir, config_.config);
79
+
80
+ std::string lmMain = joinPath(config_.modelDir, config_.lmMain);
81
+ std::string lmFlow = joinPath(config_.modelDir, config_.lmFlow);
82
+ std::string textConditioner = joinPath(config_.modelDir, config_.textConditioner);
83
+ std::string pocketEncoder = joinPath(config_.modelDir, config_.pocketEncoder);
84
+ std::string pocketDecoder = joinPath(config_.modelDir, config_.pocketDecoder);
85
+ std::string vocabJson = joinPath(config_.modelDir, config_.vocabJson);
86
+ std::string tokenScoresJson = joinPath(config_.modelDir, config_.tokenScoresJson);
87
+
88
+ std::string zipvoiceEncoder = joinPath(config_.modelDir, config_.zipvoiceEncoder);
89
+ std::string zipvoiceDecoder = joinPath(config_.modelDir, config_.zipvoiceDecoder);
90
+
91
+ switch (config_.type) {
92
+ case TtsModelType::KOKORO: {
93
+ const std::string& kokoroModel = config_.model.empty() ? acoustic : model;
94
+ TTS_LOG("Kokoro model: %s", kokoroModel.c_str());
95
+ TTS_LOG("Kokoro voices: %s", voices.c_str());
96
+ TTS_LOG("Kokoro tokens: %s", tokens.c_str());
97
+ TTS_LOG("Kokoro data_dir: %s", espeakNgData.c_str());
98
+ TTS_LOG("Kokoro dict_dir: %s", dictDir.c_str());
99
+ TTS_LOG("Kokoro lexicon: %s", lexicon.c_str());
100
+ checkFile(kokoroModel, "kokoro.model");
101
+ checkFile(voices, "kokoro.voices");
102
+ checkFile(tokens, "kokoro.tokens");
103
+ checkFile(lexicon, "kokoro.lexicon");
104
+ c.model.kokoro.model = kokoroModel.c_str();
105
+ c.model.kokoro.voices = voices.c_str();
106
+ c.model.kokoro.tokens = tokens.c_str();
107
+ c.model.kokoro.data_dir = espeakNgData.c_str();
108
+ c.model.kokoro.dict_dir = dictDir.c_str();
109
+ c.model.kokoro.lexicon = lexicon.c_str();
110
+ break;
111
+ }
112
+ case TtsModelType::VITS: {
113
+ const std::string& vitsModel = config_.model.empty() ? acoustic : model;
114
+ c.model.vits.model = vitsModel.c_str();
115
+ c.model.vits.lexicon = lexicon.c_str();
116
+ c.model.vits.tokens = tokens.c_str();
117
+ c.model.vits.data_dir = espeakNgData.c_str();
118
+ c.model.vits.dict_dir = dictDir.c_str();
119
+ break;
120
+ }
121
+ case TtsModelType::MATCHA:
122
+ c.model.matcha.acoustic_model = acoustic.c_str();
123
+ c.model.matcha.vocoder = vocoder.c_str();
124
+ c.model.matcha.lexicon = lexicon.c_str();
125
+ c.model.matcha.tokens = tokens.c_str();
126
+ c.model.matcha.data_dir = espeakNgData.c_str();
127
+ c.model.matcha.dict_dir = dictDir.c_str();
128
+ break;
129
+ case TtsModelType::POCKET:
130
+ c.model.pocket.lm_main = lmMain.c_str();
131
+ c.model.pocket.lm_flow = lmFlow.c_str();
132
+ c.model.pocket.encoder = pocketEncoder.c_str();
133
+ c.model.pocket.decoder = pocketDecoder.c_str();
134
+ c.model.pocket.text_conditioner = textConditioner.c_str();
135
+ c.model.pocket.vocab_json = vocabJson.c_str();
136
+ c.model.pocket.token_scores_json = tokenScoresJson.c_str();
137
+ break;
138
+ case TtsModelType::ZIPVOICE:
139
+ c.model.zipvoice.tokens = tokens.c_str();
140
+ c.model.zipvoice.encoder = zipvoiceEncoder.c_str();
141
+ c.model.zipvoice.decoder = zipvoiceDecoder.c_str();
142
+ c.model.zipvoice.vocoder = vocoder.c_str();
143
+ c.model.zipvoice.data_dir = espeakNgData.c_str();
144
+ c.model.zipvoice.lexicon = lexicon.c_str();
145
+ break;
146
+ }
147
+
148
+ c.model.num_threads = config_.numThreads;
149
+ c.model.debug = 1;
150
+ c.model.provider = "cpu";
151
+ c.rule_fsts = "";
152
+ c.rule_fars = "";
153
+ c.max_num_sentences = 1;
154
+
155
+ const SherpaOnnxOfflineTts* tts = SherpaOnnxCreateOfflineTts(&c);
156
+ if (tts == nullptr) {
157
+ TTS_LOG("SherpaOnnxCreateOfflineTts returned nullptr (type=%d, modelDir=%s)",
158
+ static_cast<int>(config_.type), config_.modelDir.c_str());
159
+ throw std::runtime_error(
160
+ "Failed to create TTS engine (type=" +
161
+ std::to_string(static_cast<int>(config_.type)) +
162
+ ", modelDir=" + config_.modelDir + ")");
163
+ }
164
+ return std::shared_ptr<const SherpaOnnxOfflineTts>(tts, SherpaOnnxDestroyOfflineTts);
165
+ });
166
+
167
+ tts_ = cached;
168
+ }
169
+
170
+ bool TtsEngine::isLoaded() const {
171
+ return tts_ != nullptr;
172
+ }
173
+
174
+ TtsEngineResult TtsEngine::synthesize(const std::string& text, int32_t speakerId, float speed) {
175
+ if (tts_ == nullptr) {
176
+ throw std::runtime_error("TTS not loaded");
177
+ }
178
+
179
+ const int32_t sid = speakerId >= 0 ? speakerId : config_.speakerId;
180
+ const float playbackSpeed = speed > 0.0f ? speed : config_.speed;
181
+
182
+ SherpaOnnxGenerationConfig genConfig;
183
+ std::memset(&genConfig, 0, sizeof(genConfig));
184
+ genConfig.sid = sid;
185
+ genConfig.speed = playbackSpeed;
186
+
187
+ const SherpaOnnxGeneratedAudio* audio = SherpaOnnxOfflineTtsGenerateWithConfig(
188
+ tts_.get(), text.c_str(), &genConfig, nullptr, nullptr);
189
+ if (audio == nullptr) {
190
+ throw std::runtime_error("TTS synthesis failed");
191
+ }
192
+
193
+ TtsEngineResult result;
194
+ result.sampleRate = audio->sample_rate;
195
+ result.samples.assign(audio->samples, audio->samples + audio->n);
196
+
197
+ if (config_.outputSampleRate > 0 && config_.outputSampleRate != audio->sample_rate) {
198
+ const SherpaOnnxLinearResampler* resampler = SherpaOnnxCreateLinearResampler(
199
+ audio->sample_rate, config_.outputSampleRate, 0.0f, 0);
200
+ if (resampler != nullptr) {
201
+ const SherpaOnnxResampleOut* resampled = SherpaOnnxLinearResamplerResample(
202
+ resampler, result.samples.data(), static_cast<int32_t>(result.samples.size()), 1);
203
+ if (resampled != nullptr) {
204
+ result.samples.assign(resampled->samples, resampled->samples + resampled->n);
205
+ result.sampleRate = config_.outputSampleRate;
206
+ SherpaOnnxLinearResamplerResampleFree(resampled);
207
+ }
208
+ SherpaOnnxDestroyLinearResampler(resampler);
209
+ }
210
+ }
211
+
212
+ result.durationMs = static_cast<double>(result.samples.size()) * 1000.0 / result.sampleRate;
213
+
214
+ SherpaOnnxDestroyOfflineTtsGeneratedAudio(audio);
215
+ return result;
216
+ }
217
+
218
+ void TtsEngine::unload() {
219
+ tts_.reset();
220
+ }
221
+
222
+ } // namespace margelo::nitro::onnx::speech
@@ -0,0 +1,77 @@
1
+ // ------------------------------------------------------------------------------
2
+ // TtsEngine.hpp
3
+ // Text-to-speech wrapper backed by sherpa-onnx OfflineTts.
4
+ // Supports Kokoro, VITS, Matcha, Pocket and ZipVoice (placeholder) models.
5
+ // ------------------------------------------------------------------------------
6
+ #pragma once
7
+
8
+ #include "AudioUtils.hpp"
9
+ #include "ModelSingleton.hpp"
10
+ #include "ThreadPool.hpp"
11
+
12
+ #include "TtsModelType.hpp"
13
+
14
+ #include <memory>
15
+ #include <string>
16
+ #include <vector>
17
+
18
+ struct SherpaOnnxOfflineTts;
19
+
20
+ namespace margelo::nitro::onnx::speech {
21
+
22
+ /** Unified native configuration for any supported TTS model. */
23
+ struct TtsEngineConfig {
24
+ TtsModelType type = TtsModelType::KOKORO;
25
+ std::string modelDir;
26
+ std::string model;
27
+ std::string acousticModel;
28
+ std::string vocoder;
29
+ std::string tokens;
30
+ std::string lexicon;
31
+ std::string voices;
32
+ std::string espeakNgData;
33
+ std::string dictDir;
34
+ std::string lmMain;
35
+ std::string lmFlow;
36
+ std::string textConditioner;
37
+ std::string pocketEncoder;
38
+ std::string pocketDecoder;
39
+ std::string vocabJson;
40
+ std::string tokenScoresJson;
41
+ std::string zipvoiceEncoder;
42
+ std::string zipvoiceDecoder;
43
+ std::string config;
44
+ int32_t numThreads = 2;
45
+ int32_t outputSampleRate = 16000;
46
+ int32_t speakerId = 0;
47
+ float speed = 1.0f;
48
+ };
49
+
50
+ /** Native synthesis result (samples are kept as a float vector). */
51
+ struct TtsEngineResult {
52
+ std::vector<float> samples;
53
+ int32_t sampleRate = 16000;
54
+ double durationMs = 0.0;
55
+ };
56
+
57
+ /** Text-to-speech engine. */
58
+ class TtsEngine final {
59
+ public:
60
+ explicit TtsEngine(std::shared_ptr<ThreadPool> threadPool);
61
+ ~TtsEngine();
62
+
63
+ TtsEngine(const TtsEngine&) = delete;
64
+ TtsEngine& operator=(const TtsEngine&) = delete;
65
+
66
+ void load(const TtsEngineConfig& config);
67
+ bool isLoaded() const;
68
+ TtsEngineResult synthesize(const std::string& text, int32_t speakerId, float speed);
69
+ void unload();
70
+
71
+ private:
72
+ std::shared_ptr<ThreadPool> threadPool_;
73
+ TtsEngineConfig config_;
74
+ std::shared_ptr<const SherpaOnnxOfflineTts> tts_;
75
+ };
76
+
77
+ } // namespace margelo::nitro::onnx::speech
package/cpp/Vad.cpp ADDED
@@ -0,0 +1,126 @@
1
+ // ------------------------------------------------------------------------------
2
+ // Vad.cpp
3
+ // ------------------------------------------------------------------------------
4
+ #include "Vad.hpp"
5
+
6
+ #include "AudioUtils.hpp"
7
+ #include "ResourceDir.hpp"
8
+
9
+ #include <NitroModules/ArrayBuffer.hpp>
10
+ #include <NitroModules/Promise.hpp>
11
+
12
+ namespace margelo::nitro::onnx::speech {
13
+
14
+ namespace {
15
+
16
+ VadSegment toVadSegment(const VadEngineSegment& native) {
17
+ std::vector<uint8_t> bytes = floatVectorToBytes(native.samples);
18
+ return VadSegment(
19
+ static_cast<double>(native.startMs),
20
+ static_cast<double>(native.endMs),
21
+ ArrayBuffer::move(std::move(bytes)));
22
+ }
23
+
24
+ std::vector<VadSegment> toVadSegments(const std::vector<VadEngineSegment>& natives) {
25
+ std::vector<VadSegment> result;
26
+ result.reserve(natives.size());
27
+ for (const auto& native : natives) {
28
+ result.push_back(toVadSegment(native));
29
+ }
30
+ return result;
31
+ }
32
+
33
+ } // namespace
34
+
35
+ Vad::Vad(std::shared_ptr<ThreadPool> threadPool)
36
+ : HybridObject(TAG), engine_(std::move(threadPool)) {}
37
+
38
+ Vad::~Vad() {
39
+ engine_.dispose();
40
+ }
41
+
42
+ std::shared_ptr<Promise<void>> Vad::initialize(const VadConfig& config) {
43
+ currentConfig_ = config;
44
+ return Promise<void>::async([this, config]() {
45
+ // Use the bundled silero_vad.onnx when no custom path is provided.
46
+ const std::string& resDir = getResourceDir();
47
+ const std::string modelPath = config.modelPath.value_or(
48
+ resDir.empty() ? "silero_vad.onnx" : resDir + "/silero_vad.onnx");
49
+ engine_.initialize(
50
+ {
51
+ .modelPath = modelPath,
52
+ .threshold = static_cast<float>(config.threshold.value_or(0.5)),
53
+ .minSilenceDuration = static_cast<float>(config.minSilenceDurationMs.value_or(500.0)) / 1000.0f,
54
+ .minSpeechDuration = static_cast<float>(config.minSpeechDurationMs.value_or(250.0)) / 1000.0f,
55
+ .preBufferMs = static_cast<int32_t>(config.preBufferMs.value_or(300.0)),
56
+ },
57
+ shared_cast<Vad>());
58
+ });
59
+ }
60
+
61
+ bool Vad::isInitialized() {
62
+ return engine_.isInitialized();
63
+ }
64
+
65
+ std::shared_ptr<Promise<void>> Vad::process(const std::shared_ptr<ArrayBuffer>& samples) {
66
+ return Promise<void>::async([this, samples]() {
67
+ const auto data = samples->data();
68
+ auto floatSamples = bytesToFloatVector(data, samples->size());
69
+ engine_.acceptWaveform(floatSamples);
70
+ });
71
+ }
72
+
73
+ std::shared_ptr<Promise<std::vector<VadSegment>>> Vad::pullSegments() {
74
+ return Promise<std::vector<VadSegment>>::async([this]() { return toVadSegments(engine_.pullSegments()); });
75
+ }
76
+
77
+ std::shared_ptr<Promise<void>> Vad::reset() {
78
+ return Promise<void>::async([this]() { engine_.reset(); });
79
+ }
80
+
81
+ std::optional<std::function<void(const VadSegment& /* segment */)>> Vad::getOnSpeechStart() {
82
+ return onSpeechStart_;
83
+ }
84
+
85
+ void Vad::setOnSpeechStart(
86
+ const std::optional<std::function<void(const VadSegment& /* segment */)>>& onSpeechStart) {
87
+ onSpeechStart_ = onSpeechStart;
88
+ }
89
+
90
+ std::optional<std::function<void(const VadSegment& /* segment */)>> Vad::getOnSpeechEnd() {
91
+ return onSpeechEnd_;
92
+ }
93
+
94
+ void Vad::setOnSpeechEnd(
95
+ const std::optional<std::function<void(const VadSegment& /* segment */)>>& onSpeechEnd) {
96
+ onSpeechEnd_ = onSpeechEnd;
97
+ }
98
+
99
+ std::optional<std::function<void(const std::string& /* error */)>> Vad::getOnError() {
100
+ return onError_;
101
+ }
102
+
103
+ void Vad::setOnError(
104
+ const std::optional<std::function<void(const std::string& /* error */)>>& onError) {
105
+ onError_ = onError;
106
+ }
107
+
108
+ void Vad::onSpeechStart(const VadEngineSegment& segment) {
109
+ if (onSpeechStart_.has_value()) {
110
+ onSpeechStart_.value()(toVadSegment(segment));
111
+ }
112
+ }
113
+
114
+ void Vad::onSpeechEnd(const VadEngineSegment& segment) {
115
+ if (onSpeechEnd_.has_value()) {
116
+ onSpeechEnd_.value()(toVadSegment(segment));
117
+ }
118
+ }
119
+
120
+ void Vad::onError(const std::string& error) {
121
+ if (onError_.has_value()) {
122
+ onError_.value()(error);
123
+ }
124
+ }
125
+
126
+ } // namespace margelo::nitro::onnx::speech
package/cpp/Vad.hpp ADDED
@@ -0,0 +1,62 @@
1
+ // ------------------------------------------------------------------------------
2
+ // Vad.hpp
3
+ // Nitro HybridObject implementation for the Vad spec.
4
+ // ------------------------------------------------------------------------------
5
+ #pragma once
6
+
7
+ #include "VadEngine.hpp"
8
+
9
+ #include <NitroModules/Promise.hpp>
10
+ #include <NitroModules/HybridObject.hpp>
11
+ #include <HybridVadSpec.hpp>
12
+
13
+ #include <functional>
14
+ #include <memory>
15
+ #include <optional>
16
+
17
+ namespace margelo::nitro::onnx::speech {
18
+
19
+ /**
20
+ * Bridge between the Nitro Vad spec and the native VadEngine.
21
+ * Events are forwarded to JS through the generated event handler.
22
+ */
23
+ class Vad : public HybridVadSpec,
24
+ public VadListener {
25
+ public:
26
+ static constexpr auto TAG = "Vad";
27
+
28
+ explicit Vad(std::shared_ptr<ThreadPool> threadPool);
29
+ ~Vad() override;
30
+
31
+ std::shared_ptr<Promise<void>> initialize(const VadConfig& config) override;
32
+ bool isInitialized() override;
33
+ std::shared_ptr<Promise<void>> process(const std::shared_ptr<ArrayBuffer>& samples) override;
34
+ std::shared_ptr<Promise<std::vector<VadSegment>>> pullSegments() override;
35
+ std::shared_ptr<Promise<void>> reset() override;
36
+
37
+ // Nitro event callback getters/setters.
38
+ std::optional<std::function<void(const VadSegment& /* segment */)>> getOnSpeechStart() override;
39
+ void setOnSpeechStart(
40
+ const std::optional<std::function<void(const VadSegment& /* segment */)>>& onSpeechStart) override;
41
+ std::optional<std::function<void(const VadSegment& /* segment */)>> getOnSpeechEnd() override;
42
+ void setOnSpeechEnd(
43
+ const std::optional<std::function<void(const VadSegment& /* segment */)>>& onSpeechEnd) override;
44
+ std::optional<std::function<void(const std::string& /* error */)>> getOnError() override;
45
+ void setOnError(
46
+ const std::optional<std::function<void(const std::string& /* error */)>>& onError) override;
47
+
48
+ // VadListener callbacks run on the background processor thread and forward
49
+ // into JS via the stored Nitro callbacks.
50
+ void onSpeechStart(const VadEngineSegment& segment) override;
51
+ void onSpeechEnd(const VadEngineSegment& segment) override;
52
+ void onError(const std::string& error) override;
53
+
54
+ private:
55
+ VadEngine engine_;
56
+ VadConfig currentConfig_;
57
+ std::optional<std::function<void(const VadSegment&)>> onSpeechStart_;
58
+ std::optional<std::function<void(const VadSegment&)>> onSpeechEnd_;
59
+ std::optional<std::function<void(const std::string&)>> onError_;
60
+ };
61
+
62
+ } // namespace margelo::nitro::onnx::speech
@@ -0,0 +1,217 @@
1
+ // ------------------------------------------------------------------------------
2
+ // VadEngine.cpp
3
+ // ------------------------------------------------------------------------------
4
+ #include "VadEngine.hpp"
5
+
6
+ #include "sherpa-onnx/c-api/c-api.h"
7
+
8
+ #include <algorithm>
9
+ #include <cstring>
10
+ #include <stdexcept>
11
+
12
+ namespace margelo::nitro::onnx::speech {
13
+
14
+ namespace {
15
+
16
+ constexpr int32_t kSampleRate = 16000;
17
+
18
+ } // namespace
19
+
20
+ VadEngine::VadEngine(std::shared_ptr<ThreadPool> threadPool)
21
+ : threadPool_(std::move(threadPool)) {}
22
+
23
+ VadEngine::~VadEngine() {
24
+ dispose();
25
+ }
26
+
27
+ void VadEngine::initialize(const VadEngineConfig& config, std::shared_ptr<VadListener> listener) {
28
+ dispose();
29
+
30
+ config_ = config;
31
+ listener_ = std::move(listener);
32
+ preBufferCapacity_ = static_cast<size_t>(msToSamples(config_.preBufferMs));
33
+ preBuffer_.reserve(preBufferCapacity_);
34
+ streamMs_ = 0.0f;
35
+ inSpeech_ = false;
36
+
37
+ SherpaOnnxVadModelConfig vadConfig;
38
+ std::memset(&vadConfig, 0, sizeof(vadConfig));
39
+ vadConfig.silero_vad.model = config_.modelPath.c_str();
40
+ vadConfig.silero_vad.threshold = config_.threshold;
41
+ vadConfig.silero_vad.min_silence_duration = config_.minSilenceDuration;
42
+ vadConfig.silero_vad.min_speech_duration = config_.minSpeechDuration;
43
+ vadConfig.sample_rate = kSampleRate;
44
+ vadConfig.num_threads = 1;
45
+
46
+ // The second argument is the buffer size in milliseconds used internally by
47
+ // sherpa-onnx. We reuse the configured pre-buffer duration.
48
+ vad_ = SherpaOnnxCreateVoiceActivityDetector(&vadConfig, config_.preBufferMs);
49
+ if (vad_ == nullptr) {
50
+ throw std::runtime_error("Failed to create sherpa-onnx VAD");
51
+ }
52
+
53
+ initialized_ = true;
54
+ stop_ = false;
55
+ processorThread_ = std::thread(&VadEngine::processLoop, this);
56
+ }
57
+
58
+ bool VadEngine::isInitialized() const {
59
+ return initialized_.load();
60
+ }
61
+
62
+ void VadEngine::acceptWaveform(const std::vector<float>& samples) {
63
+ {
64
+ std::lock_guard<std::mutex> lock(inputMutex_);
65
+ inputQueue_.emplace(samples);
66
+ }
67
+ inputCv_.notify_one();
68
+ }
69
+
70
+ std::vector<VadEngineSegment> VadEngine::pullSegments() {
71
+ std::lock_guard<std::mutex> lock(outputMutex_);
72
+ std::vector<VadEngineSegment> result = std::move(pendingSegments_);
73
+ pendingSegments_.clear();
74
+ return result;
75
+ }
76
+
77
+ void VadEngine::reset() {
78
+ {
79
+ std::lock_guard<std::mutex> lock(inputMutex_);
80
+ std::queue<std::vector<float>> empty;
81
+ inputQueue_.swap(empty);
82
+ }
83
+ {
84
+ std::lock_guard<std::mutex> lock(preBufferMutex_);
85
+ preBuffer_.clear();
86
+ }
87
+ {
88
+ std::lock_guard<std::mutex> lock(outputMutex_);
89
+ pendingSegments_.clear();
90
+ }
91
+
92
+ if (vad_) {
93
+ SherpaOnnxVoiceActivityDetectorClear(vad_);
94
+ }
95
+
96
+ // Defer speech-state clearing to the processor thread to avoid a data race.
97
+ resetRequested_ = true;
98
+ }
99
+
100
+ void VadEngine::dispose() {
101
+ stop_ = true;
102
+ inputCv_.notify_all();
103
+ if (processorThread_.joinable()) {
104
+ processorThread_.join();
105
+ }
106
+ if (vad_) {
107
+ SherpaOnnxDestroyVoiceActivityDetector(vad_);
108
+ vad_ = nullptr;
109
+ }
110
+ initialized_ = false;
111
+ listener_.reset();
112
+ }
113
+
114
+ void VadEngine::processLoop() {
115
+ while (!stop_) {
116
+ std::vector<float> chunk;
117
+ {
118
+ std::unique_lock<std::mutex> lock(inputMutex_);
119
+ inputCv_.wait(lock, [this]() { return stop_ || !inputQueue_.empty(); });
120
+ if (stop_) {
121
+ break;
122
+ }
123
+ chunk = std::move(inputQueue_.front());
124
+ inputQueue_.pop();
125
+ }
126
+
127
+ if (chunk.empty()) {
128
+ continue;
129
+ }
130
+
131
+ if (resetRequested_.exchange(false)) {
132
+ inSpeech_ = false;
133
+ currentSpeechSamples_.clear();
134
+ streamMs_ = 0.0f;
135
+ }
136
+
137
+ // Update the sliding pre-buffer with the newest chunk. This is the key
138
+ // mechanism that preserves leading audio for onSpeechStart/onSpeechEnd.
139
+ {
140
+ std::lock_guard<std::mutex> lock(preBufferMutex_);
141
+ preBuffer_.insert(preBuffer_.end(), chunk.begin(), chunk.end());
142
+ if (preBuffer_.size() > preBufferCapacity_) {
143
+ const size_t excess = preBuffer_.size() - preBufferCapacity_;
144
+ preBuffer_.erase(preBuffer_.begin(), preBuffer_.begin() + excess);
145
+ }
146
+ }
147
+
148
+ // Feed audio to sherpa-onnx on the background thread.
149
+ SherpaOnnxVoiceActivityDetectorAcceptWaveform(vad_, chunk.data(), static_cast<int32_t>(chunk.size()));
150
+ streamMs_ += samplesToMs(static_cast<int32_t>(chunk.size()));
151
+
152
+ const bool speechDetected = SherpaOnnxVoiceActivityDetectorDetected(vad_) != 0;
153
+
154
+ if (speechDetected && !inSpeech_) {
155
+ // Transition to speech: capture the sliding pre-buffer as the start of
156
+ // the segment and notify JS immediately so the UI can react without
157
+ // waiting for speech end.
158
+ inSpeech_ = true;
159
+ currentSpeechStartMs_ = streamMs_ - samplesToMs(static_cast<int32_t>(chunk.size()));
160
+ {
161
+ std::lock_guard<std::mutex> lock(preBufferMutex_);
162
+ currentSpeechSamples_ = preBuffer_;
163
+ }
164
+
165
+ VadEngineSegment startSegment;
166
+ startSegment.startMs = currentSpeechStartMs_ - samplesToMs(static_cast<int32_t>(currentSpeechSamples_.size()));
167
+ startSegment.endMs = currentSpeechStartMs_;
168
+ startSegment.samples = currentSpeechSamples_;
169
+ if (listener_) {
170
+ listener_->onSpeechStart(startSegment);
171
+ }
172
+ }
173
+
174
+ else if (inSpeech_) {
175
+ // Accumulate subsequent chunks into the active speech buffer.
176
+ // (On the iteration where speech is first detected, the pre-buffer
177
+ // already contains the triggering chunk, so we skip it here.)
178
+ currentSpeechSamples_.insert(currentSpeechSamples_.end(), chunk.begin(), chunk.end());
179
+ }
180
+
181
+ // Drain completed segments from sherpa-onnx. The segment returned by Front
182
+ // does not include our pre-buffer, so we emit the accumulated buffer
183
+ // instead. Timing is taken from our stream clock for consistency.
184
+ while (!SherpaOnnxVoiceActivityDetectorEmpty(vad_)) {
185
+ const SherpaOnnxSpeechSegment* seg = SherpaOnnxVoiceActivityDetectorFront(vad_);
186
+ if (seg == nullptr) {
187
+ break;
188
+ }
189
+
190
+ VadEngineSegment endSegment;
191
+ endSegment.startMs = currentSpeechStartMs_;
192
+ endSegment.endMs = streamMs_;
193
+ endSegment.samples = std::move(currentSpeechSamples_);
194
+
195
+ SherpaOnnxDestroySpeechSegment(seg);
196
+ SherpaOnnxVoiceActivityDetectorPop(vad_);
197
+
198
+ {
199
+ std::lock_guard<std::mutex> lock(outputMutex_);
200
+ pendingSegments_.push_back(endSegment);
201
+ }
202
+ if (listener_) {
203
+ listener_->onSpeechEnd(endSegment);
204
+ }
205
+
206
+ inSpeech_ = false;
207
+ }
208
+ }
209
+ }
210
+
211
+ void VadEngine::flushPreBuffer(std::vector<float>& target) {
212
+ std::lock_guard<std::mutex> lock(preBufferMutex_);
213
+ target.insert(target.end(), preBuffer_.begin(), preBuffer_.end());
214
+ preBuffer_.clear();
215
+ }
216
+
217
+ } // namespace margelo::nitro::onnx::speech