feat: upgrade local speech recognition engine to SenseVoice-Small with hardware audio enhancements

This commit is contained in:
shen
2026-09-15 22:36:10 +08:00
parent be5075fa3d
commit 042e867521
10 changed files with 25090 additions and 532 deletions
+22 -19
View File
@@ -15,7 +15,7 @@ class SherpaSttService {
bool get isReady => _isInitialized && _recognizer != null;
/// Initializes Sherpa-ONNX bindings and unpacks bundled model assets to local disk if needed.
/// Initializes SenseVoice-Small 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;
@@ -29,32 +29,30 @@ class SherpaSttService {
}
final docDir = await getApplicationDocumentsDirectory();
final modelDir = Directory('${docDir.path}/sherpa_models');
final modelDir = Directory('${docDir.path}/sense_voice_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',
'model.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 ByteData data = await rootBundle.load('assets/models/sense_voice/$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',
senseVoice: sherpa_onnx.OfflineSenseVoiceModelConfig(
model: '${modelDir.path}/model.int8.onnx',
language: 'auto',
useInverseTextNormalization: true,
),
tokens: '${modelDir.path}/tokens.txt',
numThreads: 2,
@@ -69,17 +67,17 @@ class SherpaSttService {
_recognizer = sherpa_onnx.OfflineRecognizer(recognizerConfig);
_isInitialized = true;
_isInitializing = false;
debugPrint('[SherpaSttService] Local ONNX ASR engine initialized successfully.');
debugPrint('[SherpaSttService] SenseVoice-Small ONNX ASR engine initialized successfully.');
return true;
} catch (e, stack) {
debugPrint('[SherpaSttService] Failed to initialize local ASR engine: $e\n$stack');
debugPrint('[SherpaSttService] Failed to initialize SenseVoice ASR engine: $e\n$stack');
_isInitializing = false;
_isInitialized = false;
return false;
}
}
/// Transcribes a local 16kHz mono WAV audio file.
/// Transcribes a local 16kHz mono WAV audio file using SenseVoice-Small.
Future<String?> transcribeWav(String wavPath) async {
try {
if (!_isInitialized) {
@@ -115,14 +113,19 @@ class SherpaSttService {
}
}
/// Cleans and formats raw recognized text into natural English casing.
/// Cleans and formats raw recognized SenseVoice text (strips emotion/event tags, normalizes 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);
// Strip SenseVoice special tags like <|zh|>, <|en|>, <|NEUTRAL|>, <|HAPPY|>, <|Speech|>, <|withitn|>, <|woitn|>, etc.
var cleaned = text.replaceAll(RegExp(r'<\|[a-zA-Z0-9_\-\s]+\|>'), '').trim();
if (cleaned.isEmpty) return cleaned;
// Normalize consecutive spaces
cleaned = cleaned.replaceAll(RegExp(r'\s+'), ' ').trim();
// Capitalize first character if it's a letter
if (cleaned.isNotEmpty && cleaned[0].toLowerCase() != cleaned[0].toUpperCase()) {
cleaned = cleaned[0].toUpperCase() + cleaned.substring(1);
}
return cleaned;
}
void dispose() {
@@ -81,6 +81,9 @@ class VoiceService {
encoder: AudioEncoder.wav,
sampleRate: 16000,
numChannels: 1,
noiseSuppress: true,
echoCancel: true,
autoGain: true,
),
path: '${recordings.path}/practice_$timestamp.wav',
);