Add JavaScript API (WebAssembly) for Moonshine v2 models (#3241)

This pull request expands the sherpa-onnx library's capabilities by adding comprehensive support for Moonshine v2 models within its JavaScript/WebAssembly API. This includes necessary modifications to model configurations, the introduction of a new example for practical usage, and updates to the continuous integration pipeline to ensure stability and correctness.
This commit is contained in:
Fangjun Kuang authored and GitHub committed 2026-02-28 12:21:37 +08:00
1 parent 8519d61296
commit d9e3fcf17d
10 files changed
+117 -40

No files matched your search

+10
View File
@@ -9,6 +9,16 @@ git status
ls -lh
ls -lh node_modules
echo "---test moonshine v2---"
curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27.tar.bz2
tar xvf sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27.tar.bz2
rm sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27.tar.bz2
node ./test-offline-moonshine-v2.js
rm -rf sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27
echo "---test FireRedASR CTC---"
curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-fire-red-asr2-ctc-zh_en-int8-2026-02-25.tar.bz2
tar xvf sherpa-onnx-fire-red-asr2-ctc-zh_en-int8-2026-02-25.tar.bz2
+16
View File
@@ -422,6 +422,22 @@ rm sherpa-onnx-fire-red-asr-large-zh_en-2025-02-16.tar.bz2
node ./test-offline-fire-red-asr.js
```
## ./test-offline-moonshine-v2.js
[./test-offline-moonshine-v2.js](./test-offline-moonshine-v2.js) demonstrates
how to decode a file with a Moonshine v2 model. In the code we use
[sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27](https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27.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-moonshine-tiny-en-quantized-2026-02-27.tar.bz2
tar xvf sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27.tar.bz2
rm sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27.tar.bz2
node ./test-offline-moonshine-v2.js
```
## ./test-offline-moonshine.js
[./test-offline-moonshine.js](./test-offline-moonshine.js) demonstrates
+2 -2
View File
@@ -21,8 +21,8 @@ function createOfflineRecognizer() {
return sherpa_onnx.createOfflineRecognizer(config);
}
recognizer = createOfflineRecognizer();
stream = recognizer.createStream();
const recognizer = createOfflineRecognizer();
const stream = recognizer.createStream();
const waveFilename =
'./sherpa-onnx-fire-red-asr-large-zh_en-2025-02-16/test_wavs/0.wav';
@@ -0,0 +1,36 @@
// Copyright (c) 2023-2026 Xiaomi Corporation (authors: Fangjun Kuang)
//
const sherpa_onnx = require('sherpa-onnx');
function createOfflineRecognizer() {
let modelConfig = {
moonshine: {
encoder:
'./sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27/encoder_model.ort',
mergedDecoder:
'./sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27/decoder_model_merged.ort',
},
tokens: './sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27/tokens.txt',
};
let config = {
modelConfig: modelConfig,
};
return sherpa_onnx.createOfflineRecognizer(config);
}
const recognizer = createOfflineRecognizer();
const stream = recognizer.createStream();
const waveFilename =
'./sherpa-onnx-moonshine-tiny-en-quantized-2026-02-27/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();
+2 -2
View File
@@ -22,8 +22,8 @@ function createOfflineRecognizer() {
return sherpa_onnx.createOfflineRecognizer(config);
}
recognizer = createOfflineRecognizer();
stream = recognizer.createStream();
const recognizer = createOfflineRecognizer();
const stream = recognizer.createStream();
const waveFilename = './sherpa-onnx-moonshine-tiny-en-int8/test_wavs/0.wav';
const wave = sherpa_onnx.readWave(waveFilename);
@@ -18,7 +18,7 @@ function createOfflineSpeechDenoiser() {
return sherpa_onnx.createOfflineSpeechDenoiser(config);
}
speech_denoiser = createOfflineSpeechDenoiser();
const speech_denoiser = createOfflineSpeechDenoiser();
const waveFilename = './inp_16k.wav';
const wave = sherpa_onnx.readWave(waveFilename);
+2 -2
View File
@@ -24,8 +24,8 @@ function createOfflineRecognizer() {
return sherpa_onnx.createOfflineRecognizer(config);
}
recognizer = createOfflineRecognizer();
stream = recognizer.createStream();
const recognizer = createOfflineRecognizer();
const stream = recognizer.createStream();
const waveFilename = './sherpa-onnx-whisper-tiny.en/test_wavs/0.wav';
const wave = sherpa_onnx.readWave(waveFilename);
@@ -1,11 +1,15 @@
// Copyright (c) 2023 Xiaomi Corporation (authors: Fangjun Kuang)
const mic = require('mic'); // It uses `mic` for better compatibility, do check its [npm](https://www.npmjs.com/package/mic) before running it.
const mic = require(
'mic'); // It uses `mic` for better compatibility, do check its
// [npm](https://www.npmjs.com/package/mic) before running it.
const sherpa_onnx = require('sherpa-onnx');
function createOnlineRecognizer() {
let onlineParaformerModelConfig = {
encoder: './sherpa-onnx-streaming-paraformer-bilingual-zh-en/encoder.int8.onnx',
decoder: './sherpa-onnx-streaming-paraformer-bilingual-zh-en/decoder.int8.onnx',
encoder:
'./sherpa-onnx-streaming-paraformer-bilingual-zh-en/encoder.int8.onnx',
decoder:
'./sherpa-onnx-streaming-paraformer-bilingual-zh-en/decoder.int8.onnx',
};
let onlineModelConfig = {
@@ -62,32 +66,32 @@ class SpeechSession {
}
shouldStartNewSession() {
return Date.now() - this.lastUpdateTime > 10000; // 10 seconds of silence
return Date.now() - this.lastUpdateTime > 10000; // 10 seconds of silence
}
}
function formatOutput() {
clearConsole();
console.log('\n=== Automated Speech Recognition ===');
console.log(`Current Session #${sessionCount}`);
console.log('Time:', new Date().toLocaleTimeString());
clearConsole();
console.log('\n=== Automated Speech Recognition ===');
console.log(`Current Session #${sessionCount}`);
console.log('Time:', new Date().toLocaleTimeString());
console.log('------------------------');
// 显示历史句子
if (currentSession.sentences.length > 0) {
console.log('Recognized Sentences:');
currentSession.sentences.forEach((sentence, index) => {
console.log(`[${sentence.timestamp}] ${index + 1}. ${sentence.text}`);
});
console.log('------------------------');
// 显示历史句子
if (currentSession.sentences.length > 0) {
console.log('Recognized Sentences:');
currentSession.sentences.forEach((sentence, index) => {
console.log(`[${sentence.timestamp}] ${index + 1}. ${sentence.text}`);
});
console.log('------------------------');
}
// 显示当前正在识别的内容
if (currentSession.currentText) {
console.log('Recognizing:', currentSession.currentText);
}
}
// 显示当前正在识别的内容
if (currentSession.currentText) {
console.log('Recognizing:', currentSession.currentText);
}
}
const recognizer = createOnlineRecognizer();
const stream = recognizer.createStream();
@@ -113,7 +117,7 @@ function exitHandler(options, exitCode) {
const micInstance = mic({
rate: recognizer.config.featConfig.sampleRate,
channels: 1,
debug: false, // 关闭调试输出
debug: false, // 关闭调试输出
device: 'default',
bitwidth: 16,
encoding: 'signed-integer',
@@ -129,12 +133,12 @@ function startMic() {
console.log('Mic phone started.');
resolve();
});
micInputStream.once('error', (err) => {
console.error('Mic phone start error:', err);
reject(err);
});
micInstance.start();
});
}
@@ -142,7 +146,7 @@ function startMic() {
micInputStream.on('data', buffer => {
const int16Array = new Int16Array(buffer.buffer);
const samples = new Float32Array(int16Array.length);
for (let i = 0; i < int16Array.length; i++) {
samples[i] = int16Array[i] / 32768.0;
}
@@ -163,7 +167,7 @@ micInputStream.on('data', buffer => {
sessionCount++;
currentSession = new SpeechSession();
}
currentSession.addOrUpdateText(text);
formatOutput();
}
@@ -203,4 +207,4 @@ async function main() {
}
}
main();
main();
+13 -3
View File
@@ -1050,12 +1050,14 @@ function initSherpaOnnxOfflineMoonshineModelConfig(config, Module) {
Module.lengthBytesUTF8(config.uncachedDecoder || '') + 1;
const cachedDecoderLen =
Module.lengthBytesUTF8(config.cachedDecoder || '') + 1;
const mergedDecoderLen =
Module.lengthBytesUTF8(config.mergedDecoder || '') + 1;
const n =
preprocessorLen + encoderLen + uncachedDecoderLen + cachedDecoderLen;
const n = preprocessorLen + encoderLen + uncachedDecoderLen +
cachedDecoderLen + mergedDecoderLen;
const buffer = Module._malloc(n);
const len = 4 * 4; // 4 pointers
const len = 5 * 4; // 5 pointers
const ptr = Module._malloc(len);
let offset = 0;
@@ -1074,6 +1076,10 @@ function initSherpaOnnxOfflineMoonshineModelConfig(config, Module) {
config.cachedDecoder || '', buffer + offset, cachedDecoderLen);
offset += cachedDecoderLen;
Module.stringToUTF8(
config.mergedDecoder || '', buffer + offset, mergedDecoderLen);
offset += mergedDecoderLen;
offset = 0;
Module.setValue(ptr, buffer + offset, 'i8*');
offset += preprocessorLen;
@@ -1087,6 +1093,9 @@ function initSherpaOnnxOfflineMoonshineModelConfig(config, Module) {
Module.setValue(ptr + 12, buffer + offset, 'i8*');
offset += cachedDecoderLen;
Module.setValue(ptr + 16, buffer + offset, 'i8*');
offset += mergedDecoderLen;
return {
buffer: buffer,
ptr: ptr,
@@ -1288,6 +1297,7 @@ function initSherpaOnnxOfflineModelConfig(config, Module) {
encoder: '',
uncachedDecoder: '',
cachedDecoder: '',
mergedDecoder: '',
};
}
+2 -1
View File
@@ -23,7 +23,7 @@ static_assert(sizeof(SherpaOnnxOfflineDolphinModelConfig) == 4, "");
static_assert(sizeof(SherpaOnnxOfflineNemoEncDecCtcModelConfig) == 4, "");
static_assert(sizeof(SherpaOnnxOfflineWhisperModelConfig) == 7 * 4, "");
static_assert(sizeof(SherpaOnnxOfflineFireRedAsrModelConfig) == 2 * 4, "");
static_assert(sizeof(SherpaOnnxOfflineMoonshineModelConfig) == 4 * 4, "");
static_assert(sizeof(SherpaOnnxOfflineMoonshineModelConfig) == 5 * 4, "");
static_assert(sizeof(SherpaOnnxOfflineTdnnModelConfig) == 4, "");
static_assert(sizeof(SherpaOnnxOfflineSenseVoiceModelConfig) == 3 * 4, "");
static_assert(sizeof(SherpaOnnxOfflineCanaryModelConfig) == 5 * 4, "");
@@ -130,6 +130,7 @@ void PrintOfflineRecognizerConfig(SherpaOnnxOfflineRecognizerConfig *config) {
fprintf(stdout, "encoder: %s\n", moonshine->encoder);
fprintf(stdout, "uncached_decoder: %s\n", moonshine->uncached_decoder);
fprintf(stdout, "cached_decoder: %s\n", moonshine->cached_decoder);
fprintf(stdout, "merged_decoder: %s\n", moonshine->merged_decoder);
fprintf(stdout, "----------offline FireRedAsr model config----------\n");
fprintf(stdout, "encoder: %s\n", fire_red_asr->encoder);