Преглед на файлове

Add WebAssembly + NodeJS example. (#311)

Fangjun Kuang преди 1 година
родител
ревизия
41ebcfb11b

+ 23 - 46
.github/workflows/nodejs.yaml

@@ -1,24 +1,16 @@
 name: nodejs
 
 on:
-  push:
-    branches:
-      - master
-    paths:
-      - '.github/workflows/nodejs.yaml'
-      - 'CMakeLists.txt'
-      - 'cmake/**'
-      - 'nodejs-examples/**'
-      - 'sherpa-ncnn/csrc/*'
-  pull_request:
-    branches:
-      - master
-    paths:
-      - '.github/workflows/nodejs.yaml'
-      - 'CMakeLists.txt'
-      - 'cmake/**'
-      - 'nodejs-examples/**'
-      - 'sherpa-ncnn/csrc/*'
+  workflow_dispatch:
+
+  schedule:
+    # minute (0-59)
+    # hour (0-23)
+    # day of the month (1-31)
+    # month (1-12)
+    # day of the week (0-6)
+    # nightly test at 22:50 UTC time every day
+    - cron: "50 22 * * *"
 
 concurrency:
   group: nodejs-${{ github.ref }}
@@ -33,63 +25,48 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        os: [ubuntu-latest, macos-latest]
+        os: [ubuntu-latest, macos-latest, windows-latest]
         python-version: ["3.8"]
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
         with:
           fetch-depth: 0
 
       - name: Setup Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
 
-      - uses: actions/setup-node@v3
+      - uses: actions/setup-node@v4
         with:
-          node-version: 13
+          registry-url: 'https://registry.npmjs.org'
 
       - name: Display node version
         shell: bash
         run: |
           node --version
-          npm --version
-          cd nodejs-examples
-
-          npm install npm@6.14.4 -g
-          npm install npm@6.14.4
-          npm --version
 
       - name: Install npm packages
         shell: bash
         run: |
           cd nodejs-examples
-          npm install ffi-napi ref-struct-napi wav
+          npm install npm
+          npm --version
           npm list
 
-      - name: ccache
-        uses: hendrikmuhs/ccache-action@v1.2
-        with:
-          key: ${{ matrix.os }}-shared
-
       - name: Download model
         shell: bash
         run: |
           cd nodejs-examples
-          GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/csukuangfj/sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13
-          cd sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13
-          git lfs pull --include "*.bin"
-          ls -lh
+
+          curl -OL https://github.com/k2-fsa/sherpa-ncnn/releases/download/models/sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13.tar.bz2
+          tar xvf sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13.tar.bz2
+          rm sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13.tar.bz2
+          ls -lh sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13
 
       - name: Test
         shell: bash
         run: |
-          export CMAKE_CXX_COMPILER_LAUNCHER=ccache
-          export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH"
-          cmake --version
-
           cd nodejs-examples
-          ls -lh
-
-          ./run.sh
+          node ./decode-file.js

+ 86 - 0
.github/workflows/npm.yaml

@@ -0,0 +1,86 @@
+name: npm
+
+on:
+  workflow_dispatch:
+
+concurrency:
+  group: npm-${{ github.ref }}
+  cancel-in-progress: true
+
+permissions:
+  contents: read
+  id-token: write
+
+jobs:
+  nodejs:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-latest]
+        python-version: ["3.8"]
+
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+
+      - name: Install emsdk
+        uses: mymindstorm/setup-emsdk@v14
+
+      - name: View emsdk version
+        shell: bash
+        run: |
+          emcc -v
+          echo "--------------------"
+          emcc --check
+
+      - name: Setup Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v5
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - uses: actions/setup-node@v4
+        with:
+          registry-url: 'https://registry.npmjs.org'
+
+      - name: Display node version
+        shell: bash
+        run: |
+          node --version
+          npm --version
+
+          cd nodejs-examples
+          npm install npm
+          npm --version
+
+      - name: Build nodejs package
+        shell: bash
+        env:
+          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+        run: |
+          ./build-wasm-simd-for-nodejs.sh
+
+          cp -v build-wasm-simd-for-nodejs/install/bin/wasm/sherpa-ncnn-wasm-main.js ./scripts/nodejs
+          cp -v build-wasm-simd-for-nodejs/install/bin/wasm/sherpa-ncnn-wasm-main.wasm ./scripts/nodejs
+          cp -v build-wasm-simd-for-nodejs/install/bin/wasm/sherpa-ncnn.js ./scripts/nodejs
+
+          SHERPA_NCNN_VERSION=$(grep "SHERPA_NCNN_VERSION" ./CMakeLists.txt  | cut -d " " -f 2  | cut -d '"' -f 2)
+          echo "SHERPA_NCNN_VERSION $SHERPA_NCNN_VERSION"
+
+          cd scripts/nodejs
+
+          owner=${{ github.repository_owner }}
+          echo "owner: $owner"
+
+          sed -i.bak s/SHERPA_NCNN_VERSION/$SHERPA_NCNN_VERSION/g ./package.json
+          sed -i.bak s/k2-fsa/$owner/g ./package.json
+
+          rm package.json.bak
+
+          git diff .
+
+          npm install
+          npm ci
+          # see https://docs.npmjs.com/generating-provenance-statements
+          npm publish --provenance --access public

+ 1 - 0
.gitignore

@@ -116,3 +116,4 @@ generate-int8-*.sh
 cmake-build-release
 cmake-build-debug
 
+node_modules

+ 10 - 1
CMakeLists.txt

@@ -1,7 +1,7 @@
 cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
 project(sherpa-ncnn)
 
-set(SHERPA_NCNN_VERSION "2.1.8")
+set(SHERPA_NCNN_VERSION "2.1.9")
 
 # Disable warning about
 #
@@ -41,6 +41,7 @@ option(SHERPA_NCNN_ENABLE_BINARY "Whether to build the binary sherpa-ncnn" ON)
 option(SHERPA_NCNN_ENABLE_TEST "Whether to build tests" OFF)
 option(SHERPA_NCNN_ENABLE_C_API "Whether to build C API" ON)
 option(SHERPA_NCNN_ENABLE_WASM "Whether to enable WASM" OFF)
+option(SHERPA_NCNN_ENABLE_WASM_FOR_NODEJS "Whether to enable WASM for NodeJS" OFF)
 option(SHERPA_NCNN_ENABLE_GENERATE_INT8_SCALE_TABLE "Whether to generate-int8-scale-table" ON)
 option(SHERPA_NCNN_ENABLE_FFMPEG_EXAMPLES "Whether to enable ffmpeg-examples" OFF)
 
@@ -77,6 +78,14 @@ message(STATUS "SHERPA_NCNN_ENABLE_TEST ${SHERPA_NCNN_ENABLE_TEST}")
 message(STATUS "SHERPA_NCNN_ENABLE_C_API ${SHERPA_NCNN_ENABLE_C_API}")
 message(STATUS "SHERPA_NCNN_ENABLE_GENERATE_INT8_SCALE_TABLE ${SHERPA_NCNN_ENABLE_GENERATE_INT8_SCALE_TABLE}")
 message(STATUS "SHERPA_NCNN_ENABLE_FFMPEG_EXAMPLES ${SHERPA_NCNN_ENABLE_FFMPEG_EXAMPLES}")
+message(STATUS "SHERPA_NCNN_ENABLE_WASM ${SHERPA_NCNN_ENABLE_WASM}")
+message(STATUS "SHERPA_NCNN_ENABLE_WASM_FOR_NODEJS ${SHERPA_NCNN_ENABLE_WASM_FOR_NODEJS}")
+
+if(SHERPA_NCNN_ENABLE_WASM_FOR_NODEJS)
+  if(NOT SHERPA_NCNN_ENABLE_WASM)
+    message(FATAL_ERROR "Please set SHERPA_NCNN_ENABLE_WASM to ON if you enable WASM for NodeJS")
+  endif()
+endif()
 
 if(NOT CMAKE_BUILD_TYPE)
   message(STATUS "No CMAKE_BUILD_TYPE given, default to Release")

+ 76 - 0
build-wasm-simd-for-nodejs.sh

@@ -0,0 +1,76 @@
+#!/usr/bin/env bash
+# Copyright (c)  2024  Xiaomi Corporation
+#
+# This script is to build sherpa-ncnn for WebAssembly (NodeJS)
+#
+# See also
+# https://github.com/Tencent/ncnn/wiki/how-to-build#build-for-webassembly
+#
+# Please refer to
+# https://k2-fsa.github.io/sherpa/ncnn/wasm/index.html
+# for more details.
+
+set -ex
+
+if [ x"$EMSCRIPTEN" == x"" ]; then
+  if ! command -v emcc &> /dev/null; then
+    echo "Please install emscripten first"
+    echo ""
+    echo "You can use the following commands to install it:"
+    echo ""
+    echo "git clone https://github.com/emscripten-core/emsdk.git"
+    echo "cd emsdk"
+    echo "git pull"
+    echo "./emsdk install latest"
+    echo "./emsdk activate latest"
+    echo "source ./emsdk_env.sh"
+    exit 1
+  else
+    EMSCRIPTEN=$(dirname $(realpath $(which emcc)))
+  fi
+fi
+
+export EMSCRIPTEN=$EMSCRIPTEN
+echo "EMSCRIPTEN: $EMSCRIPTEN"
+if [ ! -f $EMSCRIPTEN/cmake/Modules/Platform/Emscripten.cmake ]; then
+  echo "Cannot find $EMSCRIPTEN/cmake/Modules/Platform/Emscripten.cmake"
+  echo "Please make sure you have installed emsdk correctly"
+  exit 1
+fi
+
+mkdir -p build-wasm-simd-for-nodejs
+pushd build-wasm-simd-for-nodejs
+
+export SHERPA_NCNN_IS_USING_BUILD_WASM_SH=ON
+
+cmake \
+  -DCMAKE_INSTALL_PREFIX=./install \
+  -DCMAKE_BUILD_TYPE=Release \
+  -DCMAKE_TOOLCHAIN_FILE=$EMSCRIPTEN/cmake/Modules/Platform/Emscripten.cmake \
+  -DNCNN_THREADS=OFF \
+  -DNCNN_OPENMP=OFF \
+  -DNCNN_SIMPLEOMP=OFF \
+  -DNCNN_RUNTIME_CPU=OFF \
+  -DNCNN_SSE2=ON \
+  -DNCNN_AVX2=OFF \
+  -DNCNN_AVX=OFF \
+  -DNCNN_BUILD_TOOLS=OFF \
+  -DNCNN_BUILD_EXAMPLES=OFF \
+  -DNCNN_BUILD_BENCHMARK=OFF \
+  \
+  -DSHERPA_NCNN_ENABLE_WASM=ON \
+  -DSHERPA_NCNN_ENABLE_WASM_FOR_NODEJS=ON \
+  -DBUILD_SHARED_LIBS=OFF \
+  -DSHERPA_NCNN_ENABLE_PYTHON=OFF \
+  -DSHERPA_NCNN_ENABLE_PORTAUDIO=OFF \
+  -DSHERPA_NCNN_ENABLE_JNI=OFF \
+  -DSHERPA_NCNN_ENABLE_BINARY=OFF \
+  -DSHERPA_NCNN_ENABLE_TEST=OFF \
+  -DSHERPA_NCNN_ENABLE_C_API=ON \
+  -DSHERPA_NCNN_ENABLE_GENERATE_INT8_SCALE_TABLE=OFF \
+  -DSHERPA_NCNN_ENABLE_FFMPEG_EXAMPLES=OFF \
+  ..
+
+make -j2
+make install
+ls -lh install/bin/wasm

+ 0 - 3
nodejs-examples/.gitignore

@@ -1,4 +1 @@
-install
-node_modules
-package.json
 package-lock.json

+ 47 - 0
nodejs-examples/README.md

@@ -0,0 +1,47 @@
+# Introduction
+
+This folder contains examples about how to use the sherpa-ncnn WebAssembly module
+with nodejs for speech recognition.
+
+- [decode-file.js](./decode-file.js) it shows how to decode a file
+- [real-time-speech-recognition-microphone.js](./real-time-speech-recognition-microphone.js) it shows
+  how to do real-time speech recognition with a microphone
+
+## Usage
+
+### Install dependencies
+
+```bash
+cd ./nodejs-examples
+npm i
+```
+
+### Download a model
+
+Please visit <https://github.com/k2-fsa/sherpa-ncnn/releases/tag/models> to
+select more models.
+
+The following is an example:
+
+```bash
+cd ./nodejs-examples
+wget https://github.com/k2-fsa/sherpa-ncnn/releases/download/models/sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13.tar.bz2
+tar xvf sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13.tar.bz2
+rm sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13.tar.bz2
+```
+
+### Decode a file
+
+```bash
+cd ./nodejs-examples
+
+node ./decode-file.js
+```
+
+### Real-time speech recognition from a microphone
+
+```bash
+cd ./nodejs-examples
+
+node ./real-time-speech-recognition-microphone.js
+```

+ 119 - 0
nodejs-examples/decode-file.js

@@ -0,0 +1,119 @@
+// Copyright (c)  2024  Xiaomi Corporation (authors: Fangjun Kuang)
+const fs = require('fs');
+const wav = require('wav');
+const {Readable} = require('stream');
+
+const sherpa_ncnn = require('sherpa-ncnn');
+
+function createRecognizer() {
+  let modelConfig = {
+    encoderParam:
+        './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/encoder_jit_trace-pnnx.ncnn.param',
+    encoderBin:
+        './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/encoder_jit_trace-pnnx.ncnn.bin',
+    decoderParam:
+        './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/decoder_jit_trace-pnnx.ncnn.param',
+    decoderBin:
+        './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/decoder_jit_trace-pnnx.ncnn.bin',
+    joinerParam:
+        './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/joiner_jit_trace-pnnx.ncnn.param',
+    joinerBin:
+        './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/joiner_jit_trace-pnnx.ncnn.bin',
+    tokens:
+        './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/tokens.txt',
+    useVulkanCompute: 0,
+    numThreads: 1,
+  };
+
+  let decoderConfig = {
+    decodingMethod: 'greedy_search',
+    numActivePaths: 4,
+  };
+
+  let featConfig = {
+    samplingRate: 16000,
+    featureDim: 80,
+  };
+
+  let config = {
+    featConfig: featConfig,
+    modelConfig: modelConfig,
+    decoderConfig: decoderConfig,
+    enableEndpoint: 1,
+    rule1MinTrailingSilence: 1.2,
+    rule2MinTrailingSilence: 2.4,
+    rule3MinUtternceLength: 20,
+  };
+
+  return sherpa_ncnn.createRecognizer(config);
+}
+
+const recognizer = createRecognizer();
+const stream = recognizer.createStream();
+
+console.log(recognizer.config);
+
+const waveFilename =
+    './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/test_wavs/0.wav';
+
+const reader = new wav.Reader();
+const readable = new Readable().wrap(reader);
+const buf = [];
+
+reader.on('format', ({audioFormat, bitDepth, channels, sampleRate}) => {
+  if (sampleRate != recognizer.config.featConfig.samplingRate) {
+    throw new Error(`Only support sampleRate ${
+        recognizer.config.featConfig.samplingRate}. Given ${sampleRate}`);
+  }
+
+  if (audioFormat != 1) {
+    throw new Error(`Only support PCM format. Given ${audioFormat}`);
+  }
+
+  if (channels != 1) {
+    throw new Error(`Only a single channel. Given ${channel}`);
+  }
+
+  if (bitDepth != 16) {
+    throw new Error(`Only support 16-bit samples. Given ${bitDepth}`);
+  }
+});
+
+fs.createReadStream(waveFilename, {'highWaterMark': 4096})
+    .pipe(reader)
+    .on('finish', function(err) {
+      // tail padding
+      const floatSamples =
+          new Float32Array(recognizer.config.featConfig.samplingRate * 0.5);
+
+      buf.push(floatSamples);
+      const flattened =
+          Float32Array.from(buf.reduce((a, b) => [...a, ...b], []));
+
+      stream.acceptWaveform(
+          recognizer.config.featConfig.samplingRate, flattened);
+      while (recognizer.isReady(stream)) {
+        recognizer.decode(stream);
+      }
+      const r = recognizer.getResult(stream);
+      console.log('result:', r);
+
+      stream.free();
+      recognizer.free();
+    });
+
+readable.on('readable', function() {
+  let chunk;
+  while ((chunk = readable.read()) != null) {
+    const int16Samples = new Int16Array(
+        chunk.buffer, chunk.byteOffset,
+        chunk.length / Int16Array.BYTES_PER_ELEMENT);
+
+    const floatSamples = new Float32Array(int16Samples.length);
+    for (let i = 0; i < floatSamples.length; i++) {
+      floatSamples[i] = int16Samples[i] / 32768.0;
+    }
+
+    buf.push(floatSamples);
+  }
+});

+ 0 - 181
nodejs-examples/index.js

@@ -1,181 +0,0 @@
-// Copyright (c)  2023  Xiaomi Corporation (authors: Fangjun Kuang)
-//
-// Please use
-//
-// npm install ffi-napi ref-struct-napi
-//
-// before you use this file
-//
-//
-// Please use node 13. node 16, 18, 20, and 21 are known not working.
-// See also
-// https://github.com/node-ffi-napi/node-ffi-napi/issues/244
-// and
-// https://github.com/node-ffi-napi/node-ffi-napi/issues/97
-'use strict'
-
-const debug = require('debug')('sherpa-ncnn');
-const os = require('os');
-const path = require('path');
-const ffi = require('ffi-napi');
-const ref = require('ref-napi');
-const fs = require('fs');
-
-const StructType = require('ref-struct-napi');
-const cstring = ref.types.CString;
-const int32_t = ref.types.int32;
-const float = ref.types.float;
-const floatPtr = ref.refType(float);
-
-const RecognizerPtr = ref.refType(ref.types.void);
-const StreamPtr = ref.refType(ref.types.void);
-const SherpaNcnnModelConfig = StructType({
-  'encoderParam': cstring,
-  'encoderBin': cstring,
-  'decoderParam': cstring,
-  'decoderBin': cstring,
-  'joinerParam': cstring,
-  'joinerBin': cstring,
-  'tokens': cstring,
-  'useVulkanCompute': int32_t,
-  'numThreads': int32_t,
-});
-
-const SherpaNcnnDecoderConfig = StructType({
-  'decodingMethod': cstring,
-  'numActivePaths': int32_t,
-});
-
-const SherpaNcnnFeatureExtractorConfig = StructType({
-  'sampleRate': float,
-  'featureDim': int32_t,
-});
-
-const SherpaNcnnRecognizerConfig = StructType({
-  'featConfig': SherpaNcnnFeatureExtractorConfig,
-  'modelConfig': SherpaNcnnModelConfig,
-  'decoderConfig': SherpaNcnnDecoderConfig,
-  'enableEndpoint': int32_t,
-  'rule1MinTrailingSilence': float,
-  'rule2MinTrailingSilence': float,
-  'rule3MinUtteranceLength': float,
-  'hotwordsFile': cstring,
-  'hotwordsScore': cstring,
-});
-
-const SherpaNcnnResult = StructType({
-  'text': cstring,
-  'tokens': cstring,
-  'timestamps': floatPtr,
-  'count': int32_t,
-});
-
-
-const ResultPtr = ref.refType(SherpaNcnnResult);
-const RecognizerConfigPtr = ref.refType(SherpaNcnnRecognizerConfig)
-
-let soname;
-if (os.platform() == 'win32') {
-  soname = path.join(__dirname, 'install', 'lib', 'sherpa-ncnn-c-api.dll');
-} else if (os.platform() == 'darwin') {
-  soname = path.join(__dirname, 'install', 'lib', 'libsherpa-ncnn-c-api.dylib');
-} else if (os.platform() == 'linux') {
-  soname = path.join(__dirname, 'install', 'lib', 'libsherpa-ncnn-c-api.so');
-} else {
-  throw new Error(`Unsupported platform ${os.platform()}`);
-}
-if (!fs.existsSync(soname)) {
-  throw new Error(`Cannot find file ${soname}. Please make sure you have run
-      ./build.sh`);
-}
-
-debug('soname ', soname)
-
-const libsherpa_ncnn = ffi.Library(soname, {
-  'CreateRecognizer': [RecognizerPtr, [RecognizerConfigPtr]],
-  'DestroyRecognizer': ['void', [RecognizerPtr]],
-  'CreateStream': [StreamPtr, [RecognizerPtr]],
-  'DestroyStream': ['void', [StreamPtr]],
-  'AcceptWaveform': ['void', [StreamPtr, float, floatPtr, int32_t]],
-  'IsReady': [int32_t, [RecognizerPtr, StreamPtr]],
-  'Decode': ['void', [RecognizerPtr, StreamPtr]],
-  'GetResult': [ResultPtr, [RecognizerPtr, StreamPtr]],
-  'DestroyResult': ['void', [ResultPtr]],
-  'Reset': ['void', [RecognizerPtr, StreamPtr]],
-  'InputFinished': ['void', [StreamPtr]],
-  'IsEndpoint': [int32_t, [RecognizerPtr, StreamPtr]],
-});
-
-class Recognizer {
-  /**
-   * @param {SherpaNcnnRecognizerConfig} config Configuration for the recognizer
-   *
-   * The user has to invoke this.free() at the end to avoid memory leak.
-   */
-  constructor(config) {
-    this.recognizer_handle = libsherpa_ncnn.CreateRecognizer(config.ref());
-    this.stream_handle = libsherpa_ncnn.CreateStream(this.recognizer_handle);
-  }
-
-  free() {
-    if (this.stream_handle) {
-      libsherpa_ncnn.DestroyStream(this.stream_handle);
-      this.stream_handle = null;
-    }
-
-    libsherpa_ncnn.DestroyRecognizer(this.recognizer_handle);
-    this.handle = null;
-  }
-
-  /**
-   * @param {bool} true to create a new stream
-   */
-  reset(recreate) {
-    if (recreate) {
-      libsherpa_ncnn.DestroyStream(this.stream_handle);
-      this.stream_handle = libsherpa_ncnn.CreateStream(this.recognizer_handle);
-      return;
-    }
-    libsherpa_ncnn.Reset(this.recognizer_handle, this.stream_handle)
-  }
-  /**
-   * @param {float} Sample rate of the input data
-   * @param {float[]} A 1-d float array containing audio samples. It should be
-   *                  in the range [-1, 1].
-   */
-  acceptWaveform(sampleRate, samples) {
-    libsherpa_ncnn.AcceptWaveform(
-        this.stream_handle, sampleRate, samples, samples.length);
-  }
-
-  isReady() {
-    return libsherpa_ncnn.IsReady(this.recognizer_handle, this.stream_handle);
-  }
-
-  decode() {
-    libsherpa_ncnn.Decode(this.recognizer_handle, this.stream_handle);
-  }
-
-  getResult() {
-    const h =
-        libsherpa_ncnn.GetResult(this.recognizer_handle, this.stream_handle);
-    const text = Buffer.from(h.deref().text, 'utf-8').toString();
-    libsherpa_ncnn.DestroyResult(h);
-    return text;
-  }
-};
-
-// alias
-
-const ModelConfig = SherpaNcnnModelConfig;
-const DecoderConfig = SherpaNcnnDecoderConfig;
-const FeatureConfig = SherpaNcnnFeatureExtractorConfig;
-const RecognizerConfig = SherpaNcnnRecognizerConfig;
-
-module.exports = {
-  FeatureConfig,
-  ModelConfig,
-  DecoderConfig,
-  Recognizer,
-  RecognizerConfig,
-};

+ 3 - 25
nodejs-examples/package.json

@@ -1,29 +1,7 @@
 {
-  "name": "sherpa-ncnn",
-  "version": "2.1.4",
-  "description": "real-time speech recognition with Next-gen Kaldi",
-  "main": "index.js",
   "dependencies": {
-    "ffi-napi": "^4.0.3",
-    "ref-struct-napi": "^1.1.1",
+    "naudiodon2": "^2.4.0",
+    "sherpa-ncnn": "*",
     "wav": "^1.0.2"
-  },
-  "devDependencies": {},
-  "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1"
-  },
-  "repository": {
-    "type": "git",
-    "url": "git+ssh://git@github.com/k2-fsa/sherpa-ncnn.git"
-  },
-  "keywords": [
-    "speech-to-text;",
-    "ASR"
-  ],
-  "author": "The Next-gen Kaldi team",
-  "license": "Apache-2.0",
-  "bugs": {
-    "url": "https://github.com/k2-fsa/sherpa-ncnn/issues"
-  },
-  "homepage": "https://github.com/k2-fsa/sherpa-ncnn#readme"
+  }
 }

+ 100 - 0
nodejs-examples/real-time-speech-recognition-microphone.js

@@ -0,0 +1,100 @@
+// Copyright (c)  2024  Xiaomi Corporation (authors: Fangjun Kuang)
+//
+const portAudio = require('naudiodon2');
+// console.log(portAudio.getDevices());
+
+const sherpa_ncnn = require('sherpa-ncnn');
+
+function createRecognizer() {
+  let modelConfig = {
+    encoderParam:
+        './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/encoder_jit_trace-pnnx.ncnn.param',
+    encoderBin:
+        './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/encoder_jit_trace-pnnx.ncnn.bin',
+    decoderParam:
+        './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/decoder_jit_trace-pnnx.ncnn.param',
+    decoderBin:
+        './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/decoder_jit_trace-pnnx.ncnn.bin',
+    joinerParam:
+        './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/joiner_jit_trace-pnnx.ncnn.param',
+    joinerBin:
+        './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/joiner_jit_trace-pnnx.ncnn.bin',
+    tokens:
+        './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/tokens.txt',
+    useVulkanCompute: 0,
+    numThreads: 1,
+  };
+
+  let decoderConfig = {
+    decodingMethod: 'greedy_search',
+    numActivePaths: 4,
+  };
+
+  let featConfig = {
+    samplingRate: 16000,
+    featureDim: 80,
+  };
+
+  let config = {
+    featConfig: featConfig,
+    modelConfig: modelConfig,
+    decoderConfig: decoderConfig,
+    enableEndpoint: 1,
+    rule1MinTrailingSilence: 1.2,
+    rule2MinTrailingSilence: 2.4,
+    rule3MinUtternceLength: 20,
+  };
+
+  return sherpa_ncnn.createRecognizer(config);
+}
+
+const recognizer = createRecognizer();
+const stream = recognizer.createStream();
+
+let lastText = '';
+let segmentIndex = 0;
+
+const ai = new portAudio.AudioIO({
+  inOptions: {
+    channelCount: 1,
+    closeOnError: true,  // Close the stream if an audio error is detected, if
+                         // set false then just log the error
+    deviceId: -1,  // Use -1 or omit the deviceId to select the default device
+    sampleFormat: portAudio.SampleFormatFloat32,
+    sampleRate: recognizer.config.featConfig.samplingRate
+  }
+});
+
+ai.on('data', data => {
+  const samples = new Float32Array(data.buffer);
+
+  stream.acceptWaveform(recognizer.config.featConfig.samplingRate, samples);
+
+  while (recognizer.isReady(stream)) {
+    recognizer.decode(stream);
+  }
+
+  const isEndpoint = recognizer.isEndpoint(stream);
+  const text = recognizer.getResult(stream);
+
+  if (text.length > 0 && lastText != text) {
+    lastText = text;
+    console.log(segmentIndex, lastText);
+  }
+  if (isEndpoint) {
+    if (text.length > 0) {
+      lastText = text;
+      segmentIndex += 1;
+    }
+    recognizer.reset(stream)
+  }
+});
+
+ai.on('close', () => {
+  console.log('Free resources');
+  stream.free();
+  recognizer.free();
+});
+
+ai.start();
+console.log('Started! Please speak')

+ 0 - 34
nodejs-examples/run.sh

@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-# Copyright (c)  2023  Xiaomi Corporation (authors: Fangjun Kuang)
-
-npm list | grep ffi-napi >/dev/null || npm install ffi-napi
-npm list | grep ref-struct-napi >/dev/null || npm install ref-struct-napi
-npm list | grep wav >/dev/null || npm install wav
-
-if [ ! -e ./install ]; then
-  cd ..
-  mkdir -p build
-  cd build
-  cmake -DBUILD_SHARED_LIBS=ON \
-    -DCMAKE_BUILD_TYPE=Release \
-    -DCMAKE_INSTALL_PREFIX=./install \
-    -DSHERPA_NCNN_ENABLE_PORTAUDIO=OFF \
-    -DSHERPA_NCNN_ENABLE_BINARY=OFF \
-    -DSHERPA_NCNN_ENABLE_C_API=ON \
-    -DSHERPA_NCNN_ENABLE_GENERATE_INT8_SCALE_TABLE=OFF \
-    -DSHERPA_NCNN_ENABLE_PYTHON=OFF \
-    ..
-  make -j3
-  make install
-  cd ../nodejs-examples
-  ln -s $PWD/../build/install .
-fi
-
-if [ ! -d ./sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13 ]; then
-  echo "Please refer to"
-  echo "https://k2-fsa.github.io/sherpa/ncnn/pretrained_models/zipformer-transucer-models.html#csukuangfj-sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13-bilingual-chinese-english"
-  echo "to download the models"
-  exit 0
-fi
-
-node ./test.js

+ 0 - 98
nodejs-examples/test.js

@@ -1,98 +0,0 @@
-// Copyright (c)  2023  Xiaomi Corporation (authors: Fangjun Kuang)
-//
-const fs = require('fs');
-const {Readable} = require('stream');
-const wav = require('wav');
-
-sherpa_ncnn = require('./index.js')
-
-const featConfig = new sherpa_ncnn.FeatureConfig();
-featConfig.sampleRate = 16000;
-featConfig.featureDim = 80;
-
-const decoderConfig = new sherpa_ncnn.DecoderConfig();
-decoderConfig.decodingMethod = 'greedy_search';
-decoderConfig.numActivePaths = 4;
-
-const modelConfig = new sherpa_ncnn.ModelConfig();
-modelConfig.encoderParam =
-    './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/encoder_jit_trace-pnnx.ncnn.param';
-modelConfig.encoderBin =
-    './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/encoder_jit_trace-pnnx.ncnn.bin';
-
-modelConfig.decoderParam =
-    './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/decoder_jit_trace-pnnx.ncnn.param';
-modelConfig.decoderBin =
-    './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/decoder_jit_trace-pnnx.ncnn.bin';
-
-modelConfig.joinerParam =
-    './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/joiner_jit_trace-pnnx.ncnn.param';
-modelConfig.joinerBin =
-    './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/joiner_jit_trace-pnnx.ncnn.bin';
-
-modelConfig.tokens =
-    './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/tokens.txt';
-modelConfig.useVulkanCompute = 0;
-modelConfig.numThreads = 1;
-
-const recognizerConfig = new sherpa_ncnn.RecognizerConfig();
-recognizerConfig.featConfig = featConfig;
-recognizerConfig.modelConfig = modelConfig;
-recognizerConfig.decoderConfig = decoderConfig;
-
-const recognizer = new sherpa_ncnn.Recognizer(recognizerConfig);
-
-const waveFilename =
-    './sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13/test_wavs/2.wav'
-
-const reader = new wav.Reader();
-const readable = new Readable().wrap(reader);
-
-function decode(samples) {
-  recognizer.acceptWaveform(recognizerConfig.featConfig.sampleRate, samples);
-
-  while (recognizer.isReady()) {
-    recognizer.decode();
-  }
-  const text = recognizer.getResult();
-  console.log(text);
-}
-
-reader.on('format', ({audioFormat, sampleRate, channels, bitDepth}) => {
-  if (audioFormat != 1) {
-    throw new Error(`Only support PCM format. Given ${audioFormat}`);
-  }
-  if (channels != 1) {
-    throw new Error(`Only a single channel. Given ${channel}`);
-  }
-
-  if (bitDepth != 16) {
-    throw new Error(`Only support 16-bit samples. Given ${bitDepth}`);
-  }
-});
-
-fs.createReadStream(waveFilename, {'highWaterMark': 4096})
-    .pipe(reader)
-    .on('finish', function(err) {
-      // tail padding
-      const floatSamples =
-          new Float32Array(recognizerConfig.featConfig.sampleRate * 0.5);
-      decode(floatSamples);
-      recognizer.free()
-    });
-
-
-readable.on('readable', function() {
-  let chunk;
-  while ((chunk = readable.read()) != null) {
-    const int16Samples = new Int16Array(
-        chunk.buffer, chunk.byteOffset,
-        chunk.length / Int16Array.BYTES_PER_ELEMENT);
-
-    let floatSamples = new Float32Array(int16Samples.length);
-    for (let i = 0; i < floatSamples.length; i++) {
-      floatSamples[i] = int16Samples[i] / 32768.0;
-    }
-    decode(floatSamples);
-  }
-});

+ 9 - 0
scripts/nodejs/README.md

@@ -0,0 +1,9 @@
+# Introduction
+
+Real-time speech-to-text with [Next-gen Kaldi](https://github.com/k2-fsa/).
+
+It processes everything locally without accessing the Internet.
+
+Please refer to
+https://github.com/k2-fsa/sherpa-ncnn/tree/master/nodejs-examples
+for examples.

+ 14 - 0
scripts/nodejs/index.js

@@ -0,0 +1,14 @@
+// Copyright (c)  2023-2024  Xiaomi Corporation (authors: Fangjun Kuang)
+//
+'use strict'
+
+const wasmModule = require('./sherpa-ncnn-wasm-main.js')();
+const sherpa_ncnn = require('./sherpa-ncnn.js');
+
+function createRecognizer(config) {
+  return sherpa_ncnn.createRecognizer(wasmModule, config);
+}
+
+module.exports = {
+  createRecognizer,
+};

+ 36 - 0
scripts/nodejs/package.json

@@ -0,0 +1,36 @@
+{
+  "name": "sherpa-ncnn",
+  "version": "SHERPA_NCNN_VERSION",
+  "description": "Real-time speech recognition with Next-gen Kaldi",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/k2-fsa/sherpa-ncnn.git"
+  },
+  "keywords": [
+    "speech-to-text",
+    "real-time speech recognition",
+    "without internet connection",
+    "embedded systems",
+    "open source",
+    "zipformer",
+    "asr",
+    "speech",
+    "WebAssembly",
+    "wasm",
+    "local",
+    "privacy",
+    "ncnn"
+  ],
+  "author": "The next-gen Kaldi team",
+  "license": "Apache-2.0",
+  "bugs": {
+    "url": "https://github.com/k2-fsa/sherpa-ncnn/issues"
+  },
+  "homepage": "https://github.com/k2-fsa/sherpa-ncnn#readme",
+  "dependencies": {
+  }
+}

+ 24 - 4
wasm/CMakeLists.txt

@@ -2,7 +2,7 @@ if(NOT $ENV{SHERPA_NCNN_IS_USING_BUILD_WASM_SH})
   message(FATAL_ERROR "Please use ./build-wasm.sh to build for wasm")
 endif()
 
-if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/assets/decoder_jit_trace-pnnx.ncnn.bin")
+if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/assets/decoder_jit_trace-pnnx.ncnn.bin" AND NOT SHERPA_NCNN_ENABLE_WASM_FOR_NODEJS)
   message(WARNING "${CMAKE_CURRENT_SOURCE_DIR}/assets/decoder_jit_trace-pnnx.ncnn.bin does not exist")
   message(FATAL_ERROR "Please read ${CMAKE_CURRENT_SOURCE_DIR}/assets/README.md before you continue")
 endif()
@@ -31,8 +31,20 @@ list(JOIN mangled_exported_functions "," all_exported_functions)
 include_directories(${CMAKE_SOURCE_DIR})
 set(MY_FLAGS " -s FORCE_FILESYSTEM=1 -s INITIAL_MEMORY=512MB ")
 string(APPEND MY_FLAGS " -sEXPORTED_FUNCTIONS=[_CopyHeap,_malloc,_free,${all_exported_functions}] ")
-string(APPEND MY_FLAGS "--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/assets@. ")
-string(APPEND MY_FLAGS " -sEXPORTED_RUNTIME_METHODS=['ccall','stringToUTF8','setValue','getValue'] ")
+
+if(SHERPA_NCNN_ENABLE_WASM_FOR_NODEJS)
+  string(APPEND MY_FLAGS " -sNODERAWFS=1 ")
+else()
+  string(APPEND MY_FLAGS "--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/assets@. ")
+endif()
+
+string(APPEND MY_FLAGS " -sEXPORTED_RUNTIME_METHODS=['ccall','stringToUTF8','setValue','getValue','lengthBytesUTF8','UTF8ToString'] ")
+
+if(SHERPA_NCNN_ENABLE_WASM_FOR_NODEJS)
+  string(APPEND MY_FLAGS " -sMODULARIZE=1 -sWASM_ASYNC_COMPILATION=0 ")
+endif()
+
+
 message(STATUS "MY_FLAGS: ${MY_FLAGS}")
 
 set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${MY_FLAGS}")
@@ -50,7 +62,15 @@ install(
     "index.html"
     "$<TARGET_FILE_DIR:sherpa-ncnn-wasm-main>/sherpa-ncnn-wasm-main.js"
     "$<TARGET_FILE_DIR:sherpa-ncnn-wasm-main>/sherpa-ncnn-wasm-main.wasm"
-    "$<TARGET_FILE_DIR:sherpa-ncnn-wasm-main>/sherpa-ncnn-wasm-main.data"
   DESTINATION
     bin/wasm
 )
+
+if(NOT SHERPA_NCNN_ENABLE_WASM_FOR_NODEJS)
+  install(
+    FILES
+      "$<TARGET_FILE_DIR:sherpa-ncnn-wasm-main>/sherpa-ncnn-wasm-main.data"
+    DESTINATION
+      bin/wasm
+  )
+endif()

+ 1 - 1
wasm/app.js

@@ -45,7 +45,7 @@ Module.onRuntimeInitialized = function() {
 
   startBtn.disabled = false;
 
-  recognizer = createRecognizer();
+  recognizer = createRecognizer(Module);
   console.log('recognizer is created!', recognizer);
 };
 

+ 80 - 68
wasm/sherpa-ncnn.js

@@ -1,51 +1,51 @@
 
 
-function freeConfig(config) {
+function freeConfig(config, Module) {
   if ('buffer' in config) {
-    _free(config.buffer);
+    Module._free(config.buffer);
   }
-  _free(config.ptr);
+  Module._free(config.ptr);
 }
 
 // The user should free the returned pointers
-function initSherpaNcnnModelConfig(config) {
-  let encoderParamLen = lengthBytesUTF8(config.encoderParam) + 1;
-  let decoderParamLen = lengthBytesUTF8(config.decoderParam) + 1;
-  let joinerParamLen = lengthBytesUTF8(config.joinerParam) + 1;
+function initSherpaNcnnModelConfig(config, Module) {
+  let encoderParamLen = Module.lengthBytesUTF8(config.encoderParam) + 1;
+  let decoderParamLen = Module.lengthBytesUTF8(config.decoderParam) + 1;
+  let joinerParamLen = Module.lengthBytesUTF8(config.joinerParam) + 1;
 
-  let encoderBinLen = lengthBytesUTF8(config.encoderBin) + 1;
-  let decoderBinLen = lengthBytesUTF8(config.decoderBin) + 1;
-  let joinerBinLen = lengthBytesUTF8(config.joinerBin) + 1;
+  let encoderBinLen = Module.lengthBytesUTF8(config.encoderBin) + 1;
+  let decoderBinLen = Module.lengthBytesUTF8(config.decoderBin) + 1;
+  let joinerBinLen = Module.lengthBytesUTF8(config.joinerBin) + 1;
 
-  let tokensLen = lengthBytesUTF8(config.tokens) + 1;
+  let tokensLen = Module.lengthBytesUTF8(config.tokens) + 1;
 
   let n = encoderParamLen + decoderParamLen + joinerParamLen;
   n += encoderBinLen + decoderBinLen + joinerBinLen;
   n += tokensLen;
 
-  let buffer = _malloc(n);
-  let ptr = _malloc(4 * 9);
+  let buffer = Module._malloc(n);
+  let ptr = Module._malloc(4 * 9);
 
   let offset = 0;
-  stringToUTF8(config.encoderParam, buffer + offset, encoderParamLen);
+  Module.stringToUTF8(config.encoderParam, buffer + offset, encoderParamLen);
   offset += encoderParamLen;
 
-  stringToUTF8(config.encoderBin, buffer + offset, encoderBinLen);
+  Module.stringToUTF8(config.encoderBin, buffer + offset, encoderBinLen);
   offset += encoderBinLen;
 
-  stringToUTF8(config.decoderParam, buffer + offset, decoderParamLen);
+  Module.stringToUTF8(config.decoderParam, buffer + offset, decoderParamLen);
   offset += decoderParamLen;
 
-  stringToUTF8(config.decoderBin, buffer + offset, decoderBinLen);
+  Module.stringToUTF8(config.decoderBin, buffer + offset, decoderBinLen);
   offset += decoderBinLen;
 
-  stringToUTF8(config.joinerParam, buffer + offset, joinerParamLen);
+  Module.stringToUTF8(config.joinerParam, buffer + offset, joinerParamLen);
   offset += joinerParamLen;
 
-  stringToUTF8(config.joinerBin, buffer + offset, joinerBinLen);
+  Module.stringToUTF8(config.joinerBin, buffer + offset, joinerBinLen);
   offset += joinerBinLen;
 
-  stringToUTF8(config.tokens, buffer + offset, tokensLen);
+  Module.stringToUTF8(config.tokens, buffer + offset, tokensLen);
   offset += tokensLen;
 
   offset = 0;
@@ -78,12 +78,12 @@ function initSherpaNcnnModelConfig(config) {
   }
 }
 
-function initSherpaNcnnDecoderConfig(config) {
-  let n = lengthBytesUTF8(config.decodingMethod) + 1;
-  let buffer = _malloc(n);
-  let ptr = _malloc(4 * 2);
+function initSherpaNcnnDecoderConfig(config, Module) {
+  let n = Module.lengthBytesUTF8(config.decodingMethod) + 1;
+  let buffer = Module._malloc(n);
+  let ptr = Module._malloc(4 * 2);
 
-  stringToUTF8(config.decodingMethod, buffer, n);
+  Module.stringToUTF8(config.decodingMethod, buffer, n);
 
   Module.setValue(ptr, buffer, 'i8*');
   Module.setValue(ptr + 4, config.numActivePaths, 'i32');
@@ -93,8 +93,8 @@ function initSherpaNcnnDecoderConfig(config) {
   }
 }
 
-function initSherpaNcnnFeatureExtractorConfig(config) {
-  let ptr = _malloc(4 * 2);
+function initSherpaNcnnFeatureExtractorConfig(config, Module) {
+  let ptr = Module._malloc(4 * 2);
   Module.setValue(ptr, config.samplingRate, 'float');
   Module.setValue(ptr + 4, config.featureDim, 'i32');
   return {
@@ -102,23 +102,24 @@ function initSherpaNcnnFeatureExtractorConfig(config) {
   }
 }
 
-function initSherpaNcnnRecognizerConfig(config) {
-  let featConfig = initSherpaNcnnFeatureExtractorConfig(config.featConfig);
-  let modelConfig = initSherpaNcnnModelConfig(config.modelConfig);
-  let decoderConfig = initSherpaNcnnDecoderConfig(config.decoderConfig);
+function initSherpaNcnnRecognizerConfig(config, Module) {
+  let featConfig =
+      initSherpaNcnnFeatureExtractorConfig(config.featConfig, Module);
+  let modelConfig = initSherpaNcnnModelConfig(config.modelConfig, Module);
+  let decoderConfig = initSherpaNcnnDecoderConfig(config.decoderConfig, Module);
 
   let numBytes =
       featConfig.len + modelConfig.len + decoderConfig.len + 4 * 4 + 4 * 2;
 
-  let ptr = _malloc(numBytes);
+  let ptr = Module._malloc(numBytes);
   let offset = 0;
-  _CopyHeap(featConfig.ptr, featConfig.len, ptr + offset);
+  Module._CopyHeap(featConfig.ptr, featConfig.len, ptr + offset);
   offset += featConfig.len;
 
-  _CopyHeap(modelConfig.ptr, modelConfig.len, ptr + offset)
+  Module._CopyHeap(modelConfig.ptr, modelConfig.len, ptr + offset)
   offset += modelConfig.len;
 
-  _CopyHeap(decoderConfig.ptr, decoderConfig.len, ptr + offset)
+  Module._CopyHeap(decoderConfig.ptr, decoderConfig.len, ptr + offset)
   offset += decoderConfig.len;
 
   Module.setValue(ptr + offset, config.enableEndpoint, 'i32');
@@ -146,17 +147,18 @@ function initSherpaNcnnRecognizerConfig(config) {
 }
 
 class Stream {
-  constructor(handle) {
+  constructor(handle, Module) {
     this.handle = handle;
     this.pointer = null;
-    this.n = 0
+    this.n = 0;
+    this.Module = Module;
   }
 
   free() {
     if (this.handle) {
-      _DestroyStream(this.handle);
+      this.Module._DestroyStream(this.handle);
       this.handle = null;
-      _free(this.pointer);
+      this.Module._free(this.pointer);
       this.pointer = null;
       this.n = 0;
     }
@@ -168,13 +170,15 @@ class Stream {
    */
   acceptWaveform(sampleRate, samples) {
     if (this.n < samples.length) {
-      _free(this.pointer);
-      this.pointer = _malloc(samples.length * samples.BYTES_PER_ELEMENT);
+      this.Module._free(this.pointer);
+      this.pointer =
+          this.Module._malloc(samples.length * samples.BYTES_PER_ELEMENT);
       this.n = samples.length
     }
 
-    Module.HEAPF32.set(samples, this.pointer / samples.BYTES_PER_ELEMENT);
-    _AcceptWaveform(this.handle, sampleRate, this.pointer, samples.length);
+    this.Module.HEAPF32.set(samples, this.pointer / samples.BYTES_PER_ELEMENT);
+    this.Module._AcceptWaveform(
+        this.handle, sampleRate, this.pointer, samples.length);
   }
 
   inputFinished() {
@@ -183,59 +187,56 @@ class Stream {
 };
 
 class Recognizer {
-  constructor(configObj, borrowedHandle) {
-    if (borrowedHandle) {
-      this.handle = borrowedHandle;
-      return;
-    }
-
-    let config = initSherpaNcnnRecognizerConfig(configObj)
-    let handle = _CreateRecognizer(config.ptr);
+  constructor(configObj, Module) {
+    this.config = configObj;
+    let config = initSherpaNcnnRecognizerConfig(configObj, Module)
+    let handle = Module._CreateRecognizer(config.ptr);
 
-    freeConfig(config.featConfig);
-    freeConfig(config.modelConfig);
-    freeConfig(config.decoderConfig);
-    freeConfig(config);
+    freeConfig(config.featConfig, Module);
+    freeConfig(config.modelConfig, Module);
+    freeConfig(config.decoderConfig, Module);
+    freeConfig(config, Module);
 
     this.handle = handle;
+    this.Module = Module;
   }
 
   free() {
-    _DestroyRecognizer(this.handle);
+    this.Module._DestroyRecognizer(this.handle);
     this.handle = 0
   }
 
   createStream() {
-    let handle = _CreateStream(this.handle);
-    return new Stream(handle);
+    let handle = this.Module._CreateStream(this.handle);
+    return new Stream(handle, this.Module);
   }
 
   isReady(stream) {
-    return _IsReady(this.handle, stream.handle) == 1;
+    return this.Module._IsReady(this.handle, stream.handle) == 1;
   }
 
   isEndpoint(stream) {
-    return _IsEndpoint(this.handle, stream.handle) == 1;
+    return this.Module._IsEndpoint(this.handle, stream.handle) == 1;
   }
 
   decode(stream) {
-    return _Decode(this.handle, stream.handle);
+    return this.Module._Decode(this.handle, stream.handle);
   }
 
   reset(stream) {
-    _Reset(this.handle, stream.handle);
+    this.Module._Reset(this.handle, stream.handle);
   }
 
   getResult(stream) {
-    let r = _GetResult(this.handle, stream.handle);
-    let textPtr = getValue(r, 'i8*');
-    let text = UTF8ToString(textPtr);
-    _DestroyResult(r);
+    let r = this.Module._GetResult(this.handle, stream.handle);
+    let textPtr = this.Module.getValue(r, 'i8*');
+    let text = this.Module.UTF8ToString(textPtr);
+    this.Module._DestroyResult(r);
     return text;
   }
 }
 
-function createRecognizer() {
+function createRecognizer(Module, myConfig) {
   let modelConfig = {
     encoderParam: './encoder_jit_trace-pnnx.ncnn.param',
     encoderBin: './encoder_jit_trace-pnnx.ncnn.bin',
@@ -268,5 +269,16 @@ function createRecognizer() {
     rule3MinUtternceLength: 20,
   };
 
-  return new Recognizer(configObj);
+  if (myConfig) {
+    configObj = myConfig;
+  }
+
+  return new Recognizer(configObj, Module);
+}
+
+if (typeof process == 'object' && typeof process.versions == 'object' &&
+    typeof process.versions.node == 'string') {
+  module.exports = {
+    createRecognizer,
+  };
 }