Fangjun Kuang
Committed by GitHub

Begin to add node-addon-api for sherpa-onnx (#826)

  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
@@ -102,3 +102,5 @@ sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01 @@ -102,3 +102,5 @@ sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01
102 *.tar.bz2 102 *.tar.bz2
103 *.zip 103 *.zip
104 sherpa-onnx-ced-* 104 sherpa-onnx-ced-*
  105 +node_modules
  106 +package-lock.json
  1 +# Introduction
  2 +
  3 +This folder contains `node-addon-api` wrapper for `sherpa-onnx`.
  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 +}
  1 +const addon = require('bindings')('sherpa-onnx-node-addon-api-native');
  2 +const streaming_asr = require('./streaming-asr.js');
  3 +
  4 +module.exports = {
  5 + OnlineRecognizer: streaming_asr.OnlineRecognizer,
  6 + readWave: addon.readWave,
  7 +}
  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}
  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)
  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 +}
  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)
  1 +const sherpa_onnx = require('../lib/sherpa-onnx.js');
  2 +console.log(sherpa_onnx)
  3 +
  4 +console.log('Tests passed- everything looks OK!');