diff --git a/kouyu_english/assets/config/ai_config.json b/kouyu_english/assets/config/ai_config.json index 0fd6cb4..b7b6c40 100644 --- a/kouyu_english/assets/config/ai_config.json +++ b/kouyu_english/assets/config/ai_config.json @@ -1,6 +1,6 @@ { "provider": "compatible", - "endpoint": "https://codex.slcydia.fun/v1/responses", + "endpoint": "https://kmwq8ckvr0ehsgqyudcer1.slcydia.fun/v1/responses", "model": "gemini-3.7-flash-high", "reasoningEffort": "low", "apiKey": "sk-242EMNuXYjxSEktp91E8QqS8ejGs9XImrDddIA5JHXdeCKLSUcB91vrSmhyv45pf", diff --git a/kouyu_english/assets/models/sherpa/decoder-epoch-99-avg-1.int8.onnx b/kouyu_english/assets/models/sherpa/decoder-epoch-99-avg-1.int8.onnx new file mode 100644 index 0000000..88b09a6 Binary files /dev/null and b/kouyu_english/assets/models/sherpa/decoder-epoch-99-avg-1.int8.onnx differ diff --git a/kouyu_english/assets/models/sherpa/encoder-epoch-99-avg-1.int8.onnx b/kouyu_english/assets/models/sherpa/encoder-epoch-99-avg-1.int8.onnx new file mode 100644 index 0000000..673248a Binary files /dev/null and b/kouyu_english/assets/models/sherpa/encoder-epoch-99-avg-1.int8.onnx differ diff --git a/kouyu_english/assets/models/sherpa/joiner-epoch-99-avg-1.int8.onnx b/kouyu_english/assets/models/sherpa/joiner-epoch-99-avg-1.int8.onnx new file mode 100644 index 0000000..a5146b0 Binary files /dev/null and b/kouyu_english/assets/models/sherpa/joiner-epoch-99-avg-1.int8.onnx differ diff --git a/kouyu_english/assets/models/sherpa/tokens.txt b/kouyu_english/assets/models/sherpa/tokens.txt new file mode 100644 index 0000000..b4d1bf8 --- /dev/null +++ b/kouyu_english/assets/models/sherpa/tokens.txt @@ -0,0 +1,502 @@ + 0 + 1 + 2 +S 3 +▁THE 4 +▁A 5 +T 6 +▁AND 7 +ED 8 +▁OF 9 +▁TO 10 +E 11 +D 12 +N 13 +ING 14 +▁IN 15 +Y 16 +M 17 +C 18 +▁I 19 +A 20 +P 21 +▁HE 22 +R 23 +O 24 +L 25 +RE 26 +I 27 +U 28 +ER 29 +▁IT 30 +LY 31 +▁THAT 32 +▁WAS 33 +▁ 34 +▁S 35 +AR 36 +▁BE 37 +F 38 +▁C 39 +IN 40 +B 41 +▁FOR 42 +OR 43 +LE 44 +' 45 +▁HIS 46 +▁YOU 47 +AL 48 +▁RE 49 +V 50 +▁B 51 +G 52 +RI 53 +▁E 54 +▁WITH 55 +▁T 56 +▁AS 57 +LL 58 +▁P 59 +▁HER 60 +ST 61 +▁HAD 62 +▁SO 63 +▁F 64 +W 65 +CE 66 +▁IS 67 +ND 68 +▁NOT 69 +TH 70 +▁BUT 71 +EN 72 +▁SHE 73 +▁ON 74 +VE 75 +ON 76 +SE 77 +▁DE 78 +UR 79 +▁G 80 +CH 81 +K 82 +TER 83 +▁AT 84 +IT 85 +▁ME 86 +RO 87 +NE 88 +RA 89 +ES 90 +IL 91 +NG 92 +IC 93 +▁NO 94 +▁HIM 95 +ENT 96 +IR 97 +▁WE 98 +H 99 +▁DO 100 +▁ALL 101 +▁HAVE 102 +LO 103 +▁BY 104 +▁MY 105 +▁MO 106 +▁THIS 107 +LA 108 +▁ST 109 +▁WHICH 110 +▁CON 111 +▁THEY 112 +CK 113 +TE 114 +▁SAID 115 +▁FROM 116 +▁GO 117 +▁WHO 118 +▁TH 119 +▁OR 120 +▁D 121 +▁W 122 +VER 123 +LI 124 +▁SE 125 +▁ONE 126 +▁CA 127 +▁AN 128 +▁LA 129 +▁WERE 130 +EL 131 +▁HA 132 +▁MAN 133 +▁FA 134 +▁EX 135 +AD 136 +▁SU 137 +RY 138 +▁MI 139 +AT 140 +▁BO 141 +▁WHEN 142 +AN 143 +THER 144 +PP 145 +ATION 146 +▁FI 147 +▁WOULD 148 +▁PRO 149 +OW 150 +ET 151 +▁O 152 +▁THERE 153 +▁HO 154 +ION 155 +▁WHAT 156 +▁FE 157 +▁PA 158 +US 159 +MENT 160 +▁MA 161 +UT 162 +▁OUT 163 +▁THEIR 164 +▁IF 165 +▁LI 166 +▁K 167 +▁WILL 168 +▁ARE 169 +ID 170 +▁RO 171 +DE 172 +TION 173 +▁WA 174 +PE 175 +▁UP 176 +▁SP 177 +▁PO 178 +IGHT 179 +▁UN 180 +RU 181 +▁LO 182 +AS 183 +OL 184 +▁LE 185 +▁BEEN 186 +▁SH 187 +▁RA 188 +▁SEE 189 +KE 190 +UL 191 +TED 192 +▁SA 193 +UN 194 +UND 195 +ANT 196 +▁NE 197 +IS 198 +▁THEM 199 +CI 200 +GE 201 +▁COULD 202 +▁DIS 203 +OM 204 +ISH 205 +HE 206 +EST 207 +▁SOME 208 +ENCE 209 +ITY 210 +IVE 211 +▁US 212 +▁MORE 213 +▁EN 214 +ARD 215 +ATE 216 +▁YOUR 217 +▁INTO 218 +▁KNOW 219 +▁CO 220 +ANCE 221 +▁TIME 222 +▁WI 223 +▁YE 224 +AGE 225 +▁NOW 226 +TI 227 +FF 228 +ABLE 229 +▁VERY 230 +▁LIKE 231 +AM 232 +HI 233 +Z 234 +▁OTHER 235 +▁THAN 236 +▁LITTLE 237 +▁DID 238 +▁LOOK 239 +TY 240 +ERS 241 +▁CAN 242 +▁CHA 243 +▁AR 244 +X 245 +FUL 246 +UGH 247 +▁BA 248 +▁DAY 249 +▁ABOUT 250 +TEN 251 +IM 252 +▁ANY 253 +▁PRE 254 +▁OVER 255 +IES 256 +NESS 257 +ME 258 +BLE 259 +▁M 260 +ROW 261 +▁HAS 262 +▁GREAT 263 +▁VI 264 +TA 265 +▁AFTER 266 +PER 267 +▁AGAIN 268 +HO 269 +SH 270 +▁UPON 271 +▁DI 272 +▁HAND 273 +▁COM 274 +IST 275 +TURE 276 +▁STA 277 +▁THEN 278 +▁SHOULD 279 +▁GA 280 +OUS 281 +OUR 282 +▁WELL 283 +▁ONLY 284 +MAN 285 +▁GOOD 286 +▁TWO 287 +▁MAR 288 +▁SAY 289 +▁HU 290 +TING 291 +▁OUR 292 +RESS 293 +▁DOWN 294 +IOUS 295 +▁BEFORE 296 +▁DA 297 +▁NA 298 +QUI 299 +▁MADE 300 +▁EVERY 301 +▁OLD 302 +▁EVEN 303 +IG 304 +▁COME 305 +▁GRA 306 +▁RI 307 +▁LONG 308 +OT 309 +SIDE 310 +WARD 311 +▁FO 312 +▁WHERE 313 +MO 314 +LESS 315 +▁SC 316 +▁MUST 317 +▁NEVER 318 +▁HOW 319 +▁CAME 320 +▁SUCH 321 +▁RU 322 +▁TAKE 323 +▁WO 324 +▁CAR 325 +UM 326 +AK 327 +▁THINK 328 +▁MUCH 329 +▁MISTER 330 +▁MAY 331 +▁JO 332 +▁WAY 333 +▁COMP 334 +▁THOUGHT 335 +▁STO 336 +▁MEN 337 +▁BACK 338 +▁DON 339 +J 340 +▁LET 341 +▁TRA 342 +▁FIRST 343 +▁JUST 344 +▁VA 345 +▁OWN 346 +▁PLA 347 +▁MAKE 348 +ATED 349 +▁HIMSELF 350 +▁WENT 351 +▁PI 352 +GG 353 +RING 354 +▁DU 355 +▁MIGHT 356 +▁PART 357 +▁GIVE 358 +▁IMP 359 +▁BU 360 +▁PER 361 +▁PLACE 362 +▁HOUSE 363 +▁THROUGH 364 +IAN 365 +▁SW 366 +▁UNDER 367 +QUE 368 +▁AWAY 369 +▁LOVE 370 +QUA 371 +▁LIFE 372 +▁GET 373 +▁WITHOUT 374 +▁PASS 375 +▁TURN 376 +IGN 377 +▁HEAD 378 +▁MOST 379 +▁THOSE 380 +▁SHALL 381 +▁EYES 382 +▁COL 383 +▁STILL 384 +▁NIGHT 385 +▁NOTHING 386 +ITION 387 +HA 388 +▁TELL 389 +▁WORK 390 +▁LAST 391 +▁NEW 392 +▁FACE 393 +▁HI 394 +▁WORD 395 +▁FOUND 396 +▁COUNT 397 +▁OB 398 +▁WHILE 399 +▁SHA 400 +▁MEAN 401 +▁SAW 402 +▁PEOPLE 403 +▁FRIEND 404 +▁THREE 405 +▁ROOM 406 +▁SAME 407 +▁THOUGH 408 +▁RIGHT 409 +▁CHILD 410 +▁FATHER 411 +▁ANOTHER 412 +▁HEART 413 +▁WANT 414 +▁TOOK 415 +OOK 416 +▁LIGHT 417 +▁MISSUS 418 +▁OPEN 419 +▁JU 420 +▁ASKED 421 +PORT 422 +▁LEFT 423 +▁JA 424 +▁WORLD 425 +▁HOME 426 +▁WHY 427 +▁ALWAYS 428 +▁ANSWER 429 +▁SEEMED 430 +▁SOMETHING 431 +▁GIRL 432 +▁BECAUSE 433 +▁NAME 434 +▁TOLD 435 +▁NI 436 +▁HIGH 437 +IZE 438 +▁WOMAN 439 +▁FOLLOW 440 +▁RETURN 441 +▁KNEW 442 +▁EACH 443 +▁KIND 444 +▁JE 445 +▁ACT 446 +▁LU 447 +▁CERTAIN 448 +▁YEARS 449 +▁QUITE 450 +▁APPEAR 451 +▁BETTER 452 +▁HALF 453 +▁PRESENT 454 +▁PRINCE 455 +SHIP 456 +▁ALSO 457 +▁BEGAN 458 +▁HAVING 459 +▁ENOUGH 460 +▁PERSON 461 +▁LADY 462 +▁WHITE 463 +▁COURSE 464 +▁VOICE 465 +▁SPEAK 466 +▁POWER 467 +▁MORNING 468 +▁BETWEEN 469 +▁AMONG 470 +▁KEEP 471 +▁WALK 472 +▁MATTER 473 +▁TEA 474 +▁BELIEVE 475 +▁SMALL 476 +▁TALK 477 +▁FELT 478 +▁HORSE 479 +▁MYSELF 480 +▁SIX 481 +▁HOWEVER 482 +▁FULL 483 +▁HERSELF 484 +▁POINT 485 +▁STOOD 486 +▁HUNDRED 487 +▁ALMOST 488 +▁SINCE 489 +▁LARGE 490 +▁LEAVE 491 +▁PERHAPS 492 +▁DARK 493 +▁SUDDEN 494 +▁REPLIED 495 +▁ANYTHING 496 +▁WONDER 497 +▁UNTIL 498 +Q 499 +#0 500 +#1 501 diff --git a/kouyu_english/lib/core/ai_service.dart b/kouyu_english/lib/core/ai_service.dart index 5c6e2a2..7d3bf74 100644 --- a/kouyu_english/lib/core/ai_service.dart +++ b/kouyu_english/lib/core/ai_service.dart @@ -1,3 +1,4 @@ +import 'sherpa_stt_service.dart'; import 'dart:io'; import 'dart:convert'; @@ -118,6 +119,12 @@ class AiService { required String endpoint, required String model, }) async { + if (filePath.toLowerCase().endsWith('.wav')) { + final localText = await SherpaSttService.instance.transcribeWav(filePath); + if (localText != null && localText.trim().isNotEmpty) { + return localText.trim(); + } + } if (provider == AiProviderType.mock) return null; final file = File(filePath); if (!await file.exists()) return null; diff --git a/kouyu_english/lib/core/app_state.dart b/kouyu_english/lib/core/app_state.dart index 572df2b..e0ebd39 100644 --- a/kouyu_english/lib/core/app_state.dart +++ b/kouyu_english/lib/core/app_state.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -11,6 +12,7 @@ import 'local_store.dart'; import 'seed_courses.dart'; import 'ai_config.dart'; import 'ai_service.dart'; +import 'sherpa_stt_service.dart'; class AppState extends ChangeNotifier { static const _storageKey = 'learning_state_v1'; @@ -270,6 +272,7 @@ class AppState extends ChangeNotifier { Future load() async { try { + unawaited(SherpaSttService.instance.initialize()); final config = await AiConfigFile.loadFromAsset(); if (config != null) { if (config.apiKey != null && config.apiKey!.trim().isNotEmpty) { diff --git a/kouyu_english/lib/core/sherpa_stt_service.dart b/kouyu_english/lib/core/sherpa_stt_service.dart new file mode 100644 index 0000000..7bb0b58 --- /dev/null +++ b/kouyu_english/lib/core/sherpa_stt_service.dart @@ -0,0 +1,135 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:sherpa_onnx/sherpa_onnx.dart' as sherpa_onnx; + +class SherpaSttService { + SherpaSttService._(); + static final instance = SherpaSttService._(); + + sherpa_onnx.OfflineRecognizer? _recognizer; + bool _isInitialized = false; + bool _isInitializing = false; + + bool get isReady => _isInitialized && _recognizer != null; + + /// Initializes Sherpa-ONNX bindings and unpacks bundled model assets to local disk if needed. + Future initialize({String? nativeLibDir}) async { + if (_isInitialized) return true; + if (_isInitializing) return false; + _isInitializing = true; + + try { + try { + sherpa_onnx.initBindings(nativeLibDir); + } catch (e) { + debugPrint('[SherpaSttService] initBindings warning: $e'); + } + + final docDir = await getApplicationDocumentsDirectory(); + final modelDir = Directory('${docDir.path}/sherpa_models'); + if (!await modelDir.exists()) { + await modelDir.create(recursive: true); + } + + final modelFiles = [ + 'encoder-epoch-99-avg-1.int8.onnx', + 'decoder-epoch-99-avg-1.int8.onnx', + 'joiner-epoch-99-avg-1.int8.onnx', + 'tokens.txt', + ]; + + for (final filename in modelFiles) { + final targetFile = File('${modelDir.path}/$filename'); + if (!await targetFile.exists() || (await targetFile.length()) == 0) { + final ByteData data = await rootBundle.load('assets/models/sherpa/$filename'); + final Uint8List bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); + await targetFile.writeAsBytes(bytes, flush: true); + } + } + + final modelConfig = sherpa_onnx.OfflineModelConfig( + transducer: sherpa_onnx.OfflineTransducerModelConfig( + encoder: '${modelDir.path}/encoder-epoch-99-avg-1.int8.onnx', + decoder: '${modelDir.path}/decoder-epoch-99-avg-1.int8.onnx', + joiner: '${modelDir.path}/joiner-epoch-99-avg-1.int8.onnx', + ), + tokens: '${modelDir.path}/tokens.txt', + numThreads: 2, + debug: false, + ); + + final recognizerConfig = sherpa_onnx.OfflineRecognizerConfig( + model: modelConfig, + feat: const sherpa_onnx.FeatureConfig(sampleRate: 16000, featureDim: 80), + ); + + _recognizer = sherpa_onnx.OfflineRecognizer(recognizerConfig); + _isInitialized = true; + _isInitializing = false; + debugPrint('[SherpaSttService] Local ONNX ASR engine initialized successfully.'); + return true; + } catch (e, stack) { + debugPrint('[SherpaSttService] Failed to initialize local ASR engine: $e\n$stack'); + _isInitializing = false; + _isInitialized = false; + return false; + } + } + + /// Transcribes a local 16kHz mono WAV audio file. + Future transcribeWav(String wavPath) async { + try { + if (!_isInitialized) { + final ready = await initialize(); + if (!ready || _recognizer == null) return null; + } + + final file = File(wavPath); + if (!await file.exists()) { + debugPrint('[SherpaSttService] Audio file does not exist: $wavPath'); + return null; + } + + final wave = sherpa_onnx.readWave(wavPath); + if (wave.samples.isEmpty) { + debugPrint('[SherpaSttService] Read 0 wave samples from: $wavPath'); + return null; + } + + final stream = _recognizer!.createStream(); + stream.acceptWaveform(samples: wave.samples, sampleRate: wave.sampleRate); + _recognizer!.decode(stream); + final result = _recognizer!.getResult(stream); + stream.free(); + + final rawText = result.text.trim(); + if (rawText.isEmpty) return null; + + return _cleanText(rawText); + } catch (e) { + debugPrint('[SherpaSttService] Transcribe error: $e'); + return null; + } + } + + /// Cleans and formats raw recognized text into natural English casing. + String _cleanText(String text) { + if (text.isEmpty) return text; + // Lowercase first to normalize uppercase model output + final lower = text.toLowerCase().trim(); + if (lower.isEmpty) return lower; + // Capitalize the first letter + return lower[0].toUpperCase() + lower.substring(1); + } + + void dispose() { + try { + _recognizer?.free(); + } catch (_) {} + _recognizer = null; + _isInitialized = false; + } +} diff --git a/kouyu_english/lib/core/voice_service.dart b/kouyu_english/lib/core/voice_service.dart index dee8008..b6b5636 100644 --- a/kouyu_english/lib/core/voice_service.dart +++ b/kouyu_english/lib/core/voice_service.dart @@ -77,8 +77,12 @@ class VoiceService { if (!await recordings.exists()) await recordings.create(recursive: true); final timestamp = DateTime.now().microsecondsSinceEpoch; await _recorder.start( - const RecordConfig(encoder: AudioEncoder.aacLc), - path: '${recordings.path}/practice_$timestamp.m4a', + const RecordConfig( + encoder: AudioEncoder.wav, + sampleRate: 16000, + numChannels: 1, + ), + path: '${recordings.path}/practice_$timestamp.wav', ); return true; } @@ -118,7 +122,7 @@ class VoiceService { if (!await recordings.exists()) return const []; final files = await recordings .list() - .where((item) => item is File && item.path.endsWith('.m4a')) + .where((item) => item is File && (item.path.endsWith('.wav') || item.path.endsWith('.m4a'))) .cast() .toList(); files.sort((left, right) => right.path.compareTo(left.path)); diff --git a/kouyu_english/pubspec.lock b/kouyu_english/pubspec.lock index 394614f..9cd5a1f 100644 --- a/kouyu_english/pubspec.lock +++ b/kouyu_english/pubspec.lock @@ -624,6 +624,86 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + sherpa_onnx: + dependency: "direct main" + description: + name: sherpa_onnx + sha256: b7e65d5956f8c9213fd339c68ea058cdec7ad24bd35f9bfc5c18178d3d73b2f9 + url: "https://pub.dev" + source: hosted + version: "1.13.8" + sherpa_onnx_android_arm64: + dependency: transitive + description: + name: sherpa_onnx_android_arm64 + sha256: "29db5572735afb1bc29f0f95ab160f7fb38b4b3f926f48e9113457fd740264f4" + url: "https://pub.dev" + source: hosted + version: "1.13.8" + sherpa_onnx_android_armeabi: + dependency: transitive + description: + name: sherpa_onnx_android_armeabi + sha256: fe81837bd12f67d136a456c6d2b39876caaba09edd77761cb527cb170dac2d2a + url: "https://pub.dev" + source: hosted + version: "1.13.8" + sherpa_onnx_android_x86: + dependency: transitive + description: + name: sherpa_onnx_android_x86 + sha256: "8ddbd35982d4bb15648fc818253f2711be18448db50a2edb05c56a6c745fcea7" + url: "https://pub.dev" + source: hosted + version: "1.13.8" + sherpa_onnx_android_x86_64: + dependency: transitive + description: + name: sherpa_onnx_android_x86_64 + sha256: d20c06c0edc92286609f256d04c295cad510bbb270068c0c79877f4a10f00581 + url: "https://pub.dev" + source: hosted + version: "1.13.8" + sherpa_onnx_ios: + dependency: transitive + description: + name: sherpa_onnx_ios + sha256: "044e3a614d1847c0ff9b6475201c6e5d023cd6ca6cd17f14c16332081c01e1d2" + url: "https://pub.dev" + source: hosted + version: "1.13.8" + sherpa_onnx_linux: + dependency: transitive + description: + name: sherpa_onnx_linux + sha256: "22fb91a1c50b7bc24d93d0b67c87cf2a3a38f7564d7f14343d704a1201848960" + url: "https://pub.dev" + source: hosted + version: "1.13.8" + sherpa_onnx_macos: + dependency: transitive + description: + name: sherpa_onnx_macos + sha256: bdedc34ac0acccee7ae73a5599f66e0081137c00467c565a92f72b67428837bb + url: "https://pub.dev" + source: hosted + version: "1.13.8" + sherpa_onnx_web: + dependency: transitive + description: + name: sherpa_onnx_web + sha256: e25a3813eb080636280b23dd4b0098252902675158b66dceda1efba061adf5d7 + url: "https://pub.dev" + source: hosted + version: "1.13.8" + sherpa_onnx_windows: + dependency: transitive + description: + name: sherpa_onnx_windows + sha256: d91fb9c4baac2594cf1ae6955f5e9f51113d354de8c75543ea8481d431040a71 + url: "https://pub.dev" + source: hosted + version: "1.13.8" sky_engine: dependency: transitive description: flutter diff --git a/kouyu_english/pubspec.yaml b/kouyu_english/pubspec.yaml index a5f1f0a..9514e13 100644 --- a/kouyu_english/pubspec.yaml +++ b/kouyu_english/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: shared_preferences: ^2.5.4 sqlite3_flutter_libs: ^0.5.39 speech_to_text: ^7.3.0 + sherpa_onnx: ^1.13.8 dev_dependencies: flutter_test: @@ -32,3 +33,4 @@ flutter: uses-material-design: true assets: - assets/config/ + - assets/models/sherpa/ diff --git a/kouyu_english/test/sherpa_stt_test.dart b/kouyu_english/test/sherpa_stt_test.dart new file mode 100644 index 0000000..ab6fd04 --- /dev/null +++ b/kouyu_english/test/sherpa_stt_test.dart @@ -0,0 +1,49 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:kouyu_english/core/sherpa_stt_service.dart'; +import 'package:sherpa_onnx/sherpa_onnx.dart' as sherpa_onnx; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('SherpaSttService transcribes WAV audio file accurately', () async { + const macosDir = '/Users/shen/.pub-cache/hosted/pub.dev/sherpa_onnx_macos-1.13.8/macos'; + + // Test direct Sherpa ASR initialization and decoding with bundled test audio + sherpa_onnx.initBindings(macosDir); + + final modelConfig = sherpa_onnx.OfflineModelConfig( + transducer: const sherpa_onnx.OfflineTransducerModelConfig( + encoder: 'assets/models/sherpa/encoder-epoch-99-avg-1.int8.onnx', + decoder: 'assets/models/sherpa/decoder-epoch-99-avg-1.int8.onnx', + joiner: 'assets/models/sherpa/joiner-epoch-99-avg-1.int8.onnx', + ), + tokens: 'assets/models/sherpa/tokens.txt', + numThreads: 2, + debug: false, + ); + + final recognizerConfig = sherpa_onnx.OfflineRecognizerConfig( + model: modelConfig, + feat: const sherpa_onnx.FeatureConfig(sampleRate: 16000, featureDim: 80), + ); + + final recognizer = sherpa_onnx.OfflineRecognizer(recognizerConfig); + + const testWave = '/tmp/sherpa_test/sherpa-onnx-zipformer-small-en-2023-06-26/test_wavs/0.wav'; + if (File(testWave).existsSync()) { + final wave = sherpa_onnx.readWave(testWave); + expect(wave.samples.isNotEmpty, isTrue); + + final stream = recognizer.createStream(); + stream.acceptWaveform(samples: wave.samples, sampleRate: wave.sampleRate); + recognizer.decode(stream); + final result = recognizer.getResult(stream); + print("Transcribed result: ${result.text}"); + expect(result.text.contains("NIGHTFALL"), isTrue); + stream.free(); + } + recognizer.free(); + }); +}