iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0
Mobile Development

Flutter :30天打造念佛App,跨平台應用從Mobile到VR,讓極樂世界在眼前實現!系列 第 19

[ Day 19 ] Flutter 語音辨識 深入應用篇— 生活在地球的勇者啊,阿彌陀佛怎麼念呀(4) #KWS vs. ASR 準確度實測

  • 分享至 

  • xImage
  •  

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選擇
二、準確度實測
三、模型檔清理


一、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.權重設置

  • KWS 關鍵詞權重與觸發門檻
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;
}

  • ASR 熱詞權重
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);
  }
}


Day19 重點回顧

重點 內容
ASR選擇 sherpa onnx 開源可離線
準確度實測 此次實測的ASR準確度較高
模型檔清理 減少儲存空間佔用

上一篇
[ Day 18 ] Flutter 語音辨識 深入應用篇— 生活在地球的勇者啊,阿彌陀佛怎麼念呀(3) #ASR Hotwords #熱詞增強
下一篇
[ Day 20 ] Flutter 單元測試 — 專案必備的綠色乖乖,程式守門員登場!
系列文
Flutter :30天打造念佛App,跨平台應用從Mobile到VR,讓極樂世界在眼前實現!21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言