From 5ca2a2d265ea003b8bb3ca81f59ec3cba87b75c9 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Mon, 29 Dec 2025 11:48:59 +0800 Subject: [PATCH 1/2] Add JavaScript API (WebAssembly) for Google MedAsr model --- .github/scripts/test-nodejs-npm.sh | 8 +++++ nodejs-examples/README.md | 16 ++++++++++ nodejs-examples/test-offline-medasr-ctc.js | 35 ++++++++++++++++++++ wasm/asr/sherpa-onnx-asr.js | 37 ++++++++++++++++++++-- wasm/nodejs/sherpa-onnx-wasm-nodejs.cc | 8 ++++- 5 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 nodejs-examples/test-offline-medasr-ctc.js diff --git a/.github/scripts/test-nodejs-npm.sh b/.github/scripts/test-nodejs-npm.sh index 16f21b5ffb..0b929ca9e7 100755 --- a/.github/scripts/test-nodejs-npm.sh +++ b/.github/scripts/test-nodejs-npm.sh @@ -9,6 +9,14 @@ git status ls -lh ls -lh node_modules +curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-medasr-ctc-en-int8-2025-12-25.tar.bz2 +tar xvf sherpa-onnx-medasr-ctc-en-int8-2025-12-25.tar.bz2 +rm sherpa-onnx-medasr-ctc-en-int8-2025-12-25.tar.bz2 + +node ./test-offline-medasr-ctc.js + +rm -rf sherpa-onnx-medasr-ctc-en-int8-2025-12-25 + curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-omnilingual-asr-1600-languages-300M-ctc-int8-2025-11-12.tar.bz2 tar xvf sherpa-onnx-omnilingual-asr-1600-languages-300M-ctc-int8-2025-11-12.tar.bz2 rm sherpa-onnx-omnilingual-asr-1600-languages-300M-ctc-int8-2025-11-12.tar.bz2 diff --git a/nodejs-examples/README.md b/nodejs-examples/README.md index bc8c6832b1..72fba48b2a 100644 --- a/nodejs-examples/README.md +++ b/nodejs-examples/README.md @@ -203,6 +203,22 @@ rm sherpa-onnx-zipformer-ctc-zh-int8-2025-07-03.tar.bz2 node ./test-offline-zipformer-ctc.js ``` +## ./test-offline-medasr-ctc.js + +[./test-offline-medasr-ctc.js](./test-offline-medasr-ctc.js) demonstrates +how to decode a file with a Google MedASR CTC model. In the code we use +[sherpa-onnx-medasr-ctc-en-int8-2025-12-25.tar.bz2](https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-medasr-ctc-en-int8-2025-12-25.tar.bz2). + +You can use the following command to run it: + +```bash +wget https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-medasr-ctc-en-int8-2025-12-25.tar.bz2 +tar xvf sherpa-onnx-medasr-ctc-en-int8-2025-12-25.tar.bz2 +rm sherpa-onnx-medasr-ctc-en-int8-2025-12-25.tar.bz2 + +node ./test-offline-medasr-ctc.js +``` + ## ./test-offline-omnilingual-asr-ctc.js [./test-offline-omnilingual-asr-ctc.js](./test-offline-omnilingual-asr-ctc.js) demonstrates diff --git a/nodejs-examples/test-offline-medasr-ctc.js b/nodejs-examples/test-offline-medasr-ctc.js new file mode 100644 index 0000000000..e9070576a4 --- /dev/null +++ b/nodejs-examples/test-offline-medasr-ctc.js @@ -0,0 +1,35 @@ +// Copyright (c) 2025 Xiaomi Corporation (authors: Fangjun Kuang) +// +const fs = require('fs'); +const {Readable} = require('stream'); +const wav = require('wav'); + +const sherpa_onnx = require('sherpa-onnx'); + +function createOfflineRecognizer() { + let config = { + modelConfig: { + medasr: { + model: './sherpa-onnx-medasr-ctc-en-int8-2025-12-25/model.int8.onnx', + }, + tokens: './sherpa-onnx-medasr-ctc-en-int8-2025-12-25/tokens.txt', + } + }; + + return sherpa_onnx.createOfflineRecognizer(config); +} + +const recognizer = createOfflineRecognizer(); +const stream = recognizer.createStream(); + +const waveFilename = + './sherpa-onnx-medasr-ctc-en-int8-2025-12-25/test_wavs/0.wav'; +const wave = sherpa_onnx.readWave(waveFilename); +stream.acceptWaveform(wave.sampleRate, wave.samples); + +recognizer.decode(stream); +const text = recognizer.getResult(stream).text; +console.log(text); + +stream.free(); +recognizer.free(); diff --git a/wasm/asr/sherpa-onnx-asr.js b/wasm/asr/sherpa-onnx-asr.js index a77d61c1ea..876f947bd8 100644 --- a/wasm/asr/sherpa-onnx-asr.js +++ b/wasm/asr/sherpa-onnx-asr.js @@ -59,6 +59,10 @@ function freeConfig(config, Module) { freeConfig(config.omnilingual, Module) } + if ('medasr' in config) { + freeConfig(config.medasr, Module) + } + if ('moonshine' in config) { freeConfig(config.moonshine, Module) } @@ -776,6 +780,23 @@ function initSherpaOnnxOfflineOmnilingualAsrCtcModelConfig(config, Module) { } } +function initSherpaOnnxOfflineMedAsrCtcModelConfig(config, Module) { + const n = Module.lengthBytesUTF8(config.model || '') + 1; + + const buffer = Module._malloc(n); + + const len = 1 * 4; // 1 pointer + const ptr = Module._malloc(len); + + Module.stringToUTF8(config.model || '', buffer, n); + + Module.setValue(ptr, buffer, 'i8*'); + + return { + buffer: buffer, ptr: ptr, len: len, + } +} + function initSherpaOnnxOfflineWhisperModelConfig(config, Module) { const encoderLen = Module.lengthBytesUTF8(config.encoder || '') + 1; const decoderLen = Module.lengthBytesUTF8(config.decoder || '') + 1; @@ -1052,6 +1073,12 @@ function initSherpaOnnxOfflineModelConfig(config, Module) { }; } + if (!('medasr' in config)) { + config.medasr = { + model: '', + }; + } + if (!('whisper' in config)) { config.whisper = { encoder: '', @@ -1139,10 +1166,13 @@ function initSherpaOnnxOfflineModelConfig(config, Module) { const omnilingual = initSherpaOnnxOfflineOmnilingualAsrCtcModelConfig( config.omnilingual, Module); + const medasr = + initSherpaOnnxOfflineMedAsrCtcModelConfig(config.medasr, Module); + const len = transducer.len + paraformer.len + nemoCtc.len + whisper.len + tdnn.len + 8 * 4 + senseVoice.len + moonshine.len + fireRedAsr.len + dolphin.len + zipformerCtc.len + canary.len + wenetCtc.len + - omnilingual.len; + omnilingual.len + medasr.len; const ptr = Module._malloc(len); @@ -1256,12 +1286,15 @@ function initSherpaOnnxOfflineModelConfig(config, Module) { Module._CopyHeap(omnilingual.ptr, omnilingual.len, ptr + offset); offset += omnilingual.len; + Module._CopyHeap(medasr.ptr, medasr.len, ptr + offset); + offset += medasr.len; + return { buffer: buffer, ptr: ptr, len: len, transducer: transducer, paraformer: paraformer, nemoCtc: nemoCtc, whisper: whisper, tdnn: tdnn, senseVoice: senseVoice, moonshine: moonshine, fireRedAsr: fireRedAsr, dolphin: dolphin, zipformerCtc: zipformerCtc, canary: canary, - wenetCtc: wenetCtc, omnilingual: omnilingual + wenetCtc: wenetCtc, omnilingual: omnilingual, medasr: medasr } } diff --git a/wasm/nodejs/sherpa-onnx-wasm-nodejs.cc b/wasm/nodejs/sherpa-onnx-wasm-nodejs.cc index f5ded7013d..682a1cab9e 100644 --- a/wasm/nodejs/sherpa-onnx-wasm-nodejs.cc +++ b/wasm/nodejs/sherpa-onnx-wasm-nodejs.cc @@ -16,6 +16,7 @@ static_assert(sizeof(SherpaOnnxOfflineParaformerModelConfig) == 4, ""); static_assert(sizeof(SherpaOnnxOfflineZipformerCtcModelConfig) == 4, ""); static_assert(sizeof(SherpaOnnxOfflineWenetCtcModelConfig) == 4, ""); static_assert(sizeof(SherpaOnnxOfflineOmnilingualAsrCtcModelConfig) == 4, ""); +static_assert(sizeof(SherpaOnnxOfflineMedAsrCtcModelConfig) == 4, ""); static_assert(sizeof(SherpaOnnxOfflineDolphinModelConfig) == 4, ""); static_assert(sizeof(SherpaOnnxOfflineNemoEncDecCtcModelConfig) == 4, ""); static_assert(sizeof(SherpaOnnxOfflineWhisperModelConfig) == 5 * 4, ""); @@ -39,7 +40,8 @@ static_assert(sizeof(SherpaOnnxOfflineModelConfig) == sizeof(SherpaOnnxOfflineZipformerCtcModelConfig) + sizeof(SherpaOnnxOfflineCanaryModelConfig) + sizeof(SherpaOnnxOfflineWenetCtcModelConfig) + - sizeof(SherpaOnnxOfflineOmnilingualAsrCtcModelConfig), + sizeof(SherpaOnnxOfflineOmnilingualAsrCtcModelConfig) + + sizeof(SherpaOnnxOfflineMedAsrCtcModelConfig), ""); static_assert(sizeof(SherpaOnnxFeatureConfig) == 2 * 4, ""); @@ -89,6 +91,7 @@ void PrintOfflineRecognizerConfig(SherpaOnnxOfflineRecognizerConfig *config) { auto canary = &model_config->canary; auto wenet_ctc = &model_config->wenet_ctc; auto omnilingual = &model_config->omnilingual; + auto medasr = &model_config->medasr; fprintf(stdout, "----------offline transducer model config----------\n"); fprintf(stdout, "encoder: %s\n", transducer->encoder); @@ -145,6 +148,9 @@ void PrintOfflineRecognizerConfig(SherpaOnnxOfflineRecognizerConfig *config) { fprintf(stdout, "----------offline Omnilingual ASR model config----------\n"); fprintf(stdout, "model: %s\n", omnilingual->model); + fprintf(stdout, "----------offline MedASR model config----------\n"); + fprintf(stdout, "model: %s\n", medasr->model); + fprintf(stdout, "tokens: %s\n", model_config->tokens); fprintf(stdout, "num_threads: %d\n", model_config->num_threads); fprintf(stdout, "provider: %s\n", model_config->provider); From 462425d16daf568c4f4f9cb366a3bb7ad86e0fa2 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Mon, 29 Dec 2025 12:26:45 +0800 Subject: [PATCH 2/2] Fix CI errors --- nodejs-examples/test-offline-tts-matcha-en.js | 1 - 1 file changed, 1 deletion(-) diff --git a/nodejs-examples/test-offline-tts-matcha-en.js b/nodejs-examples/test-offline-tts-matcha-en.js index f5d1e6ded4..be1292430b 100644 --- a/nodejs-examples/test-offline-tts-matcha-en.js +++ b/nodejs-examples/test-offline-tts-matcha-en.js @@ -6,7 +6,6 @@ function createOfflineTts() { let offlineTtsMatchaModelConfig = { acousticModel: './matcha-icefall-en_US-ljspeech/model-steps-3.onnx', vocoder: './vocos-22khz-univ.onnx', - lexicon: './matcha-icefall-en_US-ljspeech/lexicon.txt', tokens: './matcha-icefall-en_US-ljspeech/tokens.txt', dataDir: './matcha-icefall-en_US-ljspeech/espeak-ng-data',