Committed by
GitHub
Begin to add node-addon-api for sherpa-onnx (#826)
正在显示
12 个修改的文件
包含
838 行增加
和
0 行删除
.github/workflows/test-nodejs-addon-api.yaml
0 → 100644
| 1 | +name: test-node-addon-api | ||
| 2 | + | ||
| 3 | +on: | ||
| 4 | + push: | ||
| 5 | + branches: | ||
| 6 | + - master | ||
| 7 | + paths: | ||
| 8 | + - '.github/workflows/test-node-addon-api.yaml' | ||
| 9 | + - 'CMakeLists.txt' | ||
| 10 | + - 'cmake/**' | ||
| 11 | + - 'sherpa-onnx/csrc/*' | ||
| 12 | + - 'sherpa-onnx/c-api/*' | ||
| 13 | + - 'scripts/node-addon-api/**' | ||
| 14 | + | ||
| 15 | + pull_request: | ||
| 16 | + branches: | ||
| 17 | + - master | ||
| 18 | + paths: | ||
| 19 | + - '.github/workflows/test-node-addon-api.yaml' | ||
| 20 | + - 'CMakeLists.txt' | ||
| 21 | + - 'cmake/**' | ||
| 22 | + - 'sherpa-onnx/csrc/*' | ||
| 23 | + - 'sherpa-onnx/c-api/*' | ||
| 24 | + - 'scripts/node-addon-api/**' | ||
| 25 | + | ||
| 26 | + workflow_dispatch: | ||
| 27 | + | ||
| 28 | +concurrency: | ||
| 29 | + group: test-node-addon-api-${{ github.ref }} | ||
| 30 | + cancel-in-progress: true | ||
| 31 | + | ||
| 32 | +permissions: | ||
| 33 | + contents: read | ||
| 34 | + | ||
| 35 | +jobs: | ||
| 36 | + test-node-addon-api: | ||
| 37 | + name: ${{ matrix.os }} | ||
| 38 | + runs-on: ${{ matrix.os }} | ||
| 39 | + strategy: | ||
| 40 | + fail-fast: false | ||
| 41 | + matrix: | ||
| 42 | + os: [macos-latest, macos-14] | ||
| 43 | + python-version: ["3.8"] | ||
| 44 | + | ||
| 45 | + steps: | ||
| 46 | + - uses: actions/checkout@v4 | ||
| 47 | + with: | ||
| 48 | + fetch-depth: 0 | ||
| 49 | + | ||
| 50 | + - name: Setup Python ${{ matrix.python-version }} | ||
| 51 | + uses: actions/setup-python@v5 | ||
| 52 | + with: | ||
| 53 | + python-version: ${{ matrix.python-version }} | ||
| 54 | + | ||
| 55 | + - uses: actions/setup-node@v4 | ||
| 56 | + with: | ||
| 57 | + registry-url: 'https://registry.npmjs.org' | ||
| 58 | + | ||
| 59 | + - name: Display node version | ||
| 60 | + shell: bash | ||
| 61 | + run: | | ||
| 62 | + node --version | ||
| 63 | + | ||
| 64 | + - name: ccache | ||
| 65 | + uses: hendrikmuhs/ccache-action@v1.2 | ||
| 66 | + with: | ||
| 67 | + key: ${{ matrix.os }}-release-shared | ||
| 68 | + | ||
| 69 | + - name: Build sherpa-onnx | ||
| 70 | + shell: bash | ||
| 71 | + run: | | ||
| 72 | + mkdir build | ||
| 73 | + cd build | ||
| 74 | + cmake -DCMAKE_INSTALL_PREFIX=/tmp/sherpa-onnx -DBUILD_SHARED_LIBS=ON .. | ||
| 75 | + make -j | ||
| 76 | + make install | ||
| 77 | + | ||
| 78 | + - name: Build node-addon-api package | ||
| 79 | + shell: bash | ||
| 80 | + run: | | ||
| 81 | + cd scripts/node-addon-api | ||
| 82 | + | ||
| 83 | + export PKG_CONFIG_PATH=/tmp/sherpa-onnx:$PKG_CONFIG_PATH | ||
| 84 | + | ||
| 85 | + ls -lh /tmp/sherpa-onnx | ||
| 86 | + | ||
| 87 | + pkg-config --cflags sherpa-onnx | ||
| 88 | + pkg-config --libs sherpa-onnx | ||
| 89 | + | ||
| 90 | + a=$(pkg-config --cflags sherpa-onnx);a=${a:2};echo $a | ||
| 91 | + | ||
| 92 | + npm i | ||
| 93 | + | ||
| 94 | + ./node_modules/.bin/node-gyp configure build --verbose | ||
| 95 | + | ||
| 96 | + - name: Test streaming transducer | ||
| 97 | + shell: bash | ||
| 98 | + run: | | ||
| 99 | + cd scripts/node-addon-api | ||
| 100 | + curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20.tar.bz2 | ||
| 101 | + tar xvf sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20.tar.bz2 | ||
| 102 | + rm sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20.tar.bz2 | ||
| 103 | + | ||
| 104 | + node test/test_asr_streaming_transducer.js | ||
| 105 | + | ||
| 106 | + rm -rf sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20 |
scripts/node-addon-api/README.md
0 → 100644
scripts/node-addon-api/binding.gyp
0 → 100644
| 1 | +{ | ||
| 2 | + 'targets': [ | ||
| 3 | + { | ||
| 4 | + 'target_name': 'sherpa-onnx-node-addon-api-native', | ||
| 5 | + 'sources': [ | ||
| 6 | + 'src/sherpa-onnx-node-addon-api.cc', | ||
| 7 | + 'src/streaming-asr.cc', | ||
| 8 | + 'src/wave-reader.cc' | ||
| 9 | + ], | ||
| 10 | + 'include_dirs': [ | ||
| 11 | + "<!@(node -p \"require('node-addon-api').include\")", | ||
| 12 | + "<!@(a=$(pkg-config --cflags sherpa-onnx);echo ${a:2})" | ||
| 13 | + ], | ||
| 14 | + 'dependencies': ["<!(node -p \"require('node-addon-api').gyp\")"], | ||
| 15 | + 'cflags!': [ | ||
| 16 | + '-fno-exceptions', | ||
| 17 | + ], | ||
| 18 | + 'cflags_cc!': [ | ||
| 19 | + '-fno-exceptions', | ||
| 20 | + '-std=c++17' | ||
| 21 | + ], | ||
| 22 | + 'libraries': [ | ||
| 23 | + "<!@(pkg-config --libs sherpa-onnx)" | ||
| 24 | + ], | ||
| 25 | + 'xcode_settings': { | ||
| 26 | + 'GCC_ENABLE_CPP_EXCEPTIONS': 'YES', | ||
| 27 | + 'CLANG_CXX_LIBRARY': 'libc++', | ||
| 28 | + 'MACOSX_DEPLOYMENT_TARGET': '10.7' | ||
| 29 | + }, | ||
| 30 | + 'msvs_settings': { | ||
| 31 | + 'VCCLCompilerTool': { 'ExceptionHandling': 1 }, | ||
| 32 | + } | ||
| 33 | + } | ||
| 34 | + ] | ||
| 35 | +} |
scripts/node-addon-api/lib/sherpa-onnx.js
0 → 100644
scripts/node-addon-api/lib/streaming-asr.js
0 → 100644
| 1 | +const addon = require('bindings')('sherpa-onnx-node-addon-api-native'); | ||
| 2 | + | ||
| 3 | +class OnlineStream { | ||
| 4 | + constructor(handle) { | ||
| 5 | + this.handle = handle; | ||
| 6 | + } | ||
| 7 | + | ||
| 8 | + // samples is a float32 array containing samples in the range [-1, 1] | ||
| 9 | + acceptWaveform(samples, sampleRate) { | ||
| 10 | + addon.acceptWaveformOnline( | ||
| 11 | + this.handle, {samples: samples, sampleRate: sampleRate}) | ||
| 12 | + } | ||
| 13 | +} | ||
| 14 | + | ||
| 15 | +class OnlineRecognizer { | ||
| 16 | + constructor(config) { | ||
| 17 | + this.handle = addon.createOnlineRecognizer(config); | ||
| 18 | + } | ||
| 19 | + | ||
| 20 | + createStream() { | ||
| 21 | + const handle = addon.createOnlineStream(this.handle); | ||
| 22 | + return new OnlineStream(handle); | ||
| 23 | + } | ||
| 24 | + | ||
| 25 | + isReady(stream) { | ||
| 26 | + return addon.isOnlineStreamReady(this.handle, stream.handle); | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + decode(stream) { | ||
| 30 | + addon.decodeOnlineStream(this.handle, stream.handle); | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + getResult(stream) { | ||
| 34 | + const jsonStr = | ||
| 35 | + addon.getOnlineStreamResultAsJson(this.handle, stream.handle); | ||
| 36 | + | ||
| 37 | + return JSON.parse(jsonStr); | ||
| 38 | + } | ||
| 39 | +} | ||
| 40 | + | ||
| 41 | +module.exports = {OnlineRecognizer} |
scripts/node-addon-api/package.json
0 → 100644
| 1 | +{ | ||
| 2 | + "main": "lib/sherpa-onnx.js", | ||
| 3 | + "version": "1.0.0", | ||
| 4 | + "description": "Speech-to-text and text-to-speech using Next-gen Kaldi without internet connection", | ||
| 5 | + "dependencies": { | ||
| 6 | + "node-addon-api": "^1.1.0", | ||
| 7 | + "bindings": "^1.5.0", | ||
| 8 | + "node-gyp": "^10.1.0" | ||
| 9 | + }, | ||
| 10 | + "scripts": { | ||
| 11 | + "test": "node --napi-modules ./test/test_binding.js" | ||
| 12 | + }, | ||
| 13 | + "repository": { | ||
| 14 | + "type": "git", | ||
| 15 | + "url": "git+https://github.com/k2-fsa/sherpa-onnx.git" | ||
| 16 | + }, | ||
| 17 | + "keywords": [ | ||
| 18 | + "speech to text", | ||
| 19 | + "text to speech", | ||
| 20 | + "transcription", | ||
| 21 | + "real-time speech recognition", | ||
| 22 | + "without internet connection", | ||
| 23 | + "embedded systems", | ||
| 24 | + "open source", | ||
| 25 | + "zipformer", | ||
| 26 | + "asr", | ||
| 27 | + "tts", | ||
| 28 | + "stt", | ||
| 29 | + "c++", | ||
| 30 | + "onnxruntime", | ||
| 31 | + "onnx", | ||
| 32 | + "ai", | ||
| 33 | + "next-gen kaldi", | ||
| 34 | + "offline", | ||
| 35 | + "privacy", | ||
| 36 | + "open source", | ||
| 37 | + "streaming speech recognition", | ||
| 38 | + "speech", | ||
| 39 | + "recognition", | ||
| 40 | + "vad", | ||
| 41 | + "node-addon-api", | ||
| 42 | + "speaker id", | ||
| 43 | + "language id" | ||
| 44 | + ], | ||
| 45 | + "author": "The next-gen Kaldi team", | ||
| 46 | + "license": "Apache-2.0", | ||
| 47 | + "gypfile": true, | ||
| 48 | + "name": "sherpa-onnx-node-addon-api", | ||
| 49 | + "bugs": { | ||
| 50 | + "url": "https://github.com/k2-fsa/sherpa-onnx/issues" | ||
| 51 | + }, | ||
| 52 | + "homepage": "https://github.com/k2-fsa/sherpa-onnx#readme" | ||
| 53 | +} |
| 1 | +// scripts/node-addon-api/src/sherpa-onnx-node-addon-api.cc | ||
| 2 | +// | ||
| 3 | +// Copyright (c) 2024 Xiaomi Corporation | ||
| 4 | +#include "napi.h" // NOLINT | ||
| 5 | + | ||
| 6 | +Napi::Object InitStreamingAsr(Napi::Env env, Napi::Object exports); | ||
| 7 | +void InitWaveReader(Napi::Env env, Napi::Object exports); | ||
| 8 | + | ||
| 9 | +Napi::Object Init(Napi::Env env, Napi::Object exports) { | ||
| 10 | + InitStreamingAsr(env, exports); | ||
| 11 | + InitWaveReader(env, exports); | ||
| 12 | + | ||
| 13 | + return exports; | ||
| 14 | +} | ||
| 15 | + | ||
| 16 | +NODE_API_MODULE(addon, Init) |
scripts/node-addon-api/src/streaming-asr.cc
0 → 100644
| 1 | +// scripts/node-addon-api/src/streaming-asr.cc | ||
| 2 | +// | ||
| 3 | +// Copyright (c) 2024 Xiaomi Corporation | ||
| 4 | +#include <sstream> | ||
| 5 | + | ||
| 6 | +#include "napi.h" // NOLINT | ||
| 7 | +#include "sherpa-onnx/c-api/c-api.h" | ||
| 8 | +/* | ||
| 9 | +{ | ||
| 10 | + 'featConfig': { | ||
| 11 | + 'sampleRate': 16000, | ||
| 12 | + 'featureDim': 80, | ||
| 13 | + } | ||
| 14 | +}; | ||
| 15 | + */ | ||
| 16 | +static SherpaOnnxFeatureConfig GetFeatureConfig(Napi::Object obj) { | ||
| 17 | + SherpaOnnxFeatureConfig config; | ||
| 18 | + memset(&config, 0, sizeof(config)); | ||
| 19 | + | ||
| 20 | + if (!obj.Has("featConfig") || !obj.Get("featConfig").IsObject()) { | ||
| 21 | + return config; | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + Napi::Object featConfig = obj.Get("featConfig").As<Napi::Object>(); | ||
| 25 | + | ||
| 26 | + if (featConfig.Has("sampleRate") && featConfig.Get("sampleRate").IsNumber()) { | ||
| 27 | + config.sample_rate = | ||
| 28 | + featConfig.Get("sampleRate").As<Napi::Number>().Int32Value(); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + if (featConfig.Has("featureDim") && featConfig.Get("featureDim").IsNumber()) { | ||
| 32 | + config.feature_dim = | ||
| 33 | + featConfig.Get("featureDim").As<Napi::Number>().Int32Value(); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + return config; | ||
| 37 | +} | ||
| 38 | +/* | ||
| 39 | +{ | ||
| 40 | + 'transducer': { | ||
| 41 | + 'encoder': './encoder.onnx', | ||
| 42 | + 'decoder': './decoder.onnx', | ||
| 43 | + 'joiner': './joiner.onnx', | ||
| 44 | + } | ||
| 45 | +} | ||
| 46 | + */ | ||
| 47 | + | ||
| 48 | +static SherpaOnnxOnlineTransducerModelConfig GetOnlineTransducerModelConfig( | ||
| 49 | + Napi::Object obj) { | ||
| 50 | + SherpaOnnxOnlineTransducerModelConfig config; | ||
| 51 | + memset(&config, 0, sizeof(config)); | ||
| 52 | + | ||
| 53 | + if (!obj.Has("transducer") || !obj.Get("transducer").IsObject()) { | ||
| 54 | + return config; | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + Napi::Object o = obj.Get("transducer").As<Napi::Object>(); | ||
| 58 | + | ||
| 59 | + if (o.Has("encoder") && o.Get("encoder").IsString()) { | ||
| 60 | + Napi::String encoder = o.Get("encoder").As<Napi::String>(); | ||
| 61 | + std::string s = encoder.Utf8Value(); | ||
| 62 | + char *p = new char[s.size() + 1]; | ||
| 63 | + std::copy(s.begin(), s.end(), p); | ||
| 64 | + p[s.size()] = 0; | ||
| 65 | + | ||
| 66 | + config.encoder = p; | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + if (o.Has("decoder") && o.Get("decoder").IsString()) { | ||
| 70 | + Napi::String decoder = o.Get("decoder").As<Napi::String>(); | ||
| 71 | + std::string s = decoder.Utf8Value(); | ||
| 72 | + char *p = new char[s.size() + 1]; | ||
| 73 | + std::copy(s.begin(), s.end(), p); | ||
| 74 | + p[s.size()] = 0; | ||
| 75 | + | ||
| 76 | + config.decoder = p; | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + if (o.Has("joiner") && o.Get("joiner").IsString()) { | ||
| 80 | + Napi::String joiner = o.Get("joiner").As<Napi::String>(); | ||
| 81 | + std::string s = joiner.Utf8Value(); | ||
| 82 | + char *p = new char[s.size() + 1]; | ||
| 83 | + std::copy(s.begin(), s.end(), p); | ||
| 84 | + p[s.size()] = 0; | ||
| 85 | + | ||
| 86 | + config.joiner = p; | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + return config; | ||
| 90 | +} | ||
| 91 | + | ||
| 92 | +static SherpaOnnxOnlineModelConfig GetOnlineModelConfig(Napi::Object obj) { | ||
| 93 | + SherpaOnnxOnlineModelConfig config; | ||
| 94 | + memset(&config, 0, sizeof(config)); | ||
| 95 | + | ||
| 96 | + if (!obj.Has("modelConfig") || !obj.Get("modelConfig").IsObject()) { | ||
| 97 | + return config; | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + Napi::Object o = obj.Get("modelConfig").As<Napi::Object>(); | ||
| 101 | + | ||
| 102 | + config.transducer = GetOnlineTransducerModelConfig(o); | ||
| 103 | + | ||
| 104 | + if (o.Has("tokens") && o.Get("tokens").IsString()) { | ||
| 105 | + Napi::String tokens = o.Get("tokens").As<Napi::String>(); | ||
| 106 | + std::string s = tokens.Utf8Value(); | ||
| 107 | + char *p = new char[s.size() + 1]; | ||
| 108 | + std::copy(s.begin(), s.end(), p); | ||
| 109 | + p[s.size()] = 0; | ||
| 110 | + | ||
| 111 | + config.tokens = p; | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + if (o.Has("numThreads") && o.Get("numThreads").IsNumber()) { | ||
| 115 | + config.num_threads = o.Get("numThreads").As<Napi::Number>().Int32Value(); | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + if (o.Has("provider") && o.Get("provider").IsString()) { | ||
| 119 | + Napi::String provider = o.Get("provider").As<Napi::String>(); | ||
| 120 | + std::string s = provider.Utf8Value(); | ||
| 121 | + char *p = new char[s.size() + 1]; | ||
| 122 | + std::copy(s.begin(), s.end(), p); | ||
| 123 | + p[s.size()] = 0; | ||
| 124 | + | ||
| 125 | + config.provider = p; | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + if (o.Has("debug") && o.Get("debug").IsNumber()) { | ||
| 129 | + config.debug = o.Get("debug").As<Napi::Number>().Int32Value(); | ||
| 130 | + } | ||
| 131 | + | ||
| 132 | + if (o.Has("modelType") && o.Get("modelType").IsString()) { | ||
| 133 | + Napi::String model_type = o.Get("modelType").As<Napi::String>(); | ||
| 134 | + std::string s = model_type.Utf8Value(); | ||
| 135 | + char *p = new char[s.size() + 1]; | ||
| 136 | + std::copy(s.begin(), s.end(), p); | ||
| 137 | + p[s.size()] = 0; | ||
| 138 | + | ||
| 139 | + config.model_type = p; | ||
| 140 | + } | ||
| 141 | + | ||
| 142 | + return config; | ||
| 143 | +} | ||
| 144 | + | ||
| 145 | +static Napi::External<SherpaOnnxOnlineRecognizer> CreateOnlineRecognizerWrapper( | ||
| 146 | + const Napi::CallbackInfo &info) { | ||
| 147 | + Napi::Env env = info.Env(); | ||
| 148 | + if (info.Length() != 1) { | ||
| 149 | + std::ostringstream os; | ||
| 150 | + os << "Expect only 1 argument. Given: " << info.Length(); | ||
| 151 | + | ||
| 152 | + Napi::TypeError::New(env, os.str()).ThrowAsJavaScriptException(); | ||
| 153 | + | ||
| 154 | + return {}; | ||
| 155 | + } | ||
| 156 | + | ||
| 157 | + if (!info[0].IsObject()) { | ||
| 158 | + Napi::TypeError::New(env, "Expect an object as the argument") | ||
| 159 | + .ThrowAsJavaScriptException(); | ||
| 160 | + | ||
| 161 | + return {}; | ||
| 162 | + } | ||
| 163 | + | ||
| 164 | + Napi::Object config = info[0].As<Napi::Object>(); | ||
| 165 | + SherpaOnnxOnlineRecognizerConfig c; | ||
| 166 | + memset(&c, 0, sizeof(c)); | ||
| 167 | + c.feat_config = GetFeatureConfig(config); | ||
| 168 | + c.model_config = GetOnlineModelConfig(config); | ||
| 169 | +#if 0 | ||
| 170 | + printf("encoder: %s\n", c.model_config.transducer.encoder | ||
| 171 | + ? c.model_config.transducer.encoder | ||
| 172 | + : "no"); | ||
| 173 | + printf("decoder: %s\n", c.model_config.transducer.decoder | ||
| 174 | + ? c.model_config.transducer.decoder | ||
| 175 | + : "no"); | ||
| 176 | + printf("joiner: %s\n", c.model_config.transducer.joiner | ||
| 177 | + ? c.model_config.transducer.joiner | ||
| 178 | + : "no"); | ||
| 179 | + | ||
| 180 | + printf("tokens: %s\n", c.model_config.tokens ? c.model_config.tokens : "no"); | ||
| 181 | + printf("num_threads: %d\n", c.model_config.num_threads); | ||
| 182 | + printf("provider: %s\n", | ||
| 183 | + c.model_config.provider ? c.model_config.provider : "no"); | ||
| 184 | + printf("debug: %d\n", c.model_config.debug); | ||
| 185 | + printf("model_type: %s\n", | ||
| 186 | + c.model_config.model_type ? c.model_config.model_type : "no"); | ||
| 187 | +#endif | ||
| 188 | + | ||
| 189 | + SherpaOnnxOnlineRecognizer *recognizer = CreateOnlineRecognizer(&c); | ||
| 190 | + | ||
| 191 | + if (c.model_config.transducer.encoder) { | ||
| 192 | + delete[] c.model_config.transducer.encoder; | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + if (c.model_config.transducer.decoder) { | ||
| 196 | + delete[] c.model_config.transducer.decoder; | ||
| 197 | + } | ||
| 198 | + | ||
| 199 | + if (c.model_config.transducer.joiner) { | ||
| 200 | + delete[] c.model_config.transducer.joiner; | ||
| 201 | + } | ||
| 202 | + | ||
| 203 | + if (c.model_config.tokens) { | ||
| 204 | + delete[] c.model_config.tokens; | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + if (c.model_config.provider) { | ||
| 208 | + delete[] c.model_config.provider; | ||
| 209 | + } | ||
| 210 | + | ||
| 211 | + if (c.model_config.model_type) { | ||
| 212 | + delete[] c.model_config.model_type; | ||
| 213 | + } | ||
| 214 | + | ||
| 215 | + if (!recognizer) { | ||
| 216 | + Napi::TypeError::New(env, "Please check your config!") | ||
| 217 | + .ThrowAsJavaScriptException(); | ||
| 218 | + | ||
| 219 | + return {}; | ||
| 220 | + } | ||
| 221 | + | ||
| 222 | + return Napi::External<SherpaOnnxOnlineRecognizer>::New( | ||
| 223 | + env, recognizer, | ||
| 224 | + [](Napi::Env env, SherpaOnnxOnlineRecognizer *recognizer) { | ||
| 225 | + DestroyOnlineRecognizer(recognizer); | ||
| 226 | + }); | ||
| 227 | +} | ||
| 228 | + | ||
| 229 | +static Napi::External<SherpaOnnxOnlineStream> CreateOnlineStreamWrapper( | ||
| 230 | + const Napi::CallbackInfo &info) { | ||
| 231 | + Napi::Env env = info.Env(); | ||
| 232 | + if (info.Length() != 1) { | ||
| 233 | + std::ostringstream os; | ||
| 234 | + os << "Expect only 1 argument. Given: " << info.Length(); | ||
| 235 | + | ||
| 236 | + Napi::TypeError::New(env, os.str()).ThrowAsJavaScriptException(); | ||
| 237 | + | ||
| 238 | + return {}; | ||
| 239 | + } | ||
| 240 | + | ||
| 241 | + if (!info[0].IsExternal()) { | ||
| 242 | + Napi::TypeError::New( | ||
| 243 | + env, "You should pass a recognizer pointer as the only argument") | ||
| 244 | + .ThrowAsJavaScriptException(); | ||
| 245 | + | ||
| 246 | + return {}; | ||
| 247 | + } | ||
| 248 | + | ||
| 249 | + SherpaOnnxOnlineRecognizer *recognizer = | ||
| 250 | + info[0].As<Napi::External<SherpaOnnxOnlineRecognizer>>().Data(); | ||
| 251 | + | ||
| 252 | + SherpaOnnxOnlineStream *stream = CreateOnlineStream(recognizer); | ||
| 253 | + | ||
| 254 | + return Napi::External<SherpaOnnxOnlineStream>::New( | ||
| 255 | + env, stream, [](Napi::Env env, SherpaOnnxOnlineStream *stream) { | ||
| 256 | + DestroyOnlineStream(stream); | ||
| 257 | + }); | ||
| 258 | +} | ||
| 259 | + | ||
| 260 | +static void AcceptWaveformWrapper(const Napi::CallbackInfo &info) { | ||
| 261 | + Napi::Env env = info.Env(); | ||
| 262 | + | ||
| 263 | + if (info.Length() != 2) { | ||
| 264 | + std::ostringstream os; | ||
| 265 | + os << "Expect only 2 arguments. Given: " << info.Length(); | ||
| 266 | + | ||
| 267 | + Napi::TypeError::New(env, os.str()).ThrowAsJavaScriptException(); | ||
| 268 | + | ||
| 269 | + return; | ||
| 270 | + } | ||
| 271 | + | ||
| 272 | + if (!info[0].IsExternal()) { | ||
| 273 | + Napi::TypeError::New(env, "Argument 0 should be a online stream pointer.") | ||
| 274 | + .ThrowAsJavaScriptException(); | ||
| 275 | + | ||
| 276 | + return; | ||
| 277 | + } | ||
| 278 | + | ||
| 279 | + SherpaOnnxOnlineStream *stream = | ||
| 280 | + info[0].As<Napi::External<SherpaOnnxOnlineStream>>().Data(); | ||
| 281 | + | ||
| 282 | + if (!info[1].IsObject()) { | ||
| 283 | + Napi::TypeError::New(env, "Argument 1 should be an object") | ||
| 284 | + .ThrowAsJavaScriptException(); | ||
| 285 | + | ||
| 286 | + return; | ||
| 287 | + } | ||
| 288 | + | ||
| 289 | + Napi::Object obj = info[1].As<Napi::Object>(); | ||
| 290 | + | ||
| 291 | + if (!obj.Has("samples")) { | ||
| 292 | + Napi::TypeError::New(env, "The argument object should have a field samples") | ||
| 293 | + .ThrowAsJavaScriptException(); | ||
| 294 | + | ||
| 295 | + return; | ||
| 296 | + } | ||
| 297 | + | ||
| 298 | + if (!obj.Get("samples").IsTypedArray()) { | ||
| 299 | + Napi::TypeError::New(env, "The object['samples'] should be a typed array") | ||
| 300 | + .ThrowAsJavaScriptException(); | ||
| 301 | + | ||
| 302 | + return; | ||
| 303 | + } | ||
| 304 | + | ||
| 305 | + if (!obj.Has("sampleRate")) { | ||
| 306 | + Napi::TypeError::New(env, | ||
| 307 | + "The argument object should have a field sampleRate") | ||
| 308 | + .ThrowAsJavaScriptException(); | ||
| 309 | + | ||
| 310 | + return; | ||
| 311 | + } | ||
| 312 | + | ||
| 313 | + if (!obj.Get("sampleRate").IsNumber()) { | ||
| 314 | + Napi::TypeError::New(env, "The object['samples'] should be a number") | ||
| 315 | + .ThrowAsJavaScriptException(); | ||
| 316 | + | ||
| 317 | + return; | ||
| 318 | + } | ||
| 319 | + | ||
| 320 | + Napi::Float32Array samples = obj.Get("samples").As<Napi::Float32Array>(); | ||
| 321 | + int32_t sample_rate = obj.Get("sampleRate").As<Napi::Number>().Int32Value(); | ||
| 322 | + | ||
| 323 | + AcceptWaveform(stream, sample_rate, samples.Data(), samples.ElementLength()); | ||
| 324 | +} | ||
| 325 | + | ||
| 326 | +static Napi::Boolean IsOnlineStreamReadyWrapper( | ||
| 327 | + const Napi::CallbackInfo &info) { | ||
| 328 | + Napi::Env env = info.Env(); | ||
| 329 | + if (info.Length() != 2) { | ||
| 330 | + std::ostringstream os; | ||
| 331 | + os << "Expect only 2 arguments. Given: " << info.Length(); | ||
| 332 | + | ||
| 333 | + Napi::TypeError::New(env, os.str()).ThrowAsJavaScriptException(); | ||
| 334 | + | ||
| 335 | + return {}; | ||
| 336 | + } | ||
| 337 | + | ||
| 338 | + if (!info[0].IsExternal()) { | ||
| 339 | + Napi::TypeError::New(env, | ||
| 340 | + "Argument 0 should be a online recognizer pointer.") | ||
| 341 | + .ThrowAsJavaScriptException(); | ||
| 342 | + | ||
| 343 | + return {}; | ||
| 344 | + } | ||
| 345 | + | ||
| 346 | + if (!info[1].IsExternal()) { | ||
| 347 | + Napi::TypeError::New(env, | ||
| 348 | + "Argument 1 should be a online recognizer pointer.") | ||
| 349 | + .ThrowAsJavaScriptException(); | ||
| 350 | + | ||
| 351 | + return {}; | ||
| 352 | + } | ||
| 353 | + | ||
| 354 | + SherpaOnnxOnlineRecognizer *recognizer = | ||
| 355 | + info[0].As<Napi::External<SherpaOnnxOnlineRecognizer>>().Data(); | ||
| 356 | + | ||
| 357 | + SherpaOnnxOnlineStream *stream = | ||
| 358 | + info[1].As<Napi::External<SherpaOnnxOnlineStream>>().Data(); | ||
| 359 | + | ||
| 360 | + int32_t is_ready = IsOnlineStreamReady(recognizer, stream); | ||
| 361 | + | ||
| 362 | + return Napi::Boolean::New(env, is_ready); | ||
| 363 | +} | ||
| 364 | + | ||
| 365 | +static void DecodeOnlineStreamWrapper(const Napi::CallbackInfo &info) { | ||
| 366 | + Napi::Env env = info.Env(); | ||
| 367 | + if (info.Length() != 2) { | ||
| 368 | + std::ostringstream os; | ||
| 369 | + os << "Expect only 2 arguments. Given: " << info.Length(); | ||
| 370 | + | ||
| 371 | + Napi::TypeError::New(env, os.str()).ThrowAsJavaScriptException(); | ||
| 372 | + | ||
| 373 | + return; | ||
| 374 | + } | ||
| 375 | + | ||
| 376 | + if (!info[0].IsExternal()) { | ||
| 377 | + Napi::TypeError::New(env, | ||
| 378 | + "Argument 0 should be a online recognizer pointer.") | ||
| 379 | + .ThrowAsJavaScriptException(); | ||
| 380 | + | ||
| 381 | + return; | ||
| 382 | + } | ||
| 383 | + | ||
| 384 | + if (!info[1].IsExternal()) { | ||
| 385 | + Napi::TypeError::New(env, | ||
| 386 | + "Argument 1 should be a online recognizer pointer.") | ||
| 387 | + .ThrowAsJavaScriptException(); | ||
| 388 | + | ||
| 389 | + return; | ||
| 390 | + } | ||
| 391 | + | ||
| 392 | + SherpaOnnxOnlineRecognizer *recognizer = | ||
| 393 | + info[0].As<Napi::External<SherpaOnnxOnlineRecognizer>>().Data(); | ||
| 394 | + | ||
| 395 | + SherpaOnnxOnlineStream *stream = | ||
| 396 | + info[1].As<Napi::External<SherpaOnnxOnlineStream>>().Data(); | ||
| 397 | + | ||
| 398 | + DecodeOnlineStream(recognizer, stream); | ||
| 399 | +} | ||
| 400 | + | ||
| 401 | +static Napi::String GetOnlineStreamResultAsJsonWrapper( | ||
| 402 | + const Napi::CallbackInfo &info) { | ||
| 403 | + Napi::Env env = info.Env(); | ||
| 404 | + if (info.Length() != 2) { | ||
| 405 | + std::ostringstream os; | ||
| 406 | + os << "Expect only 2 arguments. Given: " << info.Length(); | ||
| 407 | + | ||
| 408 | + Napi::TypeError::New(env, os.str()).ThrowAsJavaScriptException(); | ||
| 409 | + | ||
| 410 | + return {}; | ||
| 411 | + } | ||
| 412 | + | ||
| 413 | + if (!info[0].IsExternal()) { | ||
| 414 | + Napi::TypeError::New(env, | ||
| 415 | + "Argument 0 should be a online recognizer pointer.") | ||
| 416 | + .ThrowAsJavaScriptException(); | ||
| 417 | + | ||
| 418 | + return {}; | ||
| 419 | + } | ||
| 420 | + | ||
| 421 | + if (!info[1].IsExternal()) { | ||
| 422 | + Napi::TypeError::New(env, | ||
| 423 | + "Argument 1 should be a online recognizer pointer.") | ||
| 424 | + .ThrowAsJavaScriptException(); | ||
| 425 | + | ||
| 426 | + return {}; | ||
| 427 | + } | ||
| 428 | + | ||
| 429 | + SherpaOnnxOnlineRecognizer *recognizer = | ||
| 430 | + info[0].As<Napi::External<SherpaOnnxOnlineRecognizer>>().Data(); | ||
| 431 | + | ||
| 432 | + SherpaOnnxOnlineStream *stream = | ||
| 433 | + info[1].As<Napi::External<SherpaOnnxOnlineStream>>().Data(); | ||
| 434 | + | ||
| 435 | + const char *json = GetOnlineStreamResultAsJson(recognizer, stream); | ||
| 436 | + Napi::String s = Napi::String::New(env, json); | ||
| 437 | + | ||
| 438 | + DestroyOnlineStreamResultJson(json); | ||
| 439 | + | ||
| 440 | + return s; | ||
| 441 | +} | ||
| 442 | + | ||
| 443 | +void InitStreamingAsr(Napi::Env env, Napi::Object exports) { | ||
| 444 | + exports.Set(Napi::String::New(env, "createOnlineRecognizer"), | ||
| 445 | + Napi::Function::New(env, CreateOnlineRecognizerWrapper)); | ||
| 446 | + | ||
| 447 | + exports.Set(Napi::String::New(env, "createOnlineStream"), | ||
| 448 | + Napi::Function::New(env, CreateOnlineStreamWrapper)); | ||
| 449 | + | ||
| 450 | + exports.Set(Napi::String::New(env, "acceptWaveformOnline"), | ||
| 451 | + Napi::Function::New(env, AcceptWaveformWrapper)); | ||
| 452 | + | ||
| 453 | + exports.Set(Napi::String::New(env, "isOnlineStreamReady"), | ||
| 454 | + Napi::Function::New(env, IsOnlineStreamReadyWrapper)); | ||
| 455 | + | ||
| 456 | + exports.Set(Napi::String::New(env, "decodeOnlineStream"), | ||
| 457 | + Napi::Function::New(env, DecodeOnlineStreamWrapper)); | ||
| 458 | + | ||
| 459 | + exports.Set(Napi::String::New(env, "getOnlineStreamResultAsJson"), | ||
| 460 | + Napi::Function::New(env, GetOnlineStreamResultAsJsonWrapper)); | ||
| 461 | +} |
scripts/node-addon-api/src/wave-reader.cc
0 → 100644
| 1 | +// scripts/node-addon-api/src/wave-reader.cc | ||
| 2 | +// | ||
| 3 | +// Copyright (c) 2024 Xiaomi Corporation | ||
| 4 | + | ||
| 5 | +#include <sstream> | ||
| 6 | + | ||
| 7 | +#include "napi.h" // NOLINT | ||
| 8 | +#include "sherpa-onnx/c-api/c-api.h" | ||
| 9 | + | ||
| 10 | +static Napi::Object ReadWaveWrapper(const Napi::CallbackInfo &info) { | ||
| 11 | + Napi::Env env = info.Env(); | ||
| 12 | + if (info.Length() != 1) { | ||
| 13 | + std::ostringstream os; | ||
| 14 | + os << "Expect only 1 argument. Given: " << info.Length(); | ||
| 15 | + | ||
| 16 | + Napi::TypeError::New(env, os.str()).ThrowAsJavaScriptException(); | ||
| 17 | + | ||
| 18 | + return {}; | ||
| 19 | + } | ||
| 20 | + if (!info[0].IsString()) { | ||
| 21 | + Napi::TypeError::New(env, "Argument should be a string") | ||
| 22 | + .ThrowAsJavaScriptException(); | ||
| 23 | + | ||
| 24 | + return {}; | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + std::string filename = info[0].As<Napi::String>().Utf8Value(); | ||
| 28 | + | ||
| 29 | + const SherpaOnnxWave *wave = SherpaOnnxReadWave(filename.c_str()); | ||
| 30 | + if (!wave) { | ||
| 31 | + std::ostringstream os; | ||
| 32 | + os << "Failed to read '" << filename << "'"; | ||
| 33 | + Napi::TypeError::New(env, os.str()).ThrowAsJavaScriptException(); | ||
| 34 | + | ||
| 35 | + return {}; | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + Napi::ArrayBuffer arrayBuffer = Napi::ArrayBuffer::New( | ||
| 39 | + env, const_cast<float *>(wave->samples), | ||
| 40 | + sizeof(float) * wave->num_samples, | ||
| 41 | + [](Napi::Env /*env*/, void * /*data*/, const SherpaOnnxWave *hint) { | ||
| 42 | + SherpaOnnxFreeWave(hint); | ||
| 43 | + }, | ||
| 44 | + wave); | ||
| 45 | + Napi::Float32Array float32Array = | ||
| 46 | + Napi::Float32Array::New(env, wave->num_samples, arrayBuffer, 0); | ||
| 47 | + | ||
| 48 | + Napi::Object obj = Napi::Object::New(env); | ||
| 49 | + obj.Set(Napi::String::New(env, "samples"), float32Array); | ||
| 50 | + obj.Set(Napi::String::New(env, "sampleRate"), wave->sample_rate); | ||
| 51 | + return obj; | ||
| 52 | +} | ||
| 53 | + | ||
| 54 | +void InitWaveReader(Napi::Env env, Napi::Object exports) { | ||
| 55 | + exports.Set(Napi::String::New(env, "readWave"), | ||
| 56 | + Napi::Function::New(env, ReadWaveWrapper)); | ||
| 57 | +} |
| 1 | +// Copyright (c) 2024 Xiaomi Corporation | ||
| 2 | +const sherpa_onnx = require('../lib/sherpa-onnx.js'); | ||
| 3 | + | ||
| 4 | +const config = { | ||
| 5 | + 'featConfig': { | ||
| 6 | + 'sampleRate': 16000, | ||
| 7 | + 'featureDim': 80, | ||
| 8 | + }, | ||
| 9 | + 'modelConfig': { | ||
| 10 | + 'transducer': { | ||
| 11 | + 'encoder': | ||
| 12 | + './sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20/encoder-epoch-99-avg-1.onnx', | ||
| 13 | + 'decoder': | ||
| 14 | + './sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20/decoder-epoch-99-avg-1.onnx', | ||
| 15 | + 'joiner': | ||
| 16 | + './sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20/joiner-epoch-99-avg-1.onnx', | ||
| 17 | + }, | ||
| 18 | + 'tokens': | ||
| 19 | + './sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20/tokens.txt', | ||
| 20 | + 'numThreads': 2, | ||
| 21 | + 'provider': 'cpu', | ||
| 22 | + 'debug': 1, | ||
| 23 | + 'modelType': 'zipformer', | ||
| 24 | + } | ||
| 25 | +}; | ||
| 26 | + | ||
| 27 | +const waveFilename = | ||
| 28 | + './sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20/test_wavs/0.wav'; | ||
| 29 | + | ||
| 30 | +const recognizer = new sherpa_onnx.OnlineRecognizer(config); | ||
| 31 | +console.log('Started') | ||
| 32 | +let start = performance.now(); | ||
| 33 | +const stream = recognizer.createStream(); | ||
| 34 | +const wave = sherpa_onnx.readWave(waveFilename); | ||
| 35 | +stream.acceptWaveform(wave.samples, wave.sampleRate); | ||
| 36 | + | ||
| 37 | +const tailPadding = new Float32Array(wave.sampleRate * 0.4); | ||
| 38 | +stream.acceptWaveform(tailPadding, wave.sampleRate); | ||
| 39 | + | ||
| 40 | +while (recognizer.isReady(stream)) { | ||
| 41 | + recognizer.decode(stream); | ||
| 42 | +} | ||
| 43 | +result = recognizer.getResult(stream) | ||
| 44 | +let stop = performance.now(); | ||
| 45 | +console.log('Done') | ||
| 46 | + | ||
| 47 | +const elapsed_seconds = (stop - start) / 1000; | ||
| 48 | +const duration = wave.samples.length / wave.sampleRate; | ||
| 49 | +const real_time_factor = elapsed_seconds / duration; | ||
| 50 | +console.log('Wave duration', duration.toFixed(3), 'secodns') | ||
| 51 | +console.log('Elapsed', elapsed_seconds.toFixed(3), 'secodns') | ||
| 52 | +console.log('RTF', real_time_factor.toFixed(3)) | ||
| 53 | +console.log('result', result.text) |
scripts/node-addon-api/test/test_binding.js
0 → 100644
-
请 注册 或 登录 后发表评论