Skip to content

Commit 67b2254

Browse files
authored
Add Dart API for FunASR Nano (k2-fsa#3055)
1 parent eeab397 commit 67b2254

6 files changed

Lines changed: 267 additions & 64 deletions

File tree

.github/scripts/test-dart.sh

Lines changed: 67 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -8,72 +8,12 @@ pushd spoken-language-identification
88
./run-whisper.sh
99
popd
1010

11-
pushd streaming-asr
12-
13-
echo '----------streaming T-one ctc----------'
14-
./run-t-one-ctc.sh
15-
rm -rf sherpa-onnx-*
16-
17-
echo '----------streaming zipformer ctc HLG----------'
18-
./run-zipformer-ctc-hlg.sh
19-
rm -rf sherpa-onnx-*
20-
21-
echo '----------streaming zipformer ctc----------'
22-
./run-zipformer-ctc.sh
23-
rm -rf sherpa-onnx-*
24-
25-
echo '----------streaming zipformer transducer----------'
26-
./run-zipformer-transducer-itn.sh
27-
./run-zipformer-transducer.sh
28-
rm -f itn*
29-
rm -rf sherpa-onnx-*
30-
31-
echo '----------streaming NeMo transducer----------'
32-
./run-nemo-transducer.sh
33-
rm -rf sherpa-onnx-*
34-
35-
echo '----------streaming paraformer----------'
36-
./run-paraformer.sh
37-
rm -rf sherpa-onnx-*
38-
39-
popd # streaming-asr
40-
41-
pushd tts
42-
43-
echo '----------matcha tts----------'
44-
./run-kitten-en.sh
45-
./run-kokoro-zh-en.sh
46-
./run-kokoro-en.sh
47-
./run-matcha-zh.sh
48-
./run-matcha-en.sh
49-
ls -lh *.wav
50-
rm -rf matcha-icefall-*
51-
rm *.onnx
52-
53-
echo '----------piper tts----------'
54-
./run-piper.sh
55-
rm -rf vits-piper-*
56-
57-
echo '----------coqui tts----------'
58-
./run-coqui.sh
59-
rm -rf vits-coqui-*
11+
pushd non-streaming-asr
6012

61-
echo '----------zh tts----------'
62-
./run-vits-zh.sh
13+
echo '----------FunASR Nano----------'
14+
./run-funasr-nano.sh
6315
rm -rf sherpa-onnx-*
6416

65-
ls -lh *.wav
66-
67-
popd # tts
68-
69-
pushd vad
70-
./run-ten-vad.sh
71-
./run.sh
72-
rm *.onnx
73-
popd
74-
75-
pushd non-streaming-asr
76-
7717
echo '----------MedASR CTC----------'
7818
./run-medasr-ctc.sh
7919
rm -rf sherpa-onnx-*
@@ -140,6 +80,70 @@ rm -rf sherpa-onnx-*
14080

14181
popd # non-streaming-asr
14282

83+
pushd streaming-asr
84+
85+
echo '----------streaming T-one ctc----------'
86+
./run-t-one-ctc.sh
87+
rm -rf sherpa-onnx-*
88+
89+
echo '----------streaming zipformer ctc HLG----------'
90+
./run-zipformer-ctc-hlg.sh
91+
rm -rf sherpa-onnx-*
92+
93+
echo '----------streaming zipformer ctc----------'
94+
./run-zipformer-ctc.sh
95+
rm -rf sherpa-onnx-*
96+
97+
echo '----------streaming zipformer transducer----------'
98+
./run-zipformer-transducer-itn.sh
99+
./run-zipformer-transducer.sh
100+
rm -f itn*
101+
rm -rf sherpa-onnx-*
102+
103+
echo '----------streaming NeMo transducer----------'
104+
./run-nemo-transducer.sh
105+
rm -rf sherpa-onnx-*
106+
107+
echo '----------streaming paraformer----------'
108+
./run-paraformer.sh
109+
rm -rf sherpa-onnx-*
110+
111+
popd # streaming-asr
112+
113+
pushd tts
114+
115+
echo '----------matcha tts----------'
116+
./run-kitten-en.sh
117+
./run-kokoro-zh-en.sh
118+
./run-kokoro-en.sh
119+
./run-matcha-zh.sh
120+
./run-matcha-en.sh
121+
ls -lh *.wav
122+
rm -rf matcha-icefall-*
123+
rm *.onnx
124+
125+
echo '----------piper tts----------'
126+
./run-piper.sh
127+
rm -rf vits-piper-*
128+
129+
echo '----------coqui tts----------'
130+
./run-coqui.sh
131+
rm -rf vits-coqui-*
132+
133+
echo '----------zh tts----------'
134+
./run-vits-zh.sh
135+
rm -rf sherpa-onnx-*
136+
137+
ls -lh *.wav
138+
139+
popd # tts
140+
141+
pushd vad
142+
./run-ten-vad.sh
143+
./run.sh
144+
rm *.onnx
145+
popd
146+
143147
pushd speech-enhancement-gtcrn
144148
echo "speech enhancement with gtcrn models"
145149
./run.sh
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) 2026 Xiaomi Corporation
2+
import 'dart:io';
3+
4+
import 'package:args/args.dart';
5+
import 'package:sherpa_onnx/sherpa_onnx.dart' as sherpa_onnx;
6+
7+
import './init.dart';
8+
9+
void main(List<String> arguments) async {
10+
await initSherpaOnnx();
11+
12+
final parser = ArgParser()
13+
..addOption('encoder-adaptor', help: 'Path to the encoder adaptor model')
14+
..addOption('llm', help: 'Path to the llm model')
15+
..addOption('embedding', help: 'Path to the embedding model')
16+
..addOption('tokenizer', help: 'Path to the tokenizer directory')
17+
..addOption('input-wav', help: 'Path to input.wav to transcribe');
18+
19+
final res = parser.parse(arguments);
20+
if (res['encoder-adaptor'] == null ||
21+
res['llm'] == null ||
22+
res['embedding'] == null ||
23+
res['tokenizer'] == null ||
24+
res['input-wav'] == null) {
25+
print(parser.usage);
26+
exit(1);
27+
}
28+
29+
final encoderAdaptor = res['encoder-adaptor'] as String;
30+
final llm = res['llm'] as String;
31+
final embedding = res['embedding'] as String;
32+
final tokenizer = res['tokenizer'] as String;
33+
final inputWav = res['input-wav'] as String;
34+
35+
final funasrNano = sherpa_onnx.OfflineFunAsrNanoModelConfig(
36+
encoderAdaptor: encoderAdaptor,
37+
llm: llm,
38+
embedding: embedding,
39+
tokenizer: tokenizer,
40+
);
41+
42+
final modelConfig = sherpa_onnx.OfflineModelConfig(
43+
funasrNano: funasrNano,
44+
tokens: '',
45+
debug: true,
46+
numThreads: 1,
47+
);
48+
final config = sherpa_onnx.OfflineRecognizerConfig(model: modelConfig);
49+
final recognizer = sherpa_onnx.OfflineRecognizer(config);
50+
51+
final waveData = sherpa_onnx.readWave(inputWav);
52+
final stream = recognizer.createStream();
53+
54+
stream.acceptWaveform(
55+
samples: waveData.samples,
56+
sampleRate: waveData.sampleRate,
57+
);
58+
recognizer.decode(stream);
59+
60+
final result = recognizer.getResult(stream);
61+
print(result.text);
62+
63+
stream.free();
64+
recognizer.free();
65+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
3+
set -ex
4+
5+
dart pub get
6+
7+
if [ ! -f ./sherpa-onnx-funasr-nano-int8-2025-12-30/embedding.int8.onnx ]; then
8+
curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-funasr-nano-int8-2025-12-30.tar.bz2
9+
tar xvf sherpa-onnx-funasr-nano-int8-2025-12-30.tar.bz2
10+
rm sherpa-onnx-funasr-nano-int8-2025-12-30.tar.bz2
11+
fi
12+
13+
dart run \
14+
./bin/funasr-nano.dart \
15+
--encoder-adaptor ./sherpa-onnx-funasr-nano-int8-2025-12-30/encoder_adaptor.int8.onnx \
16+
--llm ./sherpa-onnx-funasr-nano-int8-2025-12-30/llm.int8.onnx \
17+
--embedding ./sherpa-onnx-funasr-nano-int8-2025-12-30/embedding.int8.onnx \
18+
--tokenizer ./sherpa-onnx-funasr-nano-int8-2025-12-30/Qwen3-0.6B \
19+
--input-wav ./sherpa-onnx-funasr-nano-int8-2025-12-30/test_wavs/lyrics.wav

flutter/sherpa_onnx/lib/src/offline_recognizer.dart

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,65 @@ class OfflineMedAsrCtcModelConfig {
168168
final String model;
169169
}
170170

171+
class OfflineFunAsrNanoModelConfig {
172+
const OfflineFunAsrNanoModelConfig({
173+
this.encoderAdaptor = '',
174+
this.llm = '',
175+
this.embedding = '',
176+
this.tokenizer = '',
177+
this.systemPrompt = 'You are a helpful assistant.',
178+
this.userPrompt = '语音转写:',
179+
this.maxNewTokens = 512,
180+
this.temperature = 1e-6,
181+
this.topP = 0.8,
182+
this.seed = 42,
183+
});
184+
185+
factory OfflineFunAsrNanoModelConfig.fromJson(Map<String, dynamic> json) {
186+
return OfflineFunAsrNanoModelConfig(
187+
encoderAdaptor: json['encoderAdaptor'] as String? ?? '',
188+
llm: json['llm'] as String? ?? '',
189+
embedding: json['embedding'] as String? ?? '',
190+
tokenizer: json['tokenizer'] as String? ?? '',
191+
systemPrompt: json['systemPrompt'] as String? ?? '',
192+
userPrompt: json['userPrompt'] as String? ?? '',
193+
maxNewTokens: json['maxNewTokens'] as int? ?? 512,
194+
temperature: (json['temperature'] as num?)?.toDouble() ?? 1e-6,
195+
topP: (json['topP'] as num?)?.toDouble() ?? 0.8,
196+
seed: json['seed'] as int? ?? 42,
197+
);
198+
}
199+
200+
@override
201+
String toString() {
202+
return 'OfflineFunAsrNanoModelConfig(encoderAdaptor: $encoderAdaptor, llm: $llm, embedding: $embedding, tokenizer: $tokenizer, systemPrompt: $systemPrompt, userPrompt: $userPrompt, maxNewTokens: $maxNewTokens, temperature: $temperature, topP: $topP, seed: $seed)';
203+
}
204+
205+
Map<String, dynamic> toJson() => {
206+
'encoderAdaptor': encoderAdaptor,
207+
'llm': llm,
208+
'embedding': embedding,
209+
'tokenizer': tokenizer,
210+
'systemPrompt': systemPrompt,
211+
'userPrompt': userPrompt,
212+
'maxNewTokens': maxNewTokens,
213+
'temperature': temperature,
214+
'topP': topP,
215+
'seed': seed,
216+
};
217+
218+
final String encoderAdaptor;
219+
final String llm;
220+
final String embedding;
221+
final String tokenizer;
222+
final String systemPrompt;
223+
final String userPrompt;
224+
final int maxNewTokens;
225+
final double temperature;
226+
final double topP;
227+
final int seed;
228+
}
229+
171230
class OfflineWhisperModelConfig {
172231
const OfflineWhisperModelConfig({
173232
this.encoder = '',
@@ -388,6 +447,7 @@ class OfflineModelConfig {
388447
this.wenetCtc = const OfflineWenetCtcModelConfig(),
389448
this.omnilingual = const OfflineOmnilingualAsrCtcModelConfig(),
390449
this.medasr = const OfflineMedAsrCtcModelConfig(),
450+
this.funasrNano = const OfflineFunAsrNanoModelConfig(),
391451
required this.tokens,
392452
this.numThreads = 1,
393453
this.debug = true,
@@ -470,6 +530,11 @@ class OfflineModelConfig {
470530
json['medasr'] as Map<String, dynamic>,
471531
)
472532
: const OfflineMedAsrCtcModelConfig(),
533+
funasrNano: json['funasrNano'] != null
534+
? OfflineFunAsrNanoModelConfig.fromJson(
535+
json['funasrNano'] as Map<String, dynamic>,
536+
)
537+
: const OfflineFunAsrNanoModelConfig(),
473538
tokens: json['tokens'] as String,
474539
numThreads: json['numThreads'] as int? ?? 1,
475540
debug: json['debug'] as bool? ?? true,
@@ -483,7 +548,7 @@ class OfflineModelConfig {
483548

484549
@override
485550
String toString() {
486-
return 'OfflineModelConfig(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, medasr: $medasr, tokens: $tokens, numThreads: $numThreads, debug: $debug, provider: $provider, modelType: $modelType, modelingUnit: $modelingUnit, bpeVocab: $bpeVocab, telespeechCtc: $telespeechCtc)';
551+
return 'OfflineModelConfig(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, medasr: $medasr, funasrNano: $funasrNano, tokens: $tokens, numThreads: $numThreads, debug: $debug, provider: $provider, modelType: $modelType, modelingUnit: $modelingUnit, bpeVocab: $bpeVocab, telespeechCtc: $telespeechCtc)';
487552
}
488553

489554
Map<String, dynamic> toJson() => {
@@ -501,6 +566,7 @@ class OfflineModelConfig {
501566
'wenetCtc': wenetCtc.toJson(),
502567
'omnilingual': omnilingual.toJson(),
503568
'medasr': medasr.toJson(),
569+
'funasrNano': funasrNano.toJson(),
504570
'tokens': tokens,
505571
'numThreads': numThreads,
506572
'debug': debug,
@@ -525,6 +591,7 @@ class OfflineModelConfig {
525591
final OfflineWenetCtcModelConfig wenetCtc;
526592
final OfflineOmnilingualAsrCtcModelConfig omnilingual;
527593
final OfflineMedAsrCtcModelConfig medasr;
594+
final OfflineFunAsrNanoModelConfig funasrNano;
528595

529596
final String tokens;
530597
final int numThreads;
@@ -773,6 +840,25 @@ class OfflineRecognizer {
773840
.toNativeUtf8();
774841
c.ref.model.medasr.model = config.model.medasr.model.toNativeUtf8();
775842

843+
c.ref.model.funasrNano.encoderAdaptor = config
844+
.model
845+
.funasrNano
846+
.encoderAdaptor
847+
.toNativeUtf8();
848+
c.ref.model.funasrNano.llm = config.model.funasrNano.llm.toNativeUtf8();
849+
c.ref.model.funasrNano.embedding = config.model.funasrNano.embedding
850+
.toNativeUtf8();
851+
c.ref.model.funasrNano.tokenizer = config.model.funasrNano.tokenizer
852+
.toNativeUtf8();
853+
c.ref.model.funasrNano.systemPrompt = config.model.funasrNano.systemPrompt
854+
.toNativeUtf8();
855+
c.ref.model.funasrNano.userPrompt = config.model.funasrNano.userPrompt
856+
.toNativeUtf8();
857+
c.ref.model.funasrNano.maxNewTokens = config.model.funasrNano.maxNewTokens;
858+
c.ref.model.funasrNano.temperature = config.model.funasrNano.temperature;
859+
c.ref.model.funasrNano.topP = config.model.funasrNano.topP;
860+
c.ref.model.funasrNano.seed = config.model.funasrNano.seed;
861+
776862
c.ref.model.tokens = config.model.tokens.toNativeUtf8();
777863

778864
c.ref.model.numThreads = config.model.numThreads;
@@ -817,6 +903,12 @@ class OfflineRecognizer {
817903
calloc.free(c.ref.model.modelType);
818904
calloc.free(c.ref.model.provider);
819905
calloc.free(c.ref.model.tokens);
906+
calloc.free(c.ref.model.funasrNano.userPrompt);
907+
calloc.free(c.ref.model.funasrNano.systemPrompt);
908+
calloc.free(c.ref.model.funasrNano.tokenizer);
909+
calloc.free(c.ref.model.funasrNano.embedding);
910+
calloc.free(c.ref.model.funasrNano.llm);
911+
calloc.free(c.ref.model.funasrNano.encoderAdaptor);
820912
calloc.free(c.ref.model.medasr.model);
821913
calloc.free(c.ref.model.omnilingual.model);
822914
calloc.free(c.ref.model.wenetCtc.model);

0 commit comments

Comments
 (0)