Fangjun Kuang
Committed by GitHub

Add speaker identification demo for HarmonyOS (#1608)

正在显示 47 个修改的文件 包含 1052 行增加9 行删除
@@ -13,5 +13,8 @@ @@ -13,5 +13,8 @@
13 - [./SherpaOnnxStreamingAsr](./SherpaOnnxStreamingAsr) It shows how to use 13 - [./SherpaOnnxStreamingAsr](./SherpaOnnxStreamingAsr) It shows how to use
14 streaming ASR models for real-time on-device speech recognition. 14 streaming ASR models for real-time on-device speech recognition.
15 15
  16 +- [./SherpaOnnxSpeakerIdentification](./SherpaOnnxSpeakerIdentification) It shows how to use
  17 + speaker embedding models for on-device speaker identification.
  18 +
16 - [./SherpaOnnxTts](./SherpaOnnxTts) It shows how to run on-device text-to-speech. 19 - [./SherpaOnnxTts](./SherpaOnnxTts) It shows how to run on-device text-to-speech.
17 Please see the doc at <https://k2-fsa.github.io/sherpa/onnx/harmony-os/tts.html> 20 Please see the doc at <https://k2-fsa.github.io/sherpa/onnx/harmony-os/tts.html>
@@ -764,7 +764,7 @@ static Napi::Array SpeakerEmbeddingManagerGetAllSpeakersWrapper( @@ -764,7 +764,7 @@ static Napi::Array SpeakerEmbeddingManagerGetAllSpeakersWrapper(
764 764
765 int32_t num_speakers = SherpaOnnxSpeakerEmbeddingManagerNumSpeakers(manager); 765 int32_t num_speakers = SherpaOnnxSpeakerEmbeddingManagerNumSpeakers(manager);
766 if (num_speakers == 0) { 766 if (num_speakers == 0) {
767 - return {}; 767 + return Napi::Array::New(env, num_speakers);
768 } 768 }
769 769
770 const char *const *all_speaker_names = 770 const char *const *all_speaker_names =
  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.speaker.identification",
  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": "SherpaOnnxSpeakerIdentification"
  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
  1 +{
  2 + "apiType": "stageMode",
  3 + "buildOption": {
  4 + "sourceOption": {
  5 + "workers": [
  6 + './src/main/ets/workers/SpeakerIdentificationWorker.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 + "sherpa_onnx": "1.10.33",
  10 + }
  11 +}
  12 +
  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 worker, { MessageEvents } from '@ohos.worker';
  2 +import { audio } from '@kit.AudioKit';
  3 +import { allAllowed, requestPermissions } from './Permission';
  4 +import { Permissions } from '@kit.AbilityKit';
  5 +import { picker } from '@kit.CoreFileKit';
  6 +import fs from '@ohos.file.fs';
  7 +
  8 +
  9 +
  10 +function flatten(samples: Float32Array[]): Float32Array {
  11 + let n = 0;
  12 + for (let i = 0; i < samples.length; ++i) {
  13 + n += samples[i].length;
  14 + }
  15 +
  16 + const ans: Float32Array = new Float32Array(n);
  17 + let offset: number = 0;
  18 + for (let i = 0; i < samples.length; ++i) {
  19 + ans.set(samples[i], offset);
  20 + offset += samples[i].length;
  21 + }
  22 +
  23 + return ans;
  24 +}
  25 +
  26 +function savePcmToWav(filename: string, samples: Int16Array, sampleRate: number) {
  27 + const fp = fs.openSync(filename, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  28 +
  29 + const header = new ArrayBuffer(44);
  30 + const view = new DataView(header);
  31 +
  32 + // http://soundfile.sapp.org/doc/WaveFormat/
  33 + // F F I R
  34 + view.setUint32(0, 0x46464952, true); // chunkID
  35 + view.setUint32(4, 36 + samples.length * 2, true); // chunkSize // E V A W
  36 + view.setUint32(8, 0x45564157, true); // format // // t m f
  37 + view.setUint32(12, 0x20746d66, true); // subchunk1ID
  38 + view.setUint32(16, 16, true); // subchunk1Size, 16 for PCM
  39 + view.setUint32(20, 1, true); // audioFormat, 1 for PCM
  40 + view.setUint16(22, 1, true); // numChannels: 1 channel
  41 + view.setUint32(24, sampleRate, true); // sampleRate
  42 + view.setUint32(28, sampleRate * 2, true); // byteRate
  43 + view.setUint16(32, 2, true); // blockAlign
  44 + view.setUint16(34, 16, true); // bitsPerSample
  45 + view.setUint32(36, 0x61746164, true); // Subchunk2ID
  46 + view.setUint32(40, samples.length * 2, true); // subchunk2Size
  47 +
  48 + fs.writeSync(fp.fd, new Uint8Array(header).buffer, { length: header.byteLength });
  49 + fs.writeSync(fp.fd, samples.buffer, { length: samples.buffer.byteLength });
  50 +
  51 + fs.closeSync(fp.fd);
  52 +}
  53 +
  54 +function toInt16Samples(samples: Float32Array): Int16Array {
  55 + const int16Samples = new Int16Array(samples.length);
  56 + for (let i = 0; i < samples.length; ++i) {
  57 + let s = samples[i] * 32767;
  58 + s = s > 32767 ? 32767 : s;
  59 + s = s < -32768 ? -32768 : s;
  60 + int16Samples[i] = s;
  61 + }
  62 +
  63 + return int16Samples;
  64 +}
  65 +
  66 +@Entry
  67 +@Component
  68 +struct Index {
  69 + @State title: string = 'Next-gen Kaldi: Speaker Identification';
  70 + @State titleFontSize: number = 18;
  71 + private controller: TabsController = new TabsController();
  72 +
  73 + @State currentIndex: number = 0;
  74 +
  75 + @State message: string = 'Hello World';
  76 +
  77 + private workerInstance?: worker.ThreadWorker
  78 + private readonly scriptURL: string = 'entry/ets/workers/SpeakerIdentificationWorker.ets'
  79 +
  80 + @State allSpeakerNames: string[] = [];
  81 + private inputSpeakerName: string = '';
  82 +
  83 + @State btnSaveAudioEnabled: boolean = false;
  84 + @State btnAddEnabled: boolean = false;
  85 +
  86 + private sampleRate: number = 16000;
  87 + private sampleList: Float32Array[] = []
  88 + private mic?: audio.AudioCapturer;
  89 +
  90 + @State infoHome: string = '';
  91 + @State infoAdd: string = '';
  92 +
  93 + @State micBtnCaption: string = 'Start recording';
  94 + @State micStarted: boolean = false;
  95 +
  96 + async initMic() {
  97 + const permissions: Permissions[] = ["ohos.permission.MICROPHONE"];
  98 + let allowed: boolean = await allAllowed(permissions);
  99 + if (!allowed) {
  100 + console.log("request to access the microphone");
  101 + const status: boolean = await requestPermissions(permissions);
  102 +
  103 + if (!status) {
  104 + console.error('access to microphone is denied')
  105 + this.infoHome = "Failed to get microphone permission. Please retry";
  106 + this.infoAdd = this.infoHome;
  107 + return;
  108 + }
  109 +
  110 + allowed = await allAllowed(permissions);
  111 + if (!allowed) {
  112 + console.error('failed to get microphone permission');
  113 + this.infoHome = "Failed to get microphone permission. Please retry";
  114 + this.infoAdd = this.infoHome;
  115 + return;
  116 + }
  117 + } else {
  118 + console.log("allowed to access microphone");
  119 + }
  120 +
  121 + const audioStreamInfo: audio.AudioStreamInfo = {
  122 + samplingRate: this.sampleRate,
  123 + channels: audio.AudioChannel.CHANNEL_1,
  124 + sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
  125 + encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW,
  126 + };
  127 +
  128 + const audioCapturerInfo: audio.AudioCapturerInfo = {
  129 + source: audio.SourceType.SOURCE_TYPE_MIC, capturerFlags: 0
  130 + };
  131 +
  132 + const audioCapturerOptions: audio.AudioCapturerOptions = {
  133 + streamInfo: audioStreamInfo, capturerInfo: audioCapturerInfo
  134 +
  135 + };
  136 + audio.createAudioCapturer(audioCapturerOptions, (err, data) => {
  137 + if (err) {
  138 + console.error(`error code is ${err.code}, error message is ${err.message}`);
  139 + this.infoHome = 'Failed to init microphone';
  140 + this.infoAdd = this.infoHome;
  141 + } else {
  142 + console.info(`init mic successfully`);
  143 + this.mic = data;
  144 + this.mic.on('readData', this.micCallback);
  145 + }
  146 + });
  147 + }
  148 +
  149 + async aboutToAppear() {
  150 + this.workerInstance = new worker.ThreadWorker(this.scriptURL, {
  151 + name: 'Speaker identification worker'
  152 + });
  153 +
  154 + this.workerInstance.onmessage = (e: MessageEvents) => {
  155 + const msgType = e.data['msgType'] as string;
  156 + console.log(`received msg from worker: ${msgType}`);
  157 +
  158 + if (msgType == 'manager-all-speaker-names') {
  159 + this.allSpeakerNames = e.data['allSpeakers'] as string[];
  160 + }
  161 + };
  162 +
  163 + this.workerInstance.postMessage({ msgType: 'init-extractor', context: getContext()});
  164 +
  165 + await this.initMic();
  166 + }
  167 +
  168 + @Builder
  169 + TabBuilder(title: string, targetIndex: number, selectedImg: Resource, normalImg: Resource) {
  170 + Column() {
  171 + Image(this.currentIndex == targetIndex ? selectedImg : normalImg).size({ width: 25, height: 25 })
  172 + Text(title).fontColor(this.currentIndex == targetIndex ? '#28bff1' : '#8a8a8a')
  173 + }.width('100%').height(50).justifyContent(FlexAlign.Center).onClick(() => {
  174 + this.currentIndex = targetIndex;
  175 + this.controller.changeIndex(this.currentIndex);
  176 + })
  177 + }
  178 +
  179 + build() {
  180 + Column() {
  181 + Tabs({ barPosition: BarPosition.End, controller: this.controller }) {
  182 + TabContent() {
  183 + Column({ space: 10 }) {
  184 + Button('Home')
  185 + }
  186 + }.tabBar(this.TabBuilder('Home', 0, $r('app.media.icon_home'), $r('app.media.icon_home')))
  187 +
  188 + TabContent() {
  189 + Column({ space: 10 }) {
  190 + Text(this.title).fontSize(this.titleFontSize).fontWeight(FontWeight.Bold);
  191 +
  192 + if (this.allSpeakerNames.length == 0) {
  193 + Text('Please add speakers first')
  194 + } else {
  195 + List({ space: 10, initialIndex: 0 }) {
  196 + ForEach(this.allSpeakerNames, (item: string, index: number) => {
  197 + ListItem() {
  198 + Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) {
  199 + Text(item)
  200 + .width('100%')
  201 + .height(80)
  202 + .fontSize(20)
  203 + .textAlign(TextAlign.Center)
  204 + .borderRadius(10)
  205 + .flexShrink(1)
  206 +
  207 + Button('Delete')
  208 + .width('30%')
  209 + .height(40)
  210 + .onClick(() => {
  211 + if (index != undefined) {
  212 + const name = this.allSpeakerNames[index];
  213 + console.log(`Deleting speaker ${name}`);
  214 + if (this.workerInstance) {
  215 + this.workerInstance.postMessage({
  216 + msgType: 'manager-delete-speaker',
  217 + name: name
  218 + });
  219 + }
  220 + }
  221 + }).stateEffect(true)
  222 +
  223 + Text('')
  224 + .width('15%')
  225 + .height(80)
  226 + }
  227 + }
  228 + }, (item: string) => item)
  229 + }
  230 + }
  231 + }
  232 + }.tabBar(this.TabBuilder('View', 1, $r('app.media.icon_view'), $r('app.media.icon_view')))
  233 +
  234 + TabContent() {
  235 + Column({ space: 10 }) {
  236 + Text(this.title).fontSize(this.titleFontSize).fontWeight(FontWeight.Bold);
  237 +
  238 + Row({space: 10}) {
  239 + Text('Speaker name')
  240 + TextInput({placeholder: 'Input speaker name'})
  241 + .onChange((value: string)=>{
  242 + this.inputSpeakerName = value.trim();
  243 + });
  244 + }.width('100%')
  245 +
  246 + Row({space: 10}) {
  247 + Button(this.micBtnCaption)
  248 + .onClick(()=> {
  249 + if (this.mic) {
  250 + if (this.micStarted) {
  251 + this.micStarted = false;
  252 + this.micBtnCaption = 'Start recording';
  253 + this.mic.stop();
  254 + this.infoAdd = '';
  255 + if (this.sampleList.length > 0) {
  256 + this.btnAddEnabled = true;
  257 + this.btnSaveAudioEnabled = true;
  258 + }
  259 + } else {
  260 + this.micStarted = true;
  261 + this.micBtnCaption = 'Stop recording';
  262 + this.sampleList = [];
  263 + this.mic.start();
  264 + this.infoAdd = '';
  265 +
  266 + this.btnAddEnabled = false;
  267 + this.btnSaveAudioEnabled = false;
  268 + }
  269 + }
  270 +
  271 + })
  272 +
  273 + Button('Add')
  274 + .enabled(this.btnAddEnabled)
  275 + .onClick(()=>{
  276 + if (this.inputSpeakerName.trim() == '') {
  277 + this.infoAdd += 'Please input a speaker name first';
  278 + return;
  279 + }
  280 +
  281 + const samples = flatten(this.sampleList);
  282 + console.log(`number of samples: ${samples.length}, ${samples.length / this.sampleRate}`);
  283 + })
  284 +
  285 + Button('Save audio')
  286 + .enabled(this.btnSaveAudioEnabled)
  287 + .onClick(()=>{
  288 + if (this.sampleList.length == 0) {
  289 + this.btnSaveAudioEnabled = false;
  290 + return;
  291 + }
  292 +
  293 + const samples = flatten(this.sampleList);
  294 +
  295 + if (samples.length == 0) {
  296 + this.btnSaveAudioEnabled = false;
  297 + return;
  298 + }
  299 +
  300 + let uri: string = '';
  301 +
  302 +
  303 + const audioOptions = new picker.AudioSaveOptions(); // audioOptions.newFileNames = ['o.wav'];
  304 +
  305 + const audioViewPicker = new picker.AudioViewPicker();
  306 +
  307 + audioViewPicker.save(audioOptions).then((audioSelectResult: Array<string>) => {
  308 + uri = audioSelectResult[0];
  309 + savePcmToWav(uri, toInt16Samples(samples), this.sampleRate);
  310 + console.log(`Saved to ${uri}`);
  311 + this.infoAdd += `\nSaved to ${uri}`;
  312 + });
  313 + })
  314 + }
  315 + TextArea({text: this.infoAdd})
  316 + .height('100%')
  317 + .width('100%')
  318 + .focusable(false)
  319 + }
  320 + }.tabBar(this.TabBuilder('Add', 2, $r('app.media.icon_add'), $r('app.media.icon_add')))
  321 +
  322 + TabContent() {
  323 + Column({ space: 10 }) {
  324 + Text(this.title).fontSize(this.titleFontSize).fontWeight(FontWeight.Bold);
  325 + TextArea({
  326 + text: `
  327 +Everyting is open-sourced.
  328 +
  329 +It runs locally, without accessing the network
  330 +
  331 +See also https://github.com/k2-fsa/sherpa-onnx
  332 +
  333 +新一代 Kaldi QQ 和微信交流群: 请看
  334 +
  335 +https://k2-fsa.github.io/sherpa/social-groups.html
  336 +
  337 +微信公众号: 新一代 Kaldi
  338 + `
  339 + }).width('100%').height('100%').focusable(false)
  340 + }
  341 + }.tabBar(this.TabBuilder('Help', 3, $r('app.media.icon_info'), $r('app.media.icon_info')))
  342 +
  343 + }.scrollable(false)
  344 + }.width('100%')
  345 + }
  346 +
  347 + private micCallback = (buffer: ArrayBuffer) => {
  348 + const view: Int16Array = new Int16Array(buffer);
  349 +
  350 + const samplesFloat: Float32Array = new Float32Array(view.length);
  351 + for (let i = 0; i < view.length; ++i) {
  352 + samplesFloat[i] = view[i] / 32768.0;
  353 + }
  354 +
  355 + this.sampleList.push(samplesFloat);
  356 + }
  357 +}
  1 +// This file is modified from
  2 +// https://gitee.com/ukSir/hmchat2/blob/master/entry/src/main/ets/utils/permissionMananger.ets
  3 +import { abilityAccessCtrl, bundleManager, common, Permissions } from '@kit.AbilityKit';
  4 +
  5 +export function allAllowed(permissions: Permissions[]): boolean {
  6 + if (permissions.length == 0) {
  7 + return false;
  8 + }
  9 +
  10 + const mgr: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  11 +
  12 + const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
  13 +
  14 + let tokenID: number = bundleInfo.appInfo.accessTokenId;
  15 +
  16 + return permissions.every(permission => abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED ==
  17 + mgr.checkAccessTokenSync(tokenID, permission));
  18 +}
  19 +
  20 +export async function requestPermissions(permissions: Permissions[]): Promise<boolean> {
  21 + const mgr: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  22 + const context: Context = getContext() as common.UIAbilityContext;
  23 +
  24 + const result = await mgr.requestPermissionsFromUser(context, permissions);
  25 + return result.authResults.length > 0 && result.authResults.every(authResults => authResults == 0);
  26 +}
  1 +import worker, { ThreadWorkerGlobalScope, MessageEvents, ErrorEvent } from '@ohos.worker';
  2 +import {
  3 + readWaveFromBinary,
  4 + Samples,
  5 + SpeakerEmbeddingExtractor,
  6 + SpeakerEmbeddingExtractorConfig,
  7 + SpeakerEmbeddingManager
  8 +} from 'sherpa_onnx';
  9 +import { fileIo } from '@kit.CoreFileKit';
  10 +
  11 +const workerPort: ThreadWorkerGlobalScope = worker.workerPort;
  12 +
  13 +let extractor: SpeakerEmbeddingExtractor;
  14 +let manager: SpeakerEmbeddingManager;
  15 +
  16 +function readWaveFromRawfile(filename: string, context: Context): Samples {
  17 + const data: Uint8Array = context.resourceManager.getRawFileContentSync(filename);
  18 + return readWaveFromBinary(data) as Samples;
  19 +}
  20 +
  21 +function initExtractor(context: Context): SpeakerEmbeddingExtractor {
  22 + const config = new SpeakerEmbeddingExtractorConfig();
  23 + config.model = '3dspeaker_speech_eres2net_base_sv_zh-cn_3dspeaker_16k.onnx';
  24 + config.numThreads = 2;
  25 + config.debug = true;
  26 +
  27 + return new SpeakerEmbeddingExtractor(config, context.resourceManager);
  28 +}
  29 +
  30 +function extractEmbedding(samples: Samples): Float32Array {
  31 + const stream = extractor.createStream();
  32 + stream.acceptWaveform(samples);
  33 + return extractor.compute(stream);
  34 +}
  35 +
  36 +/**
  37 + * Defines the event handler to be called when the worker thread receives a message sent by the host thread.
  38 + * The event handler is executed in the worker thread.
  39 + *
  40 + * @param e message data
  41 + */
  42 +workerPort.onmessage = (e: MessageEvents) => {
  43 + const msgType = e.data['msgType'] as string;
  44 +
  45 + console.log(`from the main thread, msg-type: ${msgType}`);
  46 +
  47 + if (msgType == 'init-extractor' && !extractor) {
  48 + const context: Context = e.data['context'] as Context;
  49 + extractor = initExtractor(context);
  50 + manager = new SpeakerEmbeddingManager(extractor.dim);
  51 +
  52 + const filename1 = 'sr-data/enroll/fangjun-sr-1.wav';
  53 + const samples1 = readWaveFromRawfile(filename1, context);
  54 + console.log(`sample rate: ${samples1.sampleRate}`);
  55 + let ok = manager.add({ name: 'fangjun0', v: extractEmbedding(samples1) });
  56 + ok = manager.add({ name: 'fangjun1', v: extractEmbedding(samples1) });
  57 + /*
  58 + ok = manager.add({ name: 'fangjun2', v: extractEmbedding(samples1) });
  59 + ok = manager.add({ name: 'fangjun3', v: extractEmbedding(samples1) });
  60 + ok = manager.add({ name: 'fangjun4', v: extractEmbedding(samples1) });
  61 + ok = manager.add({ name: 'fangjun5', v: extractEmbedding(samples1) });
  62 + ok = manager.add({ name: 'fangjun6', v: extractEmbedding(samples1) });
  63 + ok = manager.add({ name: 'fangjun7', v: extractEmbedding(samples1) });
  64 + ok = manager.add({ name: 'fangjun8', v: extractEmbedding(samples1) });
  65 + ok = manager.add({ name: 'fangjun9', v: extractEmbedding(samples1) });
  66 + ok = manager.add({ name: 'fangjun10', v: extractEmbedding(samples1) });
  67 + */
  68 +
  69 + if (ok) {
  70 + console.log(`Added fangjun`);
  71 + let n = manager.getNumSpeakers();
  72 + console.log(`number of speakers: ${n}`);
  73 + console.log(`speaker names: ${manager.getAllSpeakerNames().join('\n')}`);
  74 + }
  75 +
  76 + workerPort.postMessage({
  77 + msgType: 'manager-all-speaker-names', allSpeakers: manager.getAllSpeakerNames(),
  78 + });
  79 + }
  80 +
  81 + if (msgType == 'manager-delete-speaker') {
  82 + const name = e.data['name'] as string;
  83 + const ok = manager.remove(name);
  84 + if (ok) {
  85 + console.log(`Removed ${name}.`);
  86 +
  87 + console.log(`Number of speakers: ${manager.getNumSpeakers()}`);
  88 + console.log(`Number of speakers2: ${manager.getAllSpeakerNames().length}`);
  89 + console.log(JSON.stringify(manager.getAllSpeakerNames()));
  90 + workerPort.postMessage({
  91 + msgType: 'manager-all-speaker-names', allSpeakers: manager.getAllSpeakerNames(),
  92 + });
  93 + }
  94 + }
  95 +}
  96 +
  97 +/**
  98 + * Defines the event handler to be called when the worker receives a message that cannot be deserialized.
  99 + * The event handler is executed in the worker thread.
  100 + *
  101 + * @param e message data
  102 + */
  103 +workerPort.onmessageerror = (e: MessageEvents) => {
  104 +}
  105 +
  106 +/**
  107 + * Defines the event handler to be called when an exception occurs during worker execution.
  108 + * The event handler is executed in the worker thread.
  109 + *
  110 + * @param e error message
  111 + */
  112 +workerPort.onerror = (e: ErrorEvent) => {
  113 +}
  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 + "requestPermissions": [
  52 + {
  53 + "name": "ohos.permission.MICROPHONE",
  54 + "reason": "$string:mic_reason",
  55 + "usedScene": {
  56 + "abilities": [
  57 + "EntryAbility",
  58 + ],
  59 + "when": "inuse",
  60 + }
  61 + }
  62 + ]
  63 + }
  64 +}
  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": "On-device speaker identification with Next-gen Kaldi"
  6 + },
  7 + {
  8 + "name": "EntryAbility_desc",
  9 + "value": "On-device speaker identification with Next-gen Kaldi"
  10 + },
  11 + {
  12 + "name": "EntryAbility_label",
  13 + "value": "Speaker identification"
  14 + },
  15 + {
  16 + "name": "mic_reason",
  17 + "value": "access the microphone for on-device speaker identification with Next-gen Kaldi"
  18 + }
  19 + ]
  20 +}
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" version="1.1">
  3 + <!-- Generator: Sketch 63.1 (92452) - https://sketch.com -->
  4 + <title>Public/ic_public_list_add_light</title>
  5 + <desc>Created with Sketch.</desc>
  6 + <g id="_Public/ic_public_list_add_light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
  7 + <path d="M12,1 C18.0751322,1 23,5.92486775 23,12 C23,18.0751322 18.0751322,23 12,23 C5.92486775,23 1,18.0751322 1,12 C1,5.92486775 5.92486775,1 12,1 Z" id="_椭圆形" fill="#0A59F7"/>
  8 + <path d="M12.75,17.25 C12.75,17.6642136 12.4142136,18 12,18 C11.5857864,18 11.25,17.6642136 11.25,17.25 L11.25,12.749 L6.75,12.75 C6.33578644,12.75 6,12.4142136 6,12 C6,11.5857864 6.33578644,11.25 6.75,11.25 L11.25,11.249 L11.25,6.75 C11.25,6.33578644 11.5857864,6 12,6 C12.4142136,6 12.75,6.33578644 12.75,6.75 L12.75,17.25 Z M13.75,11.25 L17.25,11.25 C17.6642136,11.25 18,11.5857864 18,12 C18,12.4142136 17.6642136,12.75 17.25,12.75 L13.75,12.75 L13.75,11.25 Z" id="_形状" fill="#FFFFFF"/>
  9 + </g>
  10 +</svg>
  1 +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 -960 960 960"><path d="m480-840 440 330-48 64-72-54v380H160v-380l-72 54-48-64zM294-478q0 53 57 113t129 125q72-65 129-125t57-113q0-44-30-73t-72-29q-26 0-47.5 10.5T480-542q-15-17-37.5-27.5T396-580q-42 0-72 29t-30 73m426 278v-360L480-740 240-560v360zm0 0H240z"/></svg>
  1 +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 -960 960 960"><path d="M440-280h80v-240h-80zm40-320q17 0 28.5-11.5T520-640t-11.5-28.5T480-680t-28.5 11.5T440-640t11.5 28.5T480-600m0 520q-83 0-156-31.5T197-197t-85.5-127T80-480t31.5-156T197-763t127-85.5T480-880t156 31.5T763-763t85.5 127T880-480t-31.5 156T763-197t-127 85.5T480-80m0-80q134 0 227-93t93-227-93-227-227-93-227 93-93 227 93 227 227 93m0-320"/></svg>
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" version="1.1">
  3 + <title>Public/ic_public_view_list_filled</title>
  4 + <defs>
  5 + <path d="M3.75,17.25 C4.44035594,17.25 5,17.8096441 5,18.5 C5,19.1903559 4.44035594,19.75 3.75,19.75 C3.05964406,19.75 2.5,19.1903559 2.5,18.5 C2.5,17.8096441 3.05964406,17.25 3.75,17.25 Z M21,17.5 C21.5522847,17.5 22,17.9477153 22,18.5 C22,19.0522847 21.5522847,19.5 21,19.5 L8,19.5 C7.44771525,19.5 7,19.0522847 7,18.5 C7,17.9477153 7.44771525,17.5 8,17.5 L21,17.5 Z M3.75,10.75 C4.44035594,10.75 5,11.3096441 5,12 C5,12.6903559 4.44035594,13.25 3.75,13.25 C3.05964406,13.25 2.5,12.6903559 2.5,12 C2.5,11.3096441 3.05964406,10.75 3.75,10.75 Z M21,11 C21.5522847,11 22,11.4477153 22,12 C22,12.5522847 21.5522847,13 21,13 L8,13 C7.44771525,13 7,12.5522847 7,12 C7,11.4477153 7.44771525,11 8,11 L21,11 Z M3.75,4.25 C4.44035594,4.25 5,4.80964406 5,5.5 C5,6.19035594 4.44035594,6.75 3.75,6.75 C3.05964406,6.75 2.5,6.19035594 2.5,5.5 C2.5,4.80964406 3.05964406,4.25 3.75,4.25 Z M21,4.5 C21.5522847,4.5 22,4.94771525 22,5.5 C22,6.05228475 21.5522847,6.5 21,6.5 L8,6.5 C7.44771525,6.5 7,6.05228475 7,5.5 C7,4.94771525 7.44771525,4.5 8,4.5 L21,4.5 Z" id="_path-1"/>
  6 + </defs>
  7 + <g id="_Public/ic_public_view_list_filled" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
  8 + <mask id="_mask-2" fill="white">
  9 + <use xlink:href="#_path-1"/>
  10 + </mask>
  11 + <use id="_形状结合" fill="#000000" fill-rule="nonzero" xlink:href="#_path-1"/>
  12 + </g>
  13 +</svg>
  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": "On-device speaker identification with Next-gen Kaldi"
  6 + },
  7 + {
  8 + "name": "EntryAbility_desc",
  9 + "value": "On-device speaker identification with Next-gen Kaldi"
  10 + },
  11 + {
  12 + "name": "EntryAbility_label",
  13 + "value": "Speaker identification"
  14 + },
  15 + {
  16 + "name": "mic_reason",
  17 + "value": "access the microphone for on-device speaker identification with Next-gen Kaldi"
  18 + }
  19 + ]
  20 +}
  1 +{
  2 + "string": [
  3 + {
  4 + "name": "module_desc",
  5 + "value": "新一代Kaldi: 本地说话人识别"
  6 + },
  7 + {
  8 + "name": "EntryAbility_desc",
  9 + "value": "新一代Kaldi: 本地说话人识别"
  10 + },
  11 + {
  12 + "name": "EntryAbility_label",
  13 + "value": "说话人识别"
  14 + },
  15 + {
  16 + "name": "mic_reason",
  17 + "value": "使用新一代Kaldi, 访问麦克风进行本地说话人识别 (不需要联网)"
  18 + }
  19 + ]
  20 +}
  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 + "devDependencies": {
  7 + "@ohos/hypium": "1.0.19"
  8 + }
  9 +}
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 }, 14 },
15 { 15 {
16 "name": "mic_reason", 16 "name": "mic_reason",
17 - "value": "access the microhone for on-device real-time speech recognition with Next-gen Kaldi" 17 + "value": "access the microphone for on-device real-time speech recognition with Next-gen Kaldi"
18 } 18 }
19 ] 19 ]
20 -}  
  20 +}
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 }, 14 },
15 { 15 {
16 "name": "mic_reason", 16 "name": "mic_reason",
17 - "value": "access the microhone for on-device real-time speech recognition with Next-gen Kaldi" 17 + "value": "access the microphone for on-device real-time speech recognition with Next-gen Kaldi"
18 } 18 }
19 ] 19 ]
20 -}  
  20 +}
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 }, 14 },
15 { 15 {
16 "name": "mic_reason", 16 "name": "mic_reason",
17 - "value": "access the microhone for on-device speech recognition with Next-gen Kaldi" 17 + "value": "access the microphone for on-device speech recognition with Next-gen Kaldi"
18 } 18 }
19 ] 19 ]
20 -}  
  20 +}
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 }, 14 },
15 { 15 {
16 "name": "mic_reason", 16 "name": "mic_reason",
17 - "value": "access the microhone for on-device speech recognition with Next-gen Kaldi" 17 + "value": "access the microphone for on-device speech recognition with Next-gen Kaldi"
18 } 18 }
19 ] 19 ]
20 -}  
  20 +}