feat: integrate Sherpa-ONNX local offline speech recognition engine
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"provider": "compatible",
|
"provider": "compatible",
|
||||||
"endpoint": "https://codex.slcydia.fun/v1/responses",
|
"endpoint": "https://kmwq8ckvr0ehsgqyudcer1.slcydia.fun/v1/responses",
|
||||||
"model": "gemini-3.7-flash-high",
|
"model": "gemini-3.7-flash-high",
|
||||||
"reasoningEffort": "low",
|
"reasoningEffort": "low",
|
||||||
"apiKey": "sk-242EMNuXYjxSEktp91E8QqS8ejGs9XImrDddIA5JHXdeCKLSUcB91vrSmhyv45pf",
|
"apiKey": "sk-242EMNuXYjxSEktp91E8QqS8ejGs9XImrDddIA5JHXdeCKLSUcB91vrSmhyv45pf",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,502 @@
|
|||||||
|
<blk> 0
|
||||||
|
<sos/eos> 1
|
||||||
|
<unk> 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
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'sherpa_stt_service.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
@@ -118,6 +119,12 @@ class AiService {
|
|||||||
required String endpoint,
|
required String endpoint,
|
||||||
required String model,
|
required String model,
|
||||||
}) async {
|
}) 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;
|
if (provider == AiProviderType.mock) return null;
|
||||||
final file = File(filePath);
|
final file = File(filePath);
|
||||||
if (!await file.exists()) return null;
|
if (!await file.exists()) return null;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ import 'local_store.dart';
|
|||||||
import 'seed_courses.dart';
|
import 'seed_courses.dart';
|
||||||
import 'ai_config.dart';
|
import 'ai_config.dart';
|
||||||
import 'ai_service.dart';
|
import 'ai_service.dart';
|
||||||
|
import 'sherpa_stt_service.dart';
|
||||||
|
|
||||||
class AppState extends ChangeNotifier {
|
class AppState extends ChangeNotifier {
|
||||||
static const _storageKey = 'learning_state_v1';
|
static const _storageKey = 'learning_state_v1';
|
||||||
@@ -270,6 +272,7 @@ class AppState extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
try {
|
try {
|
||||||
|
unawaited(SherpaSttService.instance.initialize());
|
||||||
final config = await AiConfigFile.loadFromAsset();
|
final config = await AiConfigFile.loadFromAsset();
|
||||||
if (config != null) {
|
if (config != null) {
|
||||||
if (config.apiKey != null && config.apiKey!.trim().isNotEmpty) {
|
if (config.apiKey != null && config.apiKey!.trim().isNotEmpty) {
|
||||||
|
|||||||
@@ -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<bool> 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<String?> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,8 +77,12 @@ class VoiceService {
|
|||||||
if (!await recordings.exists()) await recordings.create(recursive: true);
|
if (!await recordings.exists()) await recordings.create(recursive: true);
|
||||||
final timestamp = DateTime.now().microsecondsSinceEpoch;
|
final timestamp = DateTime.now().microsecondsSinceEpoch;
|
||||||
await _recorder.start(
|
await _recorder.start(
|
||||||
const RecordConfig(encoder: AudioEncoder.aacLc),
|
const RecordConfig(
|
||||||
path: '${recordings.path}/practice_$timestamp.m4a',
|
encoder: AudioEncoder.wav,
|
||||||
|
sampleRate: 16000,
|
||||||
|
numChannels: 1,
|
||||||
|
),
|
||||||
|
path: '${recordings.path}/practice_$timestamp.wav',
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -118,7 +122,7 @@ class VoiceService {
|
|||||||
if (!await recordings.exists()) return const [];
|
if (!await recordings.exists()) return const [];
|
||||||
final files = await recordings
|
final files = await recordings
|
||||||
.list()
|
.list()
|
||||||
.where((item) => item is File && item.path.endsWith('.m4a'))
|
.where((item) => item is File && (item.path.endsWith('.wav') || item.path.endsWith('.m4a')))
|
||||||
.cast<File>()
|
.cast<File>()
|
||||||
.toList();
|
.toList();
|
||||||
files.sort((left, right) => right.path.compareTo(left.path));
|
files.sort((left, right) => right.path.compareTo(left.path));
|
||||||
|
|||||||
@@ -624,6 +624,86 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
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:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ dependencies:
|
|||||||
shared_preferences: ^2.5.4
|
shared_preferences: ^2.5.4
|
||||||
sqlite3_flutter_libs: ^0.5.39
|
sqlite3_flutter_libs: ^0.5.39
|
||||||
speech_to_text: ^7.3.0
|
speech_to_text: ^7.3.0
|
||||||
|
sherpa_onnx: ^1.13.8
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -32,3 +33,4 @@ flutter:
|
|||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
assets:
|
assets:
|
||||||
- assets/config/
|
- assets/config/
|
||||||
|
- assets/models/sherpa/
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user