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.
- package/NOTICE +241 -0
- package/NitroOnnxSpeech.podspec +51 -0
- package/Package.swift +52 -0
- package/README.md +398 -0
- package/android/CMakeLists.txt +117 -0
- package/android/build.gradle +65 -0
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/assets/silero_vad.onnx +0 -0
- package/android/src/main/cpp/cpp-adapter.cpp +24 -0
- package/android/src/main/java/com/margelo/nitro/onnx/speech/OnnxSpeechPackage.kt +67 -0
- package/assets/silero_vad.onnx +0 -0
- package/cpp/AndroidPthreadCompat.cpp +16 -0
- package/cpp/AsrEngine.cpp +277 -0
- package/cpp/AsrEngine.hpp +110 -0
- package/cpp/AudioFileReader.cpp +177 -0
- package/cpp/AudioFileReader.hpp +26 -0
- package/cpp/AudioUtils.cpp +28 -0
- package/cpp/AudioUtils.hpp +38 -0
- package/cpp/ModelSingleton.hpp +51 -0
- package/cpp/NitroOnnxSpeech.cpp +68 -0
- package/cpp/NitroOnnxSpeech.hpp +42 -0
- package/cpp/OfflineAsr.cpp +81 -0
- package/cpp/OfflineAsr.hpp +33 -0
- package/cpp/ResourceDir.cpp +29 -0
- package/cpp/ResourceDir.hpp +18 -0
- package/cpp/SpeakerEngine.cpp +170 -0
- package/cpp/SpeakerEngine.hpp +70 -0
- package/cpp/SpeakerManager.cpp +93 -0
- package/cpp/SpeakerManager.hpp +43 -0
- package/cpp/StreamingAsr.cpp +123 -0
- package/cpp/StreamingAsr.hpp +55 -0
- package/cpp/ThreadPool.cpp +41 -0
- package/cpp/ThreadPool.hpp +62 -0
- package/cpp/Tts.cpp +132 -0
- package/cpp/Tts.hpp +38 -0
- package/cpp/TtsEngine.cpp +222 -0
- package/cpp/TtsEngine.hpp +77 -0
- package/cpp/Vad.cpp +126 -0
- package/cpp/Vad.hpp +62 -0
- package/cpp/VadEngine.cpp +217 -0
- package/cpp/VadEngine.hpp +126 -0
- package/ios/OnnxSpeechInitializer.mm +37 -0
- package/ios/PrivacyInfo.xcprivacy +14 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +19 -0
- package/lib/index.js.map +1 -0
- package/lib/specs/OnnxSpeech.nitro.d.ts +264 -0
- package/lib/specs/OnnxSpeech.nitro.d.ts.map +1 -0
- package/lib/specs/OnnxSpeech.nitro.js +6 -0
- package/lib/specs/OnnxSpeech.nitro.js.map +1 -0
- package/nitro.json +19 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroOnnxSpeech+autolinking.cmake +86 -0
- package/nitrogen/generated/android/NitroOnnxSpeech+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroOnnxSpeechOnLoad.cpp +49 -0
- package/nitrogen/generated/android/NitroOnnxSpeechOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onnx/speech/NitroOnnxSpeechOnLoad.kt +35 -0
- package/nitrogen/generated/ios/NitroOnnxSpeech+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroOnnxSpeech-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/NitroOnnxSpeech-Swift-Cxx-Bridge.hpp +27 -0
- package/nitrogen/generated/ios/NitroOnnxSpeech-Swift-Cxx-Umbrella.hpp +38 -0
- package/nitrogen/generated/ios/NitroOnnxSpeechAutolinking.mm +35 -0
- package/nitrogen/generated/ios/NitroOnnxSpeechAutolinking.swift +16 -0
- package/nitrogen/generated/shared/c++/AsrModelConfig.hpp +142 -0
- package/nitrogen/generated/shared/c++/AsrModelType.hpp +112 -0
- package/nitrogen/generated/shared/c++/AsrResult.hpp +105 -0
- package/nitrogen/generated/shared/c++/HybridOfflineAsrSpec.cpp +25 -0
- package/nitrogen/generated/shared/c++/HybridOfflineAsrSpec.hpp +73 -0
- package/nitrogen/generated/shared/c++/HybridOnnxSpeechSpec.cpp +27 -0
- package/nitrogen/generated/shared/c++/HybridOnnxSpeechSpec.hpp +82 -0
- package/nitrogen/generated/shared/c++/HybridSpeakerManagerSpec.cpp +28 -0
- package/nitrogen/generated/shared/c++/HybridSpeakerManagerSpec.hpp +77 -0
- package/nitrogen/generated/shared/c++/HybridStreamingAsrSpec.cpp +32 -0
- package/nitrogen/generated/shared/c++/HybridStreamingAsrSpec.hpp +81 -0
- package/nitrogen/generated/shared/c++/HybridTtsSpec.cpp +26 -0
- package/nitrogen/generated/shared/c++/HybridTtsSpec.hpp +74 -0
- package/nitrogen/generated/shared/c++/HybridVadSpec.cpp +31 -0
- package/nitrogen/generated/shared/c++/HybridVadSpec.hpp +81 -0
- package/nitrogen/generated/shared/c++/RegisteredSpeaker.hpp +91 -0
- package/nitrogen/generated/shared/c++/SpeakerEmbeddingConfig.hpp +91 -0
- package/nitrogen/generated/shared/c++/TtsModelConfig.hpp +178 -0
- package/nitrogen/generated/shared/c++/TtsModelType.hpp +88 -0
- package/nitrogen/generated/shared/c++/TtsResult.hpp +91 -0
- package/nitrogen/generated/shared/c++/VadConfig.hpp +100 -0
- package/nitrogen/generated/shared/c++/VadSegment.hpp +91 -0
- package/package.json +62 -0
- package/scripts/prepare-sherpa-onnx.js +236 -0
- package/scripts/test-cpp.js +24 -0
- package/src/index.ts +45 -0
- 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
|