Fangjun Kuang
Committed by GitHub

Add VAD+ASR demo for HarmonyOS (#1573)

正在显示 44 个修改的文件 包含 1102 行增加0 行删除
  1 +# Introduction
  2 +
  3 +- [./SherpaOnnxHar](./SherpaOnnxHar) It is for building `sherpa_onnx.har`.
  4 + If you don't need to change the C++ or Typescript code of sherpa-onnx, then
  5 + you can download pre-built `sherpa_onnx.har` from us. Please refer to
  6 + our [doc](https://k2-fsa.github.io/sherpa/onnx) for how to download it.
  7 +
  8 +- [./SherpaOnnxVadAsr](./SherpaOnnxVadAsr) It shows how to use
  9 + VAD + Non-streaming ASR for speech recognition.
  1 +/node_modules
  2 +/oh_modules
  3 +/local.properties
  4 +/.idea
  5 +**/build
  6 +/.hvigor
  7 +.cxx
  8 +/.clangd
  9 +/.clang-format
  10 +/.clang-tidy
  11 +**/.test
  12 +/.appanalyzer
  1 +{
  2 + "app": {
  3 + "bundleName": "com.k2fsa.sherpa.onnx.vad.asr",
  4 + "vendor": "example",
  5 + "versionCode": 1000000,
  6 + "versionName": "1.0.0",
  7 + "icon": "$media:app_icon",
  8 + "label": "$string:app_name"
  9 + }
  10 +}
  1 +{
  2 + "string": [
  3 + {
  4 + "name": "app_name",
  5 + "value": "SherpaOnnxVadAsr"
  6 + }
  7 + ]
  8 +}
  1 +{
  2 + "app": {
  3 + "signingConfigs": [],
  4 + "products": [
  5 + {
  6 + "name": "default",
  7 + "signingConfig": "default",
  8 + "compatibleSdkVersion": "4.0.0(10)",
  9 + "runtimeOS": "HarmonyOS",
  10 + "buildOption": {
  11 + "strictMode": {
  12 + "caseSensitiveCheck": true,
  13 + }
  14 + }
  15 + }
  16 + ],
  17 + "buildModeSet": [
  18 + {
  19 + "name": "debug",
  20 + },
  21 + {
  22 + "name": "release"
  23 + }
  24 + ]
  25 + },
  26 + "modules": [
  27 + {
  28 + "name": "entry",
  29 + "srcPath": "./entry",
  30 + "targets": [
  31 + {
  32 + "name": "default",
  33 + "applyToProducts": [
  34 + "default"
  35 + ]
  36 + }
  37 + ]
  38 + }
  39 + ]
  40 +}
  1 +{
  2 + "files": [
  3 + "**/*.ets"
  4 + ],
  5 + "ignore": [
  6 + "**/src/ohosTest/**/*",
  7 + "**/src/test/**/*",
  8 + "**/src/mock/**/*",
  9 + "**/node_modules/**/*",
  10 + "**/oh_modules/**/*",
  11 + "**/build/**/*",
  12 + "**/.preview/**/*"
  13 + ],
  14 + "ruleSet": [
  15 + "plugin:@performance/recommended",
  16 + "plugin:@typescript-eslint/recommended"
  17 + ],
  18 + "rules": {
  19 + }
  20 +}
  1 +/node_modules
  2 +/oh_modules
  3 +/.preview
  4 +/build
  5 +/.cxx
  6 +/.test
  7 +*.har
  1 +# Introduction
  2 +
  3 +Please download ./sherpa_onnx-v1.10.32.har
  4 +from <https://huggingface.co/csukuangfj/sherpa-onnx-harmony-os/tree/main/har>
  5 +
  6 +Hint: For users who have no access to huggingface, please use
  7 +<https://hf-mirror.com/csukuangfj/sherpa-onnx-harmony-os/tree/main/har>
  1 +{
  2 + "apiType": "stageMode",
  3 + "buildOption": {
  4 + "sourceOption": {
  5 + "workers": [
  6 + './src/main/ets/workers/NonStreamingAsrWithVadWorker.ets'
  7 + ]
  8 + }
  9 + },
  10 + "buildOptionSet": [
  11 + {
  12 + "name": "release",
  13 + "arkOptions": {
  14 + "obfuscation": {
  15 + "ruleOptions": {
  16 + "enable": false,
  17 + "files": [
  18 + "./obfuscation-rules.txt"
  19 + ]
  20 + }
  21 + }
  22 + }
  23 + },
  24 + ],
  25 + "targets": [
  26 + {
  27 + "name": "default"
  28 + },
  29 + {
  30 + "name": "ohosTest",
  31 + }
  32 + ]
  33 +}
  1 +import { hapTasks } from '@ohos/hvigor-ohos-plugin';
  2 +
  3 +export default {
  4 + system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
  5 + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */
  6 +}
  1 +# Define project specific obfuscation rules here.
  2 +# You can include the obfuscation configuration files in the current module's build-profile.json5.
  3 +#
  4 +# For more details, see
  5 +# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5
  6 +
  7 +# Obfuscation options:
  8 +# -disable-obfuscation: disable all obfuscations
  9 +# -enable-property-obfuscation: obfuscate the property names
  10 +# -enable-toplevel-obfuscation: obfuscate the names in the global scope
  11 +# -compact: remove unnecessary blank spaces and all line feeds
  12 +# -remove-log: remove all console.* statements
  13 +# -print-namecache: print the name cache that contains the mapping from the old names to new names
  14 +# -apply-namecache: reuse the given cache file
  15 +
  16 +# Keep options:
  17 +# -keep-property-name: specifies property names that you want to keep
  18 +# -keep-global-name: specifies names that you want to keep in the global scope
  19 +
  20 +-enable-property-obfuscation
  21 +-enable-toplevel-obfuscation
  22 +-enable-filename-obfuscation
  23 +-enable-export-obfuscation
  1 +{
  2 + "name": "entry",
  3 + "version": "1.0.0",
  4 + "description": "Please describe the basic information.",
  5 + "main": "",
  6 + "author": "",
  7 + "license": "",
  8 + "dependencies": {}
  9 +}
  10 +
  1 +import AbilityConstant from '@ohos.app.ability.AbilityConstant';
  2 +import hilog from '@ohos.hilog';
  3 +import UIAbility from '@ohos.app.ability.UIAbility';
  4 +import Want from '@ohos.app.ability.Want';
  5 +import window from '@ohos.window';
  6 +
  7 +export default class EntryAbility extends UIAbility {
  8 + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  9 + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
  10 + }
  11 +
  12 + onDestroy(): void {
  13 + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
  14 + }
  15 +
  16 + onWindowStageCreate(windowStage: window.WindowStage): void {
  17 + // Main window is created, set main page for this ability
  18 + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
  19 +
  20 + windowStage.loadContent('pages/Index', (err) => {
  21 + if (err.code) {
  22 + hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
  23 + return;
  24 + }
  25 + hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
  26 + });
  27 + }
  28 +
  29 + onWindowStageDestroy(): void {
  30 + // Main window is destroyed, release UI related resources
  31 + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
  32 + }
  33 +
  34 + onForeground(): void {
  35 + // Ability has brought to foreground
  36 + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
  37 + }
  38 +
  39 + onBackground(): void {
  40 + // Ability has back to background
  41 + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
  42 + }
  43 +}
  1 +import hilog from '@ohos.hilog';
  2 +import BackupExtensionAbility, { BundleVersion } from '@ohos.application.BackupExtensionAbility';
  3 +
  4 +export default class EntryBackupAbility extends BackupExtensionAbility {
  5 + async onBackup() {
  6 + hilog.info(0x0000, 'testTag', 'onBackup ok');
  7 + }
  8 +
  9 + async onRestore(bundleVersion: BundleVersion) {
  10 + hilog.info(0x0000, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion));
  11 + }
  12 +}
  1 +import { LengthUnit } from '@kit.ArkUI';
  2 +import worker, { MessageEvents } from '@ohos.worker';
  3 +import { BusinessError } from '@kit.BasicServicesKit';
  4 +import { picker } from '@kit.CoreFileKit';
  5 +
  6 +
  7 +@Entry
  8 +@Component
  9 +struct Index {
  10 + @State currentIndex: number = 0;
  11 + @State resultFromFile: string = '';
  12 + @State progressForFile: number = 0;
  13 + @State selectFileBtnEnabled: boolean = false;
  14 + @State message: string = 'To be implemented';
  15 + @State lang: string = 'English';
  16 + private controller: TabsController = new TabsController();
  17 + private workerInstance?: worker.ThreadWorker
  18 + private readonly scriptURL: string = 'entry/ets/workers/NonStreamingAsrWithVadWorker.ets'
  19 +
  20 + aboutToAppear(): void {
  21 + this.workerInstance = new worker.ThreadWorker(this.scriptURL, {
  22 + name: 'NonStreaming ASR worker'
  23 + });
  24 +
  25 + this.workerInstance.onmessage = (e: MessageEvents) => {
  26 + const msgType = e.data['msgType'] as string;
  27 + console.log(`received data ${msgType}`);
  28 +
  29 + if (msgType == 'init-non-streaming-asr-done') {
  30 + this.selectFileBtnEnabled = true;
  31 + }
  32 +
  33 + if (msgType == 'non-streaming-asr-vad-decode-done') {
  34 + this.resultFromFile = e.data['text'] as string + '\n';
  35 + }
  36 +
  37 + if (msgType == 'non-streaming-asr-vad-decode-partial') {
  38 + if (this.resultFromFile == '') {
  39 + this.resultFromFile = e.data['text'] as string;
  40 + } else {
  41 + this.resultFromFile += '\n\n' + e.data['text'] as string;
  42 + }
  43 + }
  44 +
  45 + if (msgType == 'non-streaming-asr-vad-decode-error') {
  46 + this.resultFromFile = e.data['text'] as string;
  47 + }
  48 +
  49 + if (msgType == 'non-streaming-asr-vad-decode-progress') {
  50 + this.progressForFile = e.data['progress'] as number;
  51 +
  52 + this.selectFileBtnEnabled = this.progressForFile >= 100;
  53 + }
  54 + }
  55 +
  56 + const context = getContext();
  57 + this.workerInstance.postMessage({ msgType: 'init-vad', context });
  58 + this.workerInstance.postMessage({ msgType: 'init-non-streaming-asr', context });
  59 + }
  60 +
  61 + @Builder
  62 + TabBuilder(title: string, targetIndex: number, selectedImg: Resource, normalImg: Resource) {
  63 + Column() {
  64 + Image(this.currentIndex == targetIndex ? selectedImg : normalImg)
  65 + .size({ width: 25, height: 25 })
  66 + Text(title)
  67 + .fontColor(this.currentIndex == targetIndex ? '#28bff1' : '#8a8a8a')
  68 + }
  69 + .width('100%')
  70 + .height(50)
  71 + .justifyContent(FlexAlign.Center)
  72 + .onClick(() => {
  73 + this.currentIndex = targetIndex;
  74 + this.controller.changeIndex(this.currentIndex);
  75 + })
  76 + }
  77 +
  78 + build() {
  79 + Column() {
  80 + Tabs({ barPosition: BarPosition.End, controller: this.controller }) {
  81 + TabContent() {
  82 + Column({ space: 10 }) {
  83 + Text('Next-gen Kaldi: VAD + ASR')
  84 + .fontColor('#182431')
  85 + .fontSize(25)
  86 + .lineHeight(41)
  87 + .fontWeight(500)
  88 +
  89 + Button('Select .wav file ')
  90 + .enabled(this.selectFileBtnEnabled)
  91 + .fontSize(13)
  92 + .width(296)
  93 + .height(60)
  94 + .onClick(() => {
  95 + this.resultFromFile = '';
  96 + this.progressForFile = 0;
  97 +
  98 + const documentSelectOptions = new picker.DocumentSelectOptions();
  99 + documentSelectOptions.maxSelectNumber = 1;
  100 + documentSelectOptions.fileSuffixFilters = ['.wav'];
  101 + const documentViewPicker = new picker.DocumentViewPicker();
  102 + documentViewPicker.select(documentSelectOptions).then((result: Array<string>) => {
  103 + console.log(`Result: ${result}`);
  104 +
  105 + if (!result[0]) {
  106 + this.resultFromFile = 'Please select a file to decode';
  107 + this.selectFileBtnEnabled = true;
  108 + return;
  109 + }
  110 +
  111 + if (this.workerInstance) {
  112 + this.workerInstance.postMessage({
  113 + msgType: 'non-streaming-asr-vad-decode',
  114 + filename: result[0],
  115 + });
  116 + } else {
  117 + console.log(`this worker instance is undefined ${this.workerInstance}`);
  118 + }
  119 + }).catch((err: BusinessError) => {
  120 + console.error(`Failed to select file, code is ${err.code}, message is ${err.message}`);
  121 + })
  122 +
  123 + })
  124 +
  125 + Text(`Supported languages: ${this.lang}`)
  126 +
  127 + if (this.progressForFile > 0) {
  128 + Row() {
  129 + Progress({ value: 0, total: 100, type: ProgressType.Capsule })
  130 + .width('80%')
  131 + .height(20)
  132 + .value(this.progressForFile);
  133 +
  134 + Text(`${this.progressForFile.toFixed(2)}%`).width('15%')
  135 + }.width('100%').justifyContent(FlexAlign.Center)
  136 + }
  137 +
  138 + TextArea({ text: this.resultFromFile }).width('100%').lineSpacing({ value: 10, unit: LengthUnit.VP });
  139 +
  140 + }
  141 + .alignItems(HorizontalAlign.Center)
  142 + .justifyContent(FlexAlign.Start)
  143 + }.tabBar(this.TabBuilder('From file', 0, $r('app.media.icon_doc'), $r('app.media.icon_doc_default')))
  144 +
  145 + TabContent() {
  146 + Column() {
  147 + Text(this.message)
  148 + .fontSize(50)
  149 + .fontWeight(FontWeight.Bold);
  150 + }
  151 + }
  152 + .tabBar(this.TabBuilder('From mic', 1, $r('app.media.ic_public_input_voice'),
  153 + $r('app.media.ic_public_input_voice_default')))
  154 +
  155 + TabContent() {
  156 + Column() {
  157 + Text("Everything is open-sourced");
  158 + Divider();
  159 + Text("It runs locally, without accessing the network");
  160 + Divider();
  161 + Text("See also https://github.com/k2-fsa/sherpa-onnx");
  162 + Divider();
  163 + Text("and https://k2-fsa.github.io/sherpa/social-groups.html");
  164 + }.justifyContent(FlexAlign.Start)
  165 + }.tabBar(this.TabBuilder('Help', 2, $r('app.media.info_circle'),
  166 + $r('app.media.info_circle_default')))
  167 +
  168 + }.scrollable(false)
  169 + }
  170 + .width('100%')
  171 + .justifyContent(FlexAlign.Start)
  172 + }
  173 +}
  1 +// Please keep in sync with
  2 +// https://github.com/k2-fsa/sherpa-onnx/blob/master/sherpa-onnx/kotlin-api/OfflineRecognizer.kt#L184
  3 +
  4 +import { OfflineModelConfig } from 'sherpa_onnx';
  5 +
  6 +export function getOfflineModelConfig(type: number): OfflineModelConfig {
  7 + const c = new OfflineModelConfig();
  8 + switch (type) {
  9 + case 0: {
  10 + const modelDir = 'sherpa-onnx-paraformer-zh-2023-09-14'
  11 + c.paraformer.model = `${modelDir}/model.int8.onnx`;
  12 + c.tokens = `${modelDir}/tokens.txt`;
  13 + c.modelType = "paraformer";
  14 +
  15 + break;
  16 + }
  17 +
  18 + case 1: {
  19 + const modelDir = 'icefall-asr-multidataset-pruned_transducer_stateless7-2023-05-04'
  20 + c.transducer.encoder = `$modelDir}/encoder-epoch-30-avg-4.int8.onnx`;
  21 + c.transducer.decoder = `${modelDir}/decoder-epoch-30-avg-4.onnx`;
  22 + c.transducer.encoder = `${modelDir}/joiner-epoch-30-avg-4.onnx`;
  23 + c.tokens = `${modelDir}/tokens.txt`;
  24 + c.modelType = "transducer";
  25 +
  26 + break;
  27 + }
  28 +
  29 + case 2: {
  30 + const modelDir = 'sherpa-onnx-whisper-tiny.en';
  31 + c.whisper.encoder = `${modelDir}/tiny.en-encoder.int8.onnx`;
  32 + c.whisper.decoder = `${modelDir}/tiny.en-decoder.int8.onnx`;
  33 + c.tokens = `${modelDir}/tiny.en-tokens.txt`;
  34 + c.modelType = "whisper";
  35 +
  36 + break;
  37 + }
  38 +
  39 + case 3: {
  40 + const modelDir = 'sherpa-onnx-whisper-base.en';
  41 + c.whisper.encoder = `${modelDir}/base.en-encoder.int8.onnx`;
  42 + c.whisper.decoder = `${modelDir}/base.en-decoder.int8.onnx`;
  43 + c.tokens = `${modelDir}/base.en-tokens.txt`;
  44 + c.modelType = "whisper";
  45 +
  46 + break;
  47 + }
  48 +
  49 + case 4: {
  50 + const modelDir = "icefall-asr-zipformer-wenetspeech-20230615";
  51 + c.transducer.encoder = `${modelDir}/encoder-epoch-12-avg-4.int8.onnx`;
  52 + c.transducer.decoder = `${modelDir}/decoder-epoch-12-avg-4.onnx`;
  53 + c.transducer.joiner = `${modelDir}/joiner-epoch-12-avg-4.int8.onnx`;
  54 + c.tokens = `${modelDir}/tokens.txt`;
  55 + c.modelType = "transducer";
  56 +
  57 + break;
  58 + }
  59 +
  60 + case 5: {
  61 + const modelDir = "sherpa-onnx-zipformer-multi-zh-hans-2023-9-2";
  62 + c.transducer.encoder = `${modelDir}/encoder-epoch-20-avg-1.int8.onnx`;
  63 + c.transducer.decoder = `${modelDir}/decoder-epoch-20-avg-1.onnx`;
  64 + c.transducer.joiner = `${modelDir}/joiner-epoch-20-avg-1.int8.onnx`;
  65 + c.tokens = `${modelDir}/tokens.txt`;
  66 + c.modelType = "transducer";
  67 +
  68 + break;
  69 + }
  70 +
  71 + case 6: {
  72 + const modelDir = "sherpa-onnx-nemo-ctc-en-citrinet-512";
  73 + c.nemoCtc.model = `${modelDir}/model.int8.onnx`;
  74 + c.tokens = `${modelDir}/tokens.txt`;
  75 +
  76 + break;
  77 + }
  78 +
  79 + case 7: {
  80 + const modelDir = "sherpa-onnx-nemo-fast-conformer-ctc-be-de-en-es-fr-hr-it-pl-ru-uk-20k"
  81 + c.nemoCtc.model = `${modelDir}/model.onnx`;
  82 + c.tokens = `${modelDir}/tokens.txt`;
  83 +
  84 + break;
  85 + }
  86 +
  87 + case 8: {
  88 + const modelDir = "sherpa-onnx-nemo-fast-conformer-ctc-en-24500"
  89 + c.nemoCtc.model = `${modelDir}/model.onnx`;
  90 + c.tokens = `${modelDir}/tokens.txt`;
  91 +
  92 + break;
  93 + }
  94 +
  95 + case 9: {
  96 + const modelDir = "sherpa-onnx-nemo-fast-conformer-ctc-en-de-es-fr-14288"
  97 + c.nemoCtc.model = `${modelDir}/model.onnx`;
  98 + c.tokens = `${modelDir}/tokens.txt`;
  99 +
  100 + break;
  101 + }
  102 +
  103 + case 10: {
  104 + const modelDir = "sherpa-onnx-nemo-fast-conformer-ctc-es-1424"
  105 + c.nemoCtc.model = `${modelDir}/model.onnx`;
  106 + c.tokens = `${modelDir}/tokens.txt`;
  107 +
  108 + break;
  109 + }
  110 +
  111 + case 11: {
  112 + const modelDir = "sherpa-onnx-telespeech-ctc-int8-zh-2024-06-04"
  113 + c.telespeechCtc = `${modelDir}/model.int8.onnx`;
  114 + c.tokens = `${modelDir}/tokens.txt`;
  115 + c.modelType = "telespeech_ctc";
  116 +
  117 + break;
  118 + }
  119 +
  120 + case 12: {
  121 + const modelDir = "sherpa-onnx-zipformer-thai-2024-06-20"
  122 + c.transducer.encoder = `${modelDir}/encoder-epoch-12-avg-5.int8.onnx`;
  123 + c.transducer.decoder = `${modelDir}/decoder-epoch-12-avg-5.onnx`;
  124 + c.transducer.joiner = `${modelDir}/joiner-epoch-12-avg-5.int8.onnx`;
  125 + c.tokens = `${modelDir}/tokens.txt`;
  126 + c.modelType = "transducer";
  127 +
  128 + break;
  129 + }
  130 +
  131 + case 13: {
  132 + const modelDir = "sherpa-onnx-zipformer-korean-2024-06-24";
  133 + c.transducer.encoder = `${modelDir}/encoder-epoch-99-avg-1.int8.onnx`;
  134 + c.transducer.decoder = `${modelDir}/decoder-epoch-99-avg-1.onnx`;
  135 + c.transducer.joiner = `${modelDir}/joiner-epoch-99-avg-1.int8.onnx`;
  136 + c.tokens = `${modelDir}/tokens.txt`;
  137 + c.modelType = "transducer";
  138 +
  139 + break;
  140 + }
  141 +
  142 + case 14: {
  143 + const modelDir = "sherpa-onnx-paraformer-zh-small-2024-03-09";
  144 + c.paraformer.model = `${modelDir}/model.int8.onnx`;
  145 + c.tokens = `${modelDir}/tokens.txt`;
  146 + c.modelType = "paraformer";
  147 +
  148 + break;
  149 + }
  150 +
  151 + case 15: {
  152 + const modelDir = "sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17";
  153 + c.senseVoice.model = `${modelDir}/model.int8.onnx`;
  154 + c.tokens = `${modelDir}/tokens.txt`;
  155 +
  156 + break;
  157 + }
  158 +
  159 + case 16: {
  160 + const modelDir = "sherpa-onnx-zipformer-ja-reazonspeech-2024-08-01";
  161 + c.transducer.encoder = `${modelDir}/encoder-epoch-99-avg-1.int8.onnx`;
  162 + c.transducer.decoder = `${modelDir}/decoder-epoch-99-avg-1.onnx`;
  163 + c.transducer.joiner = `${modelDir}/joiner-epoch-99-avg-1.int8.onnx`;
  164 + c.tokens = `${modelDir}/tokens.txt`;
  165 + c.modelType = "transducer";
  166 +
  167 + break;
  168 + }
  169 +
  170 + case 17: {
  171 + const modelDir = "sherpa-onnx-zipformer-ru-2024-09-18";
  172 + c.transducer.encoder = `${modelDir}/encoder.int8.onnx`;
  173 + c.transducer.decoder = `${modelDir}/decoder.onnx`;
  174 + c.transducer.joiner = `${modelDir}/joiner.int8.onnx`;
  175 + c.tokens = `${modelDir}/tokens.txt`;
  176 + c.modelType = "transducer";
  177 +
  178 + break;
  179 + }
  180 +
  181 + case 18: {
  182 + const modelDir = "sherpa-onnx-small-zipformer-ru-2024-09-18";
  183 + c.transducer.encoder = `${modelDir}/encoder.int8.onnx`;
  184 + c.transducer.decoder = `${modelDir}/decoder.onnx`;
  185 + c.transducer.joiner = `${modelDir}/joiner.int8.onnx`;
  186 + c.tokens = `${modelDir}/tokens.txt`;
  187 + c.modelType = "transducer";
  188 +
  189 + break;
  190 + }
  191 +
  192 + case 19: {
  193 + const modelDir = "sherpa-onnx-nemo-ctc-giga-am-russian-2024-10-24";
  194 + c.nemoCtc.model = `${modelDir}/model.int8.onnx`;
  195 + c.tokens = `${modelDir}/tokens.txt`;
  196 +
  197 + break;
  198 + }
  199 +
  200 + case 20: {
  201 + const modelDir = "sherpa-onnx-nemo-transducer-giga-am-russian-2024-10-24";
  202 + c.transducer.encoder = `${modelDir}/encoder.int8.onnx`;
  203 + c.transducer.decoder = `${modelDir}/decoder.onnx`;
  204 + c.transducer.joiner = `${modelDir}/joiner.onnx`;
  205 + c.tokens = `${modelDir}/tokens.txt`;
  206 + c.modelType = "nemo_transducer";
  207 +
  208 + break;
  209 + }
  210 +
  211 + case 21: {
  212 + const modelDir = "sherpa-onnx-moonshine-tiny-en-int8";
  213 + c.moonshine.preprocessor = `${modelDir}/preprocess.onnx`;
  214 + c.moonshine.encoder = `${modelDir}/encode.int8.onnx`;
  215 + c.moonshine.uncachedDecoder = `${modelDir}/uncached_decode.int8.onnx`;
  216 + c.moonshine.cachedDecoder = `${modelDir}/cached_decode.int8.onnx`;
  217 + c.tokens = `${modelDir}/tokens.txt`;
  218 +
  219 + break;
  220 + }
  221 +
  222 + case 22: {
  223 + const modelDir = "sherpa-onnx-moonshine-base-en-int8";
  224 + c.moonshine.preprocessor = `${modelDir}/preprocess.onnx`;
  225 + c.moonshine.encoder = `${modelDir}/encode.int8.onnx`;
  226 + c.moonshine.uncachedDecoder = `${modelDir}/uncached_decode.int8.onnx`;
  227 + c.moonshine.cachedDecoder = `${modelDir}/cached_decode.int8.onnx`;
  228 + c.tokens = `${modelDir}/tokens.txt`;
  229 +
  230 + break;
  231 + }
  232 + }
  233 +
  234 + console.log(`Please specify a supported type. Given type ${type}`);
  235 +
  236 + return c;
  237 +}
  1 +import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';
  2 +import {
  3 + OfflineRecognizer,
  4 + OfflineRecognizerConfig,
  5 + readWaveFromBinary,
  6 + SileroVadConfig,
  7 + Vad,
  8 + VadConfig,
  9 +} from 'sherpa_onnx';
  10 +import { Context } from '@kit.AbilityKit';
  11 +import { fileIo } from '@kit.CoreFileKit';
  12 +import { getOfflineModelConfig } from '../pages/NonStreamingAsrModels';
  13 +
  14 +const workerPort: ThreadWorkerGlobalScope = worker.workerPort;
  15 +
  16 +let recognizer: OfflineRecognizer;
  17 +let vad: Vad; // vad for decoding files
  18 +
  19 +function initVad(context: Context): Vad {
  20 + let mgr = context.resourceManager;
  21 + const config = new VadConfig(
  22 + new SileroVadConfig(
  23 + 'silero_vad.onnx',
  24 + 0.5,
  25 + 0.25,
  26 + 0.5,
  27 + 512,
  28 + ),
  29 + 16000,
  30 + true,
  31 + 1,
  32 + );
  33 +
  34 + const bufferSizeInSeconds = 60;
  35 + return new Vad(config, bufferSizeInSeconds, mgr);
  36 +}
  37 +
  38 +function initNonStreamingAsr(context: Context): OfflineRecognizer {
  39 + let mgr = context.resourceManager;
  40 + const config = new OfflineRecognizerConfig();
  41 +
  42 + // Note that you can switch to a new model by changing type
  43 + //
  44 + // If you use type = 2, which means you will use
  45 + // sherpa-onnx-whisper-tiny.en
  46 + // we assume you have the following folder structure in you resources/rawfile
  47 + /*
  48 + (py38) fangjuns-MacBook-Pro:main fangjun$ pwd
  49 + /Users/fangjun/open-source/sherpa-onnx/harmony-os/SherpaOnnxVadAsr/entry/src/main
  50 + (py38) fangjuns-MacBook-Pro:main fangjun$ tree resources/rawfile/
  51 + resources/rawfile/
  52 + ├── sherpa-onnx-whisper-tiny.en
  53 + │ ├── README.md
  54 + │ ├── tiny.en-decoder.int8.onnx
  55 + │ ├── tiny.en-encoder.int8.onnx
  56 + │ └── tiny.en-tokens.txt
  57 + └── silero_vad.onnx
  58 +
  59 + 1 directory, 5 files
  60 + */
  61 + const type = 2;
  62 + config.modelConfig = getOfflineModelConfig(type);
  63 + config.modelConfig.debug = true;
  64 + return new OfflineRecognizer(config, mgr)
  65 +}
  66 +
  67 +function decode(filename: string): string {
  68 + vad.reset();
  69 +
  70 + const fp = fileIo.openSync(filename);
  71 + const stat = fileIo.statSync(fp.fd);
  72 + const arrayBuffer = new ArrayBuffer(stat.size);
  73 + fileIo.readSync(fp.fd, arrayBuffer);
  74 + const data = new Uint8Array(arrayBuffer);
  75 +
  76 + const wave = readWaveFromBinary(data);
  77 +
  78 + console.log(`sample rate ${wave.sampleRate}`);
  79 + console.log(`samples length ${wave.samples.length}`);
  80 + const resultList: string[] = [];
  81 +
  82 + const windowSize = vad.config.sileroVad.windowSize;
  83 + for (let i = 0; i < wave.samples.length; i += windowSize) {
  84 + const thisWindow = wave.samples.subarray(i, i + windowSize)
  85 + vad.acceptWaveform(thisWindow);
  86 + if (i + windowSize >= wave.samples.length) {
  87 + vad.flush();
  88 + }
  89 + while (!vad.isEmpty()) {
  90 + const segment = vad.front();
  91 + const _startTime = (segment.start / wave.sampleRate);
  92 + const _endTime = _startTime + segment.samples.length / wave.sampleRate;
  93 +
  94 + if (_endTime - _startTime < 0.2) {
  95 + vad.pop();
  96 + continue;
  97 + }
  98 +
  99 + const startTime = _startTime.toFixed(2);
  100 + const endTime = _endTime.toFixed(2);
  101 +
  102 + const progress = (segment.start + segment.samples.length) / wave.samples.length * 100;
  103 +
  104 + workerPort.postMessage({ 'msgType': 'non-streaming-asr-vad-decode-progress', progress });
  105 +
  106 + const stream = recognizer.createStream();
  107 + stream.acceptWaveform({ samples: segment.samples, sampleRate: wave.sampleRate });
  108 + recognizer.decode(stream);
  109 + const result = recognizer.getResult(stream);
  110 +
  111 + const text = `${startTime} -- ${endTime} ${result.text}`
  112 + resultList.push(text);
  113 + console.log(`partial result ${text}`);
  114 +
  115 + workerPort.postMessage({ 'msgType': 'non-streaming-asr-vad-decode-partial', text });
  116 +
  117 + vad.pop();
  118 + }
  119 + }
  120 +
  121 + return resultList.join('\n\n');
  122 +}
  123 +
  124 +/**
  125 + * Defines the event handler to be called when the worker thread receives a message sent by the host thread.
  126 + * The event handler is executed in the worker thread.
  127 + *
  128 + * @param e message data
  129 + */
  130 +workerPort.onmessage = (e: MessageEvents) => {
  131 + const msgType = e.data['msgType'] as string;
  132 + console.log(`msg-type: ${msgType}`)
  133 + if (msgType == 'init-vad' && !vad) {
  134 + const context = e.data['context'] as Context;
  135 + vad = initVad(context);
  136 + console.log('init vad done');
  137 + workerPort.postMessage({ 'msgType': 'init-vad-done' });
  138 + }
  139 +
  140 + if (msgType == 'init-non-streaming-asr' && !recognizer) {
  141 + const context = e.data['context'] as Context;
  142 + recognizer = initNonStreamingAsr(context);
  143 + console.log('init non streaming ASR done');
  144 + workerPort.postMessage({ 'msgType': 'init-non-streaming-asr-done' });
  145 + }
  146 +
  147 + if (msgType == 'non-streaming-asr-vad-decode') {
  148 + const filename = e.data['filename'] as string;
  149 + console.log(`decoding ${filename}`);
  150 + try {
  151 + const text = decode(filename);
  152 + workerPort.postMessage({ msgType: 'non-streaming-asr-vad-decode-done', text });
  153 + } catch (e) {
  154 + workerPort.postMessage({ msgType: 'non-streaming-asr-vad-decode-error', text: `Failed to decode ${filename}` });
  155 + }
  156 +
  157 + workerPort.postMessage({ 'msgType': 'non-streaming-asr-vad-decode-progress', progress: 100 });
  158 + }
  159 +}
  160 +
  161 +/**
  162 + * Defines the event handler to be called when the worker receives a message that cannot be deserialized.
  163 + * The event handler is executed in the worker thread.
  164 + *
  165 + * @param e message data
  166 + */
  167 +workerPort.onmessageerror = (e: MessageEvents) => {
  168 +}
  169 +
  170 +/**
  171 + * Defines the event handler to be called when an exception occurs during worker execution.
  172 + * The event handler is executed in the worker thread.
  173 + *
  174 + * @param e error message
  175 + */
  176 +workerPort.onerror = (e: ErrorEvent) => {
  177 +}
  1 +{
  2 + "module": {
  3 + "name": "entry",
  4 + "type": "entry",
  5 + "description": "$string:module_desc",
  6 + "mainElement": "EntryAbility",
  7 + "deviceTypes": [
  8 + "phone",
  9 + "tablet",
  10 + "2in1"
  11 + ],
  12 + "deliveryWithInstall": true,
  13 + "installationFree": false,
  14 + "pages": "$profile:main_pages",
  15 + "abilities": [
  16 + {
  17 + "name": "EntryAbility",
  18 + "srcEntry": "./ets/entryability/EntryAbility.ets",
  19 + "description": "$string:EntryAbility_desc",
  20 + "icon": "$media:layered_image",
  21 + "label": "$string:EntryAbility_label",
  22 + "startWindowIcon": "$media:startIcon",
  23 + "startWindowBackground": "$color:start_window_background",
  24 + "exported": true,
  25 + "skills": [
  26 + {
  27 + "entities": [
  28 + "entity.system.home"
  29 + ],
  30 + "actions": [
  31 + "action.system.home"
  32 + ]
  33 + }
  34 + ]
  35 + }
  36 + ],
  37 + "extensionAbilities": [
  38 + {
  39 + "name": "EntryBackupAbility",
  40 + "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
  41 + "type": "backup",
  42 + "exported": false,
  43 + "metadata": [
  44 + {
  45 + "name": "ohos.extension.backup",
  46 + "resource": "$profile:backup_config"
  47 + }
  48 + ],
  49 + }
  50 + ]
  51 + }
  52 +}
  1 +{
  2 + "color": [
  3 + {
  4 + "name": "start_window_background",
  5 + "value": "#FFFFFF"
  6 + }
  7 + ]
  8 +}
  1 +{
  2 + "string": [
  3 + {
  4 + "name": "module_desc",
  5 + "value": "VAD+ASR with Next-gen Kaldi"
  6 + },
  7 + {
  8 + "name": "EntryAbility_desc",
  9 + "value": "VAD+ASR"
  10 + },
  11 + {
  12 + "name": "EntryAbility_label",
  13 + "value": "VAD_ASR"
  14 + }
  15 + ]
  16 +}
  1 +{
  2 + "layered-image":
  3 + {
  4 + "background" : "$media:background",
  5 + "foreground" : "$media:foreground"
  6 + }
  7 +}
  1 +{
  2 + "string": [
  3 + {
  4 + "name": "module_desc",
  5 + "value": "module description"
  6 + },
  7 + {
  8 + "name": "EntryAbility_desc",
  9 + "value": "description"
  10 + },
  11 + {
  12 + "name": "EntryAbility_label",
  13 + "value": "label"
  14 + }
  15 + ]
  16 +}
  1 +{
  2 + "string": [
  3 + {
  4 + "name": "module_desc",
  5 + "value": "模块描述"
  6 + },
  7 + {
  8 + "name": "EntryAbility_desc",
  9 + "value": "description"
  10 + },
  11 + {
  12 + "name": "EntryAbility_label",
  13 + "value": "label"
  14 + }
  15 + ]
  16 +}
  1 +import hilog from '@ohos.hilog';
  2 +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
  3 +
  4 +export default function abilityTest() {
  5 + describe('ActsAbilityTest', () => {
  6 + // Defines a test suite. Two parameters are supported: test suite name and test suite function.
  7 + beforeAll(() => {
  8 + // Presets an action, which is performed only once before all test cases of the test suite start.
  9 + // This API supports only one parameter: preset action function.
  10 + })
  11 + beforeEach(() => {
  12 + // Presets an action, which is performed before each unit test case starts.
  13 + // The number of execution times is the same as the number of test cases defined by **it**.
  14 + // This API supports only one parameter: preset action function.
  15 + })
  16 + afterEach(() => {
  17 + // Presets a clear action, which is performed after each unit test case ends.
  18 + // The number of execution times is the same as the number of test cases defined by **it**.
  19 + // This API supports only one parameter: clear action function.
  20 + })
  21 + afterAll(() => {
  22 + // Presets a clear action, which is performed after all test cases of the test suite end.
  23 + // This API supports only one parameter: clear action function.
  24 + })
  25 + it('assertContain', 0, () => {
  26 + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
  27 + hilog.info(0x0000, 'testTag', '%{public}s', 'it begin');
  28 + let a = 'abc';
  29 + let b = 'b';
  30 + // Defines a variety of assertion methods, which are used to declare expected boolean conditions.
  31 + expect(a).assertContain(b);
  32 + expect(a).assertEqual(a);
  33 + })
  34 + })
  35 +}
  1 +import abilityTest from './Ability.test';
  2 +
  3 +export default function testsuite() {
  4 + abilityTest();
  5 +}
  1 +{
  2 + "module": {
  3 + "name": "entry_test",
  4 + "type": "feature",
  5 + "deviceTypes": [
  6 + "phone",
  7 + "tablet",
  8 + "2in1"
  9 + ],
  10 + "deliveryWithInstall": true,
  11 + "installationFree": false
  12 + }
  13 +}
  1 +import localUnitTest from './LocalUnit.test';
  2 +
  3 +export default function testsuite() {
  4 + localUnitTest();
  5 +}
  1 +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
  2 +
  3 +export default function localUnitTest() {
  4 + describe('localUnitTest', () => {
  5 + // Defines a test suite. Two parameters are supported: test suite name and test suite function.
  6 + beforeAll(() => {
  7 + // Presets an action, which is performed only once before all test cases of the test suite start.
  8 + // This API supports only one parameter: preset action function.
  9 + });
  10 + beforeEach(() => {
  11 + // Presets an action, which is performed before each unit test case starts.
  12 + // The number of execution times is the same as the number of test cases defined by **it**.
  13 + // This API supports only one parameter: preset action function.
  14 + });
  15 + afterEach(() => {
  16 + // Presets a clear action, which is performed after each unit test case ends.
  17 + // The number of execution times is the same as the number of test cases defined by **it**.
  18 + // This API supports only one parameter: clear action function.
  19 + });
  20 + afterAll(() => {
  21 + // Presets a clear action, which is performed after all test cases of the test suite end.
  22 + // This API supports only one parameter: clear action function.
  23 + });
  24 + it('assertContain', 0, () => {
  25 + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
  26 + let a = 'abc';
  27 + let b = 'b';
  28 + // Defines a variety of assertion methods, which are used to declare expected boolean conditions.
  29 + expect(a).assertContain(b);
  30 + expect(a).assertEqual(a);
  31 + });
  32 + });
  33 +}
  1 +{
  2 + "modelVersion": "5.0.0",
  3 + "dependencies": {
  4 + },
  5 + "execution": {
  6 + // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | false ]. Default: "normal" */
  7 + // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */
  8 + // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */
  9 + // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */
  10 + // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */
  11 + },
  12 + "logging": {
  13 + // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */
  14 + },
  15 + "debugging": {
  16 + // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */
  17 + },
  18 + "nodeOptions": {
  19 + // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/
  20 + // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/
  21 + }
  22 +}
  1 +import { appTasks } from '@ohos/hvigor-ohos-plugin';
  2 +
  3 +export default {
  4 + system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
  5 + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */
  6 +}
  1 +{
  2 + "meta": {
  3 + "stableOrder": true
  4 + },
  5 + "lockfileVersion": 3,
  6 + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
  7 + "specifiers": {
  8 + "@ohos/hypium@1.0.19": "@ohos/hypium@1.0.19"
  9 + },
  10 + "packages": {
  11 + "@ohos/hypium@1.0.19": {
  12 + "name": "@ohos/hypium",
  13 + "version": "1.0.19",
  14 + "integrity": "sha512-cEjDgLFCm3cWZDeRXk7agBUkPqjWxUo6AQeiu0gEkb3J8ESqlduQLSIXeo3cCsm8U/asL7iKjF85ZyOuufAGSQ==",
  15 + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.19.har",
  16 + "registryType": "ohpm"
  17 + }
  18 + }
  19 +}
  1 +{
  2 + "modelVersion": "5.0.0",
  3 + "description": "Please describe the basic information.",
  4 + "dependencies": {
  5 +
  6 + // You can download sherpa_onnx-v1.10.32.har
  7 + // from
  8 + // https://huggingface.co/csukuangfj/sherpa-onnx-harmony-os/tree/main/har
  9 + "sherpa_onnx": "file:./entry/sherpa_onnx-v1.10.32.har"
  10 + },
  11 + "devDependencies": {
  12 + "@ohos/hypium": "1.0.19"
  13 + }
  14 +}