2025 iThome鐵人賽
「 Flutter :30天打造念佛App,跨平台從Mobile到VR,讓極樂世界在眼前實現 ! 」
Day 19
「 Flutter 語音辨識 深入應用篇 — 生活在地球的勇者啊,阿彌陀佛怎麼念呀?(4)」
很開心從Day10開始和大家一起踏上flutter語音辨識的冒險旅程!
speech to text 原生平台語音辨識 ⭢ google STT 雲端語音辨識 ⭢
sherpa onnx 地端語音辨識,接著深入語音轉文字的完整流程與基礎原理,
並且探究關鍵字偵測與熱詞增強的實作應用。
今天是核心功能語音轉文字系列的最後一篇文章,
首先會向大家說明念佛App的「ASR開發選擇」,
接著深入sherpa onnx比較「KWS和ASR辨識/偵測相同字詞時的準確度差異」,
最後進行「移除不用的模型檔及壓縮檔」。
Day19 文章目錄:
一、ASR選擇
二、準確度實測
三、模型檔清理
1. speech to text :不適用持續性語音辨識,
Android、iOS可能會遇到使用時長及次數的限制等狀況,
所以就沒有應用在念佛App。
2. google STT : API費用
使用的時間長度和人數都會影響,長期下來可能是一筆不小的費用,
所以就暫時放備案,因為期待能長期穩定運營維護念佛App。
3. sherpa onnx : 開源、可離線、功能完善、但中文僅支援簡體
支援離線是我最主要的考量點,因為離線代表:
(1)有些長輩、使用者就算沒有行動網路,只要在有wifi的環境先下載好App、模型,也能隨時隨地去使用。
(2)如果使用者想要減少網路的訊息干擾,也可以在關閉網路的情況下使用。
所以目前念佛App是採用sherpa_onnx開發ASR。
1.「 108聲阿彌陀佛 」測試結果
點擊觀看 KWS 準確度測試
KWS 在此次測試的連續音訊中有出現漏撿(104/108聲)
點擊觀看 ASR 準確度測試
ASR 在此次測試的連續音訊中準確度較高(108/108聲)
2.權重設置
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
const customKeywords = <String>[
'ā m í t uó f ó :2.5 #0.04 @阿彌陀佛',
'ā m ī t uó f ó :2.3 #0.05 @阿彌陀佛',
'ā m í t uó f o :2.3 #0.04 @阿彌陀佛',
'ā m ī t uó f o :2.1 #0.06 @阿彌陀佛',
'ē m í t uó f ó :2.3 #0.05 @阿彌陀佛',
'ē m ī t uó f ó :2.3 #0.05 @阿彌陀佛',
'ē m í t uó f o :2.1 #0.06 @阿彌陀佛',
'ē m ī t uó f o :2.1 #0.06 @阿彌陀佛',
];
Future<String> writeCustomKeywords(String modelRoot) async {
final file = File(path.join(modelRoot, 'keywords_custom.txt'));
await file.writeAsString(customKeywords.join('\n') + '\n', encoding: utf8);
return file.path;
}
final config = sherpa_onnx.OnlineRecognizerConfig(
model: localModelConfig,
ruleFsts: '',
decodingMethod: 'modified_beam_search',
hotwordsFile: hotwordsPath,
hotwordsScore: 6,
);
1. 解壓完成刪除壓縮檔
Future<void> _unzipDownloadedFile(
String zipFilePath,
String destinationPath,
BuildContext context,
) async {
final downloadModel = Provider.of<DownloadModel>(context, listen: false);
downloadModel.setUnzipProgress(0.1);
final result = await compute(
_decompressInIsolate,
UnzipParams(zipFilePath, destinationPath),
);
downloadModel.setUnzipProgress(0.4);
final files = result[1] as List<ArchiveFile>;
final totalFiles = files.length;
int processedFiles = 0;
for (final file in files) {
await compute(_extractFileInIsolate, {
'file': file,
'destinationPath': destinationPath,
});
processedFiles++;
final progress = 0.4 + (0.6 * processedFiles / totalFiles);
if (processedFiles % 10 == 0) {
await Future.delayed(const Duration(milliseconds: 1));
}
downloadModel.setUnzipProgress(progress);
}
downloadModel.setProgress(1.0);
downloadModel.setUnzipProgress(1.0);
//解壓成功後刪掉壓縮檔
try {
final f = File(zipFilePath);
if (await f.exists()) {
await f.delete();
}
} catch (_) {
}
if (Navigator.canPop(context)) {
Navigator.of(context).pop();
_showSuccessDialog(context);
}
}
2. 清除不用的模型檔(可選)
(1) 對照 OnlineTransducerModelConfig:encoder、decoder、joiner
case "icefall-asr-zipformer-streaming-wenetspeech-20230615":
return sherpa_onnx.OnlineModelConfig(
transducer: sherpa_onnx.OnlineTransducerModelConfig(
encoder: '$modelDir/exp/encoder-epoch-12-avg-4-chunk-16-left-128.int8.onnx',
decoder: '$modelDir/exp/decoder-epoch-12-avg-4-chunk-16-left-128.onnx',
joiner: '$modelDir/exp/joiner-epoch-12-avg-4-chunk-16-left-128.onnx'
),
tokens: '$modelDir/tokens.txt',
modelType: 'zipformer',
);
(2) 刪除不用的模型檔
import 'dart:io';
import 'package:path/path.dart' ;
import 'package:flutter/foundation.dart';
/// 只刪指定檔案
Future<void> deleteSpecificFiles(
String modelRoot,
List<String> deleteRelPaths, {
bool dryRun = false,
}) async {
for (final rel in deleteRelPaths) {
final abs = normalize(join(modelRoot, rel));
// 確保在 modelRoot
final inRoot = isWithin(modelRoot, abs);
if (!inRoot) continue;
final f = File(abs);
if (await f.exists()) {
if (dryRun) {
if (kDebugMode) debugPrint('[dryRun] would delete: $abs');
} else {
try {
await f.delete();
if (kDebugMode) debugPrint('deleted: $abs');
} catch (e) {
if (kDebugMode) debugPrint('delete failed: $abs -> $e');
}
}
}
}
}
/// 各模型的「要刪除」清單
const Map<String, List<String>> _deleteMap = {
// WenetSpeech ASR(20230615)
'icefall-asr-zipformer-streaming-wenetspeech-20230615': [
'exp/encoder-epoch-12-avg-4-chunk-16-left-128.onnx',
'exp/decoder-epoch-12-avg-4-chunk-16-left-128.int8.onnx',
'exp/joiner-epoch-12-avg-4-chunk-16-left-128.int8.onnx'
],
// KWS(3.3M mobile)
'sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01-mobile': [
'encoder-epoch-12-avg-2-chunk-16-left-64.int8.onnx'
],
};
List<String> deleteListFor(String modelName) => _deleteMap[modelName] ?? const [];
///依照 modelName 刪除指定檔
Future<void> deleteSpecificFilesForModel({
required String modelName,
required String modelRoot,
bool dryRun = false,
}) async {
final list = deleteListFor(modelName);
if (list.isEmpty) return;
await deleteSpecificFiles(modelRoot, list, dryRun: dryRun);
}
(3) 解壓縮完成後進行刪除
Future<void> _unzipDownloadedFile(
String zipFilePath,
String destinationPath,
BuildContext context,
) async {
//解壓過程同前一段
//解壓縮完成
downloadModel.setProgress(1.0);
downloadModel.setUnzipProgress(1.0);
//刪除壓縮檔
try {
final f = File(zipFilePath);
if (await f.exists()) {
await f.delete();
}
} catch (_) {}
//刪除指定模型檔
final modelRootName = basenameWithoutExtension(
basenameWithoutExtension(zipFilePath),
);
final modelRoot = join(destinationPath, modelRootName);
await deleteSpecificFilesForModel(
modelName: downloadModel.modelName,
modelRoot: modelRoot,
dryRun: false, // 第一次跑先開啟 true,console確認刪除項目無誤再關閉
);
if (Navigator.canPop(context)) {
Navigator.of(context).pop();
_showSuccessDialog(context);
}
}
重點 | 內容 |
---|---|
ASR選擇 | sherpa onnx 開源可離線 |
準確度實測 | 此次實測的ASR準確度較高 |
模型檔清理 | 減少儲存空間佔用 |