Fangjun Kuang
Committed by GitHub

Support VAD+ASR for WearOS (#2404)

正在显示 32 个修改的文件 包含 1102 行增加2 行删除
@@ -31,6 +31,7 @@ jobs: @@ -31,6 +31,7 @@ jobs:
31 git config --global user.name "Fangjun Kuang" 31 git config --global user.name "Fangjun Kuang"
32 32
33 - name: FireRed ASR fp16 33 - name: FireRed ASR fp16
  34 + if: false
34 shell: bash 35 shell: bash
35 env: 36 env:
36 HF_TOKEN: ${{ secrets.HF_TOKEN }} 37 HF_TOKEN: ${{ secrets.HF_TOKEN }}
@@ -64,17 +65,37 @@ jobs: @@ -64,17 +65,37 @@ jobs:
64 - name: Zipformer CTC (non-streaming) 65 - name: Zipformer CTC (non-streaming)
65 if: false 66 if: false
66 shell: bash 67 shell: bash
  68 + env:
  69 + MS_TOKEN: ${{ secrets.MODEL_SCOPE_GIT_TOKEN }}
  70 + HF_TOKEN: ${{ secrets.HF_TOKEN }}
67 run: | 71 run: |
68 git lfs install 72 git lfs install
69 names=( 73 names=(
70 sherpa-onnx-zipformer-ctc-zh-int8-2025-07-03 74 sherpa-onnx-zipformer-ctc-zh-int8-2025-07-03
71 sherpa-onnx-zipformer-ctc-zh-2025-07-03 75 sherpa-onnx-zipformer-ctc-zh-2025-07-03
72 sherpa-onnx-zipformer-ctc-zh-fp16-2025-07-03 76 sherpa-onnx-zipformer-ctc-zh-fp16-2025-07-03
  77 + sherpa-onnx-zipformer-ctc-small-zh-int8-2025-07-16
  78 + sherpa-onnx-zipformer-ctc-small-zh-fp16-2025-07-16
  79 + sherpa-onnx-zipformer-ctc-small-zh-2025-07-16
73 ) 80 )
74 for name in ${names[@]}; do 81 for name in ${names[@]}; do
75 - git clone https://huggingface.co/csukuangfj/$name 82 + rm -rf ms
  83 + git clone https://oauth2:${MS_TOKEN}@www.modelscope.cn/csukuangfj/$name.git ms
  84 + git clone https://huggingface.co/csukuangfj/$name
  85 +
  86 + cp -av ms/test_wavs $name
  87 + cp -v ms/*.onnx $name
  88 + cp -v ms/tokens.txt $name
  89 + cp -v ms/bbpe.model $name
  90 +
76 pushd $name 91 pushd $name
77 - git lfs pull 92 + git lfs track "*.wav" "*.onnx" "*.model"
  93 + git add .
  94 + git status
  95 + git commit -m 'add models' || true
  96 + git push https://csukuangfj:$HF_TOKEN@huggingface.co/csukuangfj/$name main || true
  97 +
  98 + # git lfs pull
78 rm -rf .git 99 rm -rf .git
79 rm -rfv .gitattributes 100 rm -rfv .gitattributes
80 ls -lh 101 ls -lh
@@ -19,3 +19,4 @@ for usage. @@ -19,3 +19,4 @@ for usage.
19 |[SherpaOnnxAudioTagging](./SherpaOnnxAudioTagging)|[URL](https://k2-fsa.github.io/sherpa/onnx/audio-tagging/apk.html)| It shows how to use audio tagging.| 19 |[SherpaOnnxAudioTagging](./SherpaOnnxAudioTagging)|[URL](https://k2-fsa.github.io/sherpa/onnx/audio-tagging/apk.html)| It shows how to use audio tagging.|
20 |[SherpaOnnxAudioTaggingWearOS](./SherpaOnnxAudioTagging)|[URL](https://k2-fsa.github.io/sherpa/onnx/audio-tagging/apk-wearos.html)| It shows how to use audio tagging on WearOS.| 20 |[SherpaOnnxAudioTaggingWearOS](./SherpaOnnxAudioTagging)|[URL](https://k2-fsa.github.io/sherpa/onnx/audio-tagging/apk-wearos.html)| It shows how to use audio tagging on WearOS.|
21 |[SherpaOnnxSimulateStreamingAsr](./SherpaOnnxSimulateStreamingAsr)|| It shows how to use a non-streaming ASR model for streaming speech recognition.| 21 |[SherpaOnnxSimulateStreamingAsr](./SherpaOnnxSimulateStreamingAsr)|| It shows how to use a non-streaming ASR model for streaming speech recognition.|
  22 +|[SherpaOnnxSimulateStreamingAsrWearOs](./SherpaOnnxSimulateStreamingAsrWearOs)|| It shows how to use a non-streaming ASR model for streaming speech recognition with WearOS.|
  1 +*.iml
  2 +.gradle
  3 +/local.properties
  4 +/.idea/caches
  5 +/.idea/libraries
  6 +/.idea/modules.xml
  7 +/.idea/workspace.xml
  8 +/.idea/navEditor.xml
  9 +/.idea/assetWizardSettings.xml
  10 +.DS_Store
  11 +/build
  12 +/captures
  13 +.externalNativeBuild
  14 +.cxx
  15 +local.properties
  1 +plugins {
  2 + alias(libs.plugins.android.application)
  3 + alias(libs.plugins.jetbrains.kotlin.android)
  4 +}
  5 +
  6 +android {
  7 + namespace = "com.k2fsa.sherpa.onnx.simulate.streaming.asr.wear.os"
  8 + compileSdk = 34
  9 +
  10 + defaultConfig {
  11 + applicationId = "com.k2fsa.sherpa.onnx.simulate.streaming.asr.wear.os"
  12 + minSdk = 28
  13 + targetSdk = 34
  14 + versionCode = 1
  15 + versionName = "1.0"
  16 + vectorDrawables {
  17 + useSupportLibrary = true
  18 + }
  19 +
  20 + }
  21 +
  22 + buildTypes {
  23 + release {
  24 + isMinifyEnabled = false
  25 + proguardFiles(
  26 + getDefaultProguardFile("proguard-android-optimize.txt"),
  27 + "proguard-rules.pro"
  28 + )
  29 + }
  30 + }
  31 + compileOptions {
  32 + sourceCompatibility = JavaVersion.VERSION_1_8
  33 + targetCompatibility = JavaVersion.VERSION_1_8
  34 + }
  35 + kotlinOptions {
  36 + jvmTarget = "1.8"
  37 + }
  38 + buildFeatures {
  39 + compose = true
  40 + }
  41 + composeOptions {
  42 + kotlinCompilerExtensionVersion = "1.5.1"
  43 + }
  44 + packaging {
  45 + resources {
  46 + excludes += "/META-INF/{AL2.0,LGPL2.1}"
  47 + }
  48 + }
  49 +}
  50 +
  51 +dependencies {
  52 +
  53 + implementation(libs.play.services.wearable)
  54 + implementation(platform(libs.compose.bom))
  55 + implementation(libs.ui)
  56 + implementation(libs.ui.tooling.preview)
  57 + implementation(libs.compose.material)
  58 + implementation(libs.compose.foundation)
  59 + implementation(libs.activity.compose)
  60 + implementation(libs.core.splashscreen)
  61 + implementation("com.github.k2-fsa:sherpa-onnx:v1.12.6")
  62 + androidTestImplementation(platform(libs.compose.bom))
  63 + androidTestImplementation(libs.ui.test.junit4)
  64 + debugImplementation(libs.ui.tooling)
  65 + debugImplementation(libs.ui.test.manifest)
  66 +}
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<lint>
  3 + <!-- Ignore the IconLocation for the Tile preview images -->
  4 + <issue id="IconLocation">
  5 + <ignore path="res/drawable/tile_preview.png" />
  6 + <ignore path="res/drawable-round/tile_preview.png" />
  7 + </issue>
  8 +</lint>
  1 +# Add project specific ProGuard rules here.
  2 +# You can control the set of applied configuration files using the
  3 +# proguardFiles setting in build.gradle.
  4 +#
  5 +# For more details, see
  6 +# http://developer.android.com/guide/developing/tools/proguard.html
  7 +
  8 +# If your project uses WebView with JS, uncomment the following
  9 +# and specify the fully qualified class name to the JavaScript interface
  10 +# class:
  11 +#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
  12 +# public *;
  13 +#}
  14 +
  15 +# Uncomment this to preserve the line number information for
  16 +# debugging stack traces.
  17 +#-keepattributes SourceFile,LineNumberTable
  18 +
  19 +# If you keep the line number information, uncomment this to
  20 +# hide the original source file name.
  21 +#-renamesourcefileattribute SourceFile
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<manifest xmlns:android="http://schemas.android.com/apk/res/android">
  3 +
  4 + <uses-permission android:name="android.permission.WAKE_LOCK" />
  5 +
  6 + <uses-permission android:name="android.permission.RECORD_AUDIO" />
  7 +
  8 + <uses-feature android:name="android.hardware.type.watch" />
  9 +
  10 + <application
  11 + android:allowBackup="true"
  12 + android:icon="@mipmap/ic_launcher"
  13 + android:label="@string/app_name"
  14 + android:supportsRtl="true"
  15 + android:theme="@android:style/Theme.DeviceDefault">
  16 + <uses-library
  17 + android:name="com.google.android.wearable"
  18 + android:required="true" />
  19 +
  20 + <!--
  21 + Set to true if your app is Standalone, that is, it does not require the handheld
  22 + app to run.
  23 + -->
  24 + <meta-data
  25 + android:name="com.google.android.wearable.standalone"
  26 + android:value="true" />
  27 +
  28 + <activity
  29 + android:name=".presentation.MainActivity"
  30 + android:exported="true"
  31 + android:taskAffinity=""
  32 + android:theme="@style/MainActivityTheme.Starting">
  33 + <intent-filter>
  34 + <action android:name="android.intent.action.MAIN" />
  35 +
  36 + <category android:name="android.intent.category.LAUNCHER" />
  37 + </intent-filter>
  38 + </activity>
  39 + </application>
  40 +
  41 +</manifest>
  1 +package com.k2fsa.sherpa.onnx.simulate.streaming.asr.wear.os.presentation
  2 +
  3 +import android.Manifest
  4 +import android.app.Activity
  5 +import android.content.pm.PackageManager
  6 +import android.media.AudioFormat
  7 +import android.media.AudioRecord
  8 +import android.media.MediaRecorder
  9 +import android.util.Log
  10 +import androidx.compose.foundation.background
  11 +import androidx.compose.foundation.layout.Box
  12 +import androidx.compose.foundation.layout.Column
  13 +import androidx.compose.foundation.layout.Spacer
  14 +import androidx.compose.foundation.layout.fillMaxSize
  15 +import androidx.compose.foundation.layout.fillMaxWidth
  16 +import androidx.compose.foundation.layout.height
  17 +import androidx.compose.runtime.Composable
  18 +import androidx.compose.runtime.getValue
  19 +import androidx.compose.runtime.mutableStateOf
  20 +import androidx.compose.runtime.remember
  21 +import androidx.compose.runtime.rememberCoroutineScope
  22 +import androidx.compose.runtime.setValue
  23 +import androidx.compose.ui.Alignment
  24 +import androidx.compose.ui.Modifier
  25 +import androidx.compose.ui.platform.LocalContext
  26 +import androidx.compose.ui.text.style.TextAlign
  27 +import androidx.compose.ui.unit.dp
  28 +import androidx.core.app.ActivityCompat
  29 +import androidx.wear.compose.material.Button
  30 +import androidx.wear.compose.material.MaterialTheme
  31 +import androidx.wear.compose.material.Text
  32 +import com.k2fsa.sherpa.onnx.simulate.streaming.asr.wear.os.presentation.theme.SherpaOnnxSimulateStreamingAsrWearOsTheme
  33 +import kotlinx.coroutines.CoroutineScope
  34 +import kotlinx.coroutines.Dispatchers
  35 +import kotlinx.coroutines.channels.Channel
  36 +import kotlinx.coroutines.launch
  37 +
  38 +
  39 +private var audioRecord: AudioRecord? = null
  40 +
  41 +private const val sampleRateInHz = 16000
  42 +private var samplesChannel = Channel<FloatArray>(capacity = Channel.UNLIMITED)
  43 +
  44 +@Composable
  45 +fun HomeScreen() {
  46 + val activity = LocalContext.current as Activity
  47 +
  48 + var firstTime by remember { mutableStateOf(true) }
  49 + var isStarted by remember { mutableStateOf(false) }
  50 + var result by remember { mutableStateOf("") }
  51 +
  52 + val coroutineScope = rememberCoroutineScope()
  53 +
  54 + val onButtonClick: () -> Unit = {
  55 + firstTime = false
  56 + isStarted = !isStarted
  57 +
  58 +
  59 + if (isStarted) {
  60 + if (ActivityCompat.checkSelfPermission(
  61 + activity, Manifest.permission.RECORD_AUDIO
  62 + ) != PackageManager.PERMISSION_GRANTED
  63 + ) {
  64 + Log.i(TAG, "Recording is not allowed")
  65 + } else {
  66 + // recording is allowed
  67 + val audioSource = MediaRecorder.AudioSource.MIC
  68 + val channelConfig = AudioFormat.CHANNEL_IN_MONO
  69 + val audioFormat = AudioFormat.ENCODING_PCM_16BIT
  70 + val numBytes =
  71 + AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
  72 +
  73 + audioRecord = AudioRecord(
  74 + audioSource,
  75 + sampleRateInHz,
  76 + AudioFormat.CHANNEL_IN_MONO,
  77 + AudioFormat.ENCODING_PCM_16BIT,
  78 + numBytes * 2 // a sample has two bytes as we are using 16-bit PCM
  79 + )
  80 +
  81 + SimulateStreamingAsr.vad.reset()
  82 +
  83 + result = "Started! Please speak"
  84 +
  85 + CoroutineScope(Dispatchers.IO).launch {
  86 + Log.i(TAG, "processing samples")
  87 + val interval = 0.2 // i.e., 200 ms
  88 + val bufferSize = (interval * sampleRateInHz).toInt() // in samples
  89 + val buffer = ShortArray(bufferSize)
  90 +
  91 + audioRecord?.let { it ->
  92 + it.startRecording()
  93 +
  94 + while (isStarted) {
  95 + val ret = audioRecord?.read(buffer, 0, buffer.size)
  96 + ret?.let { n ->
  97 + val samples = FloatArray(n) { buffer[it] / 32768.0f }
  98 + samplesChannel.send(samples)
  99 + }
  100 + }
  101 + val samples = FloatArray(0)
  102 + samplesChannel.send(samples)
  103 + }
  104 + }
  105 +
  106 + CoroutineScope(Dispatchers.Default).launch {
  107 + var buffer = arrayListOf<Float>()
  108 + var offset = 0
  109 + val windowSize = 512 // change it for ten-vad
  110 +
  111 + while (isStarted) {
  112 + for (s in samplesChannel) {
  113 + if (s.isEmpty()) {
  114 + break
  115 + }
  116 +
  117 + buffer.addAll(s.toList())
  118 + while (offset + windowSize < buffer.size) {
  119 + SimulateStreamingAsr.vad.acceptWaveform(
  120 + buffer.subList(
  121 + offset, offset + windowSize
  122 + ).toFloatArray()
  123 + )
  124 +
  125 + offset += windowSize
  126 + }
  127 +
  128 + while (!SimulateStreamingAsr.vad.empty()) {
  129 + val duration = SimulateStreamingAsr.vad.front().samples.count().toFloat() / 16000
  130 +
  131 + val s0 = System.currentTimeMillis()
  132 + val stream = SimulateStreamingAsr.recognizer.createStream()
  133 + stream.acceptWaveform(
  134 + SimulateStreamingAsr.vad.front().samples,
  135 + sampleRateInHz
  136 + )
  137 + SimulateStreamingAsr.recognizer.decode(stream)
  138 +
  139 + val s1 = System.currentTimeMillis()
  140 + val diff = (s1 - s0).toFloat() / 1000
  141 + val rtf = diff / duration
  142 + Log.i(TAG, "rtf: ${rtf}, elapsed: ${diff}, duration: ${duration}")
  143 + val r = SimulateStreamingAsr.recognizer.getResult(stream)
  144 + stream.release()
  145 +
  146 + Log.i(TAG, "result: ${r.text}")
  147 +
  148 + coroutineScope.launch {
  149 + result = r.text
  150 + }
  151 +
  152 + SimulateStreamingAsr.vad.pop()
  153 + buffer = arrayListOf()
  154 + offset = 0
  155 + }
  156 + }
  157 + }
  158 + }
  159 + }
  160 + } else {
  161 + audioRecord?.stop()
  162 + audioRecord?.release()
  163 + audioRecord = null
  164 +
  165 + result = "Click Start and speak"
  166 + }
  167 + }
  168 +
  169 + SherpaOnnxSimulateStreamingAsrWearOsTheme {
  170 + Box(
  171 + modifier = Modifier
  172 + .fillMaxSize()
  173 + .background(MaterialTheme.colors.background),
  174 + contentAlignment = Alignment.Center
  175 + ) {
  176 + Column(
  177 + horizontalAlignment = Alignment.CenterHorizontally
  178 + ) {
  179 + Spacer(modifier = Modifier.height(16.dp))
  180 + if (firstTime) {
  181 + ShowMessage()
  182 + } else {
  183 + ShowResult(result)
  184 + }
  185 +
  186 + Spacer(modifier = Modifier.height(32.dp))
  187 +
  188 + Button(
  189 + onClick = onButtonClick
  190 + ) {
  191 + if (isStarted) {
  192 + Text("Stop")
  193 + } else {
  194 + Text("Start")
  195 + }
  196 + }
  197 + }
  198 + }
  199 + }
  200 +
  201 +}
  202 +
  203 +@Composable
  204 +fun ShowMessage() {
  205 + val msg = "Real-time\nspeech recognition\nwith\nNext-gen Kaldi"
  206 + Text(
  207 + modifier = Modifier.fillMaxWidth(),
  208 + textAlign = TextAlign.Center,
  209 + color = MaterialTheme.colors.primary,
  210 + text = msg,
  211 + )
  212 +}
  213 +
  214 +@Composable
  215 +fun ShowResult(result: String) {
  216 + var msg: String = result
  217 + if (msg.length > 10) {
  218 + val n = 5
  219 + val first = result.take(n)
  220 + val last = result.takeLast(result.length - n)
  221 + msg = "${first}\n${last}"
  222 + }
  223 + Text(
  224 + modifier = Modifier.fillMaxWidth(),
  225 + textAlign = TextAlign.Center,
  226 + color = MaterialTheme.colors.primary,
  227 + text = msg,
  228 + )
  229 +}
  1 +/* While this template provides a good starting point for using Wear Compose, you can always
  2 + * take a look at https://github.com/android/wear-os-samples/tree/main/ComposeStarter and
  3 + * https://github.com/android/wear-os-samples/tree/main/ComposeAdvanced to find the most up to date
  4 + * changes to the libraries and their usages.
  5 + */
  6 +
  7 +package com.k2fsa.sherpa.onnx.simulate.streaming.asr.wear.os.presentation
  8 +
  9 +import android.Manifest
  10 +import android.content.pm.PackageManager
  11 +import android.os.Bundle
  12 +import android.util.Log
  13 +import android.widget.Toast
  14 +import androidx.activity.ComponentActivity
  15 +import androidx.activity.compose.setContent
  16 +import androidx.compose.foundation.background
  17 +import androidx.compose.foundation.layout.Box
  18 +import androidx.compose.foundation.layout.fillMaxSize
  19 +import androidx.compose.foundation.layout.fillMaxWidth
  20 +import androidx.compose.runtime.Composable
  21 +import androidx.compose.ui.Alignment
  22 +import androidx.compose.ui.Modifier
  23 +import androidx.compose.ui.res.stringResource
  24 +import androidx.compose.ui.text.style.TextAlign
  25 +import androidx.compose.ui.tooling.preview.Devices
  26 +import androidx.compose.ui.tooling.preview.Preview
  27 +import androidx.core.app.ActivityCompat
  28 +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
  29 +import androidx.wear.compose.material.MaterialTheme
  30 +import androidx.wear.compose.material.Text
  31 +import androidx.wear.compose.material.TimeText
  32 +import com.k2fsa.sherpa.onnx.simulate.streaming.asr.wear.os.R
  33 +import com.k2fsa.sherpa.onnx.simulate.streaming.asr.wear.os.presentation.theme.SherpaOnnxSimulateStreamingAsrWearOsTheme
  34 +
  35 +const val TAG = "sherpa-onnx"
  36 +private const val REQUEST_RECORD_AUDIO_PERMISSION = 200
  37 +
  38 +class MainActivity : ComponentActivity() {
  39 + private val permissions: Array<String> = arrayOf(Manifest.permission.RECORD_AUDIO)
  40 + override fun onCreate(savedInstanceState: Bundle?) {
  41 + installSplashScreen()
  42 +
  43 + super.onCreate(savedInstanceState)
  44 +
  45 + setTheme(android.R.style.Theme_DeviceDefault)
  46 +
  47 + setContent {
  48 + WearApp("Android")
  49 + }
  50 +
  51 + ActivityCompat.requestPermissions(this, permissions, REQUEST_RECORD_AUDIO_PERMISSION)
  52 + SimulateStreamingAsr.initOfflineRecognizer(this.assets, this.application)
  53 + SimulateStreamingAsr.initVad(this.assets)
  54 + }
  55 +
  56 + override fun onRequestPermissionsResult(
  57 + requestCode: Int,
  58 + permissions: Array<out String>,
  59 + grantResults: IntArray
  60 + ) {
  61 + super.onRequestPermissionsResult(requestCode, permissions, grantResults)
  62 +
  63 + val permissionToRecordAccepted = if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION) {
  64 + grantResults[0] == PackageManager.PERMISSION_GRANTED
  65 + } else {
  66 + false
  67 + }
  68 +
  69 + if (!permissionToRecordAccepted) {
  70 + Log.e(TAG, "Audio record is disallowed")
  71 + Toast.makeText(
  72 + this,
  73 + "This App needs access to the microphone",
  74 + Toast.LENGTH_SHORT
  75 + )
  76 + .show()
  77 + finish()
  78 + }
  79 + Log.i(TAG, "Audio record is permitted")
  80 + }
  81 +}
  82 +
  83 +@Composable
  84 +fun WearApp(greetingName: String) {
  85 + HomeScreen()
  86 +}
  87 +
  88 +@Composable
  89 +fun Greeting(greetingName: String) {
  90 + Text(
  91 + modifier = Modifier.fillMaxWidth(),
  92 + textAlign = TextAlign.Center,
  93 + color = MaterialTheme.colors.primary,
  94 + text = stringResource(R.string.hello_world, greetingName)
  95 + )
  96 +}
  1 +package com.k2fsa.sherpa.onnx.simulate.streaming.asr.wear.os.presentation
  2 +
  3 +import android.app.Application
  4 +import android.content.res.AssetManager
  5 +import android.util.Log
  6 +import com.k2fsa.sherpa.onnx.HomophoneReplacerConfig
  7 +import com.k2fsa.sherpa.onnx.OfflineRecognizer
  8 +import com.k2fsa.sherpa.onnx.OfflineRecognizerConfig
  9 +import com.k2fsa.sherpa.onnx.Vad
  10 +import com.k2fsa.sherpa.onnx.getOfflineModelConfig
  11 +import com.k2fsa.sherpa.onnx.getVadModelConfig
  12 +import java.io.File
  13 +import java.io.FileOutputStream
  14 +import java.io.IOException
  15 +
  16 +
  17 +object SimulateStreamingAsr {
  18 + private var _recognizer: OfflineRecognizer? = null
  19 + val recognizer: OfflineRecognizer
  20 + get() {
  21 + return _recognizer!!
  22 + }
  23 +
  24 + private var _vad: Vad? = null
  25 + val vad: Vad
  26 + get() {
  27 + return _vad!!
  28 + }
  29 +
  30 + fun initOfflineRecognizer(assetManager: AssetManager? = null, application: Application) {
  31 + synchronized(this) {
  32 + if (_recognizer != null) {
  33 + return
  34 + }
  35 + Log.i(TAG, "Initializing sherpa-onnx offline recognizer")
  36 + // Please change getOfflineModelConfig() to add new models
  37 + // See https://k2-fsa.github.io/sherpa/onnx/pretrained_models/index.html
  38 + // for a list of available models
  39 + val asrModelType = 39
  40 + val asrRuleFsts: String?
  41 + asrRuleFsts = null
  42 + Log.i(TAG, "Select model type $asrModelType for ASR")
  43 +
  44 + val useHr = false
  45 + val hr = HomophoneReplacerConfig(
  46 + // Used only when useHr is true
  47 + // Please download the following 3 files from
  48 + // https://github.com/k2-fsa/sherpa-onnx/releases/tag/hr-files
  49 + //
  50 + // dict and lexicon.txt can be shared by different apps
  51 + //
  52 + // replace.fst is specific for an app
  53 + dictDir = "dict",
  54 + lexicon = "lexicon.txt",
  55 + ruleFsts = "replace.fst",
  56 + )
  57 +
  58 + val config = OfflineRecognizerConfig(
  59 + modelConfig = getOfflineModelConfig(type = asrModelType)!!,
  60 + )
  61 +
  62 + if (config.modelConfig.numThreads == 1) {
  63 + config.modelConfig.numThreads = 2
  64 + }
  65 + config.modelConfig.debug = true
  66 +
  67 + if (asrRuleFsts != null) {
  68 + config.ruleFsts = asrRuleFsts
  69 + }
  70 +
  71 + if (useHr) {
  72 + if (hr.dictDir.isNotEmpty() && hr.dictDir.first() != '/') {
  73 + // We need to copy it from the assets directory to some path
  74 + val newDir = copyDataDir(hr.dictDir, application)
  75 + hr.dictDir = "$newDir/${hr.dictDir}"
  76 + }
  77 + config.hr = hr
  78 + }
  79 +
  80 + _recognizer = OfflineRecognizer(
  81 + assetManager = assetManager,
  82 + config = config,
  83 + )
  84 +
  85 + Log.i(TAG, "sherpa-onnx offline recognizer initialized")
  86 + }
  87 + }
  88 +
  89 + fun initVad(assetManager: AssetManager? = null) {
  90 + if (_vad != null) {
  91 + return
  92 + }
  93 + val type = 0
  94 + Log.i(TAG, "Select VAD model type $type")
  95 + val config = getVadModelConfig(type)
  96 +
  97 + _vad = Vad(
  98 + assetManager = assetManager,
  99 + config = config!!,
  100 + )
  101 + Log.i(TAG, "sherpa-onnx vad initialized")
  102 + }
  103 +
  104 + private fun copyDataDir(dataDir: String, application: Application): String {
  105 + Log.i(TAG, "data dir is $dataDir")
  106 + copyAssets(dataDir, application)
  107 +
  108 + val newDataDir = application.getExternalFilesDir(null)!!.absolutePath
  109 + Log.i(TAG, "newDataDir: $newDataDir")
  110 + return newDataDir
  111 + }
  112 +
  113 + private fun copyAssets(path: String, application: Application) {
  114 + val assets: Array<String>?
  115 + try {
  116 + assets = application.assets.list(path)
  117 + if (assets!!.isEmpty()) {
  118 + copyFile(path, application)
  119 + } else {
  120 + val fullPath = "${application.getExternalFilesDir(null)}/$path"
  121 + val dir = File(fullPath)
  122 + dir.mkdirs()
  123 + for (asset in assets.iterator()) {
  124 + val p: String = if (path == "") "" else "$path/"
  125 + copyAssets(p + asset, application)
  126 + }
  127 + }
  128 + } catch (ex: IOException) {
  129 + Log.e(TAG, "Failed to copy $path. $ex")
  130 + }
  131 + }
  132 +
  133 + private fun copyFile(filename: String, application: Application) {
  134 + try {
  135 + val istream = application.assets.open(filename)
  136 + val newFilename = application.getExternalFilesDir(null).toString() + "/" + filename
  137 + val ostream = FileOutputStream(newFilename)
  138 + // Log.i(TAG, "Copying $filename to $newFilename")
  139 + val buffer = ByteArray(1024)
  140 + var read = 0
  141 + while (read != -1) {
  142 + ostream.write(buffer, 0, read)
  143 + read = istream.read(buffer)
  144 + }
  145 + istream.close()
  146 + ostream.flush()
  147 + ostream.close()
  148 + } catch (ex: Exception) {
  149 + Log.e(TAG, "Failed to copy $filename, $ex")
  150 + }
  151 + }
  152 +}
  1 +package com.k2fsa.sherpa.onnx.simulate.streaming.asr.wear.os.presentation.theme
  2 +
  3 +import androidx.compose.runtime.Composable
  4 +import androidx.wear.compose.material.MaterialTheme
  5 +
  6 +@Composable
  7 +fun SherpaOnnxSimulateStreamingAsrWearOsTheme(
  8 + content: @Composable () -> Unit
  9 +) {
  10 + /**
  11 + * Empty theme to customize for your app.
  12 + * See: https://developer.android.com/jetpack/compose/designsystems/custom
  13 + */
  14 + MaterialTheme(
  15 + content = content
  16 + )
  17 +}
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +
  3 +<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
  4 + <item
  5 + android:width="48dp"
  6 + android:height="48dp"
  7 + android:gravity="center">
  8 + <shape android:shape="oval">
  9 + <solid android:color="#FFFFFF" />
  10 + </shape>
  11 + </item>
  12 + <item
  13 + android:width="40dp"
  14 + android:height="40dp"
  15 + android:gravity="center">
  16 + <vector
  17 + android:width="24dp"
  18 + android:height="24dp"
  19 + android:tint="#000000"
  20 + android:viewportWidth="24"
  21 + android:viewportHeight="24">
  22 + <path
  23 + android:fillColor="#FF000000"
  24 + android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z" />
  25 + </vector>
  26 + </item>
  27 +</layer-list>
  1 +<resources>
  2 + <string name="hello_world">From the Round world,\nHello, %1$s!</string>
  3 +</resources>
  1 +<resources>
  2 + <string name="app_name">SherpaOnnxSimulateStreamingAsrWearOs</string>
  3 + <!--
  4 + This string is used for square devices and overridden by hello_world in
  5 + values-round/strings.xml for round devices.
  6 + -->
  7 + <string name="hello_world">From the Square world,\nHello, %1$s!</string>
  8 +</resources>
  1 +<resources>
  2 +
  3 + <style name="MainActivityTheme.Starting" parent="Theme.SplashScreen">
  4 + <item name="windowSplashScreenBackground">@android:color/black</item>
  5 + <item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
  6 + <item name="postSplashScreenTheme">@android:style/Theme.DeviceDefault</item>
  7 + </style>
  8 +</resources>
  1 +// Top-level build file where you can add configuration options common to all sub-projects/modules.
  2 +plugins {
  3 + alias(libs.plugins.android.application) apply false
  4 + alias(libs.plugins.jetbrains.kotlin.android) apply false
  5 +}
  1 +# Project-wide Gradle settings.
  2 +# IDE (e.g. Android Studio) users:
  3 +# Gradle settings configured through the IDE *will override*
  4 +# any settings specified in this file.
  5 +# For more details on how to configure your build environment visit
  6 +# http://www.gradle.org/docs/current/userguide/build_environment.html
  7 +# Specifies the JVM arguments used for the daemon process.
  8 +# The setting is particularly useful for tweaking memory settings.
  9 +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
  10 +# When configured, Gradle will run in incubating parallel mode.
  11 +# This option should only be used with decoupled projects. For more details, visit
  12 +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
  13 +# org.gradle.parallel=true
  14 +# AndroidX package structure to make it clearer which packages are bundled with the
  15 +# Android operating system, and which are packaged with your app's APK
  16 +# https://developer.android.com/topic/libraries/support-library/androidx-rn
  17 +android.useAndroidX=true
  18 +# Kotlin code style for this project: "official" or "obsolete":
  19 +kotlin.code.style=official
  20 +# Enables namespacing of each library's R class so that its R class includes only the
  21 +# resources declared in the library itself and none from the library's dependencies,
  22 +# thereby reducing the size of the R class for that library
  23 +android.nonTransitiveRClass=true
  1 +[versions]
  2 +agp = "8.4.0"
  3 +kotlin = "1.9.0"
  4 +playServicesWearable = "18.0.0"
  5 +composeBom = "2023.08.00"
  6 +composeMaterial = "1.2.1"
  7 +composeFoundation = "1.2.1"
  8 +activityCompose = "1.7.2"
  9 +coreSplashscreen = "1.0.1"
  10 +
  11 +[libraries]
  12 +play-services-wearable = { group = "com.google.android.gms", name = "play-services-wearable", version.ref = "playServicesWearable" }
  13 +compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
  14 +ui = { group = "androidx.compose.ui", name = "ui" }
  15 +ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
  16 +ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
  17 +ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
  18 +ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
  19 +compose-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "composeMaterial" }
  20 +compose-foundation = { group = "androidx.wear.compose", name = "compose-foundation", version.ref = "composeFoundation" }
  21 +activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
  22 +core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" }
  23 +
  24 +[plugins]
  25 +android-application = { id = "com.android.application", version.ref = "agp" }
  26 +jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
  27 +
  1 +#Tue Jul 15 18:18:24 CST 2025
  2 +distributionBase=GRADLE_USER_HOME
  3 +distributionPath=wrapper/dists
  4 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
  5 +zipStoreBase=GRADLE_USER_HOME
  6 +zipStorePath=wrapper/dists
  1 +#!/usr/bin/env sh
  2 +
  3 +#
  4 +# Copyright 2015 the original author or authors.
  5 +#
  6 +# Licensed under the Apache License, Version 2.0 (the "License");
  7 +# you may not use this file except in compliance with the License.
  8 +# You may obtain a copy of the License at
  9 +#
  10 +# https://www.apache.org/licenses/LICENSE-2.0
  11 +#
  12 +# Unless required by applicable law or agreed to in writing, software
  13 +# distributed under the License is distributed on an "AS IS" BASIS,
  14 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15 +# See the License for the specific language governing permissions and
  16 +# limitations under the License.
  17 +#
  18 +
  19 +##############################################################################
  20 +##
  21 +## Gradle start up script for UN*X
  22 +##
  23 +##############################################################################
  24 +
  25 +# Attempt to set APP_HOME
  26 +# Resolve links: $0 may be a link
  27 +PRG="$0"
  28 +# Need this for relative symlinks.
  29 +while [ -h "$PRG" ] ; do
  30 + ls=`ls -ld "$PRG"`
  31 + link=`expr "$ls" : '.*-> \(.*\)$'`
  32 + if expr "$link" : '/.*' > /dev/null; then
  33 + PRG="$link"
  34 + else
  35 + PRG=`dirname "$PRG"`"/$link"
  36 + fi
  37 +done
  38 +SAVED="`pwd`"
  39 +cd "`dirname \"$PRG\"`/" >/dev/null
  40 +APP_HOME="`pwd -P`"
  41 +cd "$SAVED" >/dev/null
  42 +
  43 +APP_NAME="Gradle"
  44 +APP_BASE_NAME=`basename "$0"`
  45 +
  46 +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
  47 +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
  48 +
  49 +# Use the maximum available, or set MAX_FD != -1 to use that value.
  50 +MAX_FD="maximum"
  51 +
  52 +warn () {
  53 + echo "$*"
  54 +}
  55 +
  56 +die () {
  57 + echo
  58 + echo "$*"
  59 + echo
  60 + exit 1
  61 +}
  62 +
  63 +# OS specific support (must be 'true' or 'false').
  64 +cygwin=false
  65 +msys=false
  66 +darwin=false
  67 +nonstop=false
  68 +case "`uname`" in
  69 + CYGWIN* )
  70 + cygwin=true
  71 + ;;
  72 + Darwin* )
  73 + darwin=true
  74 + ;;
  75 + MINGW* )
  76 + msys=true
  77 + ;;
  78 + NONSTOP* )
  79 + nonstop=true
  80 + ;;
  81 +esac
  82 +
  83 +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
  84 +
  85 +
  86 +# Determine the Java command to use to start the JVM.
  87 +if [ -n "$JAVA_HOME" ] ; then
  88 + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
  89 + # IBM's JDK on AIX uses strange locations for the executables
  90 + JAVACMD="$JAVA_HOME/jre/sh/java"
  91 + else
  92 + JAVACMD="$JAVA_HOME/bin/java"
  93 + fi
  94 + if [ ! -x "$JAVACMD" ] ; then
  95 + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
  96 +
  97 +Please set the JAVA_HOME variable in your environment to match the
  98 +location of your Java installation."
  99 + fi
  100 +else
  101 + JAVACMD="java"
  102 + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
  103 +
  104 +Please set the JAVA_HOME variable in your environment to match the
  105 +location of your Java installation."
  106 +fi
  107 +
  108 +# Increase the maximum file descriptors if we can.
  109 +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
  110 + MAX_FD_LIMIT=`ulimit -H -n`
  111 + if [ $? -eq 0 ] ; then
  112 + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
  113 + MAX_FD="$MAX_FD_LIMIT"
  114 + fi
  115 + ulimit -n $MAX_FD
  116 + if [ $? -ne 0 ] ; then
  117 + warn "Could not set maximum file descriptor limit: $MAX_FD"
  118 + fi
  119 + else
  120 + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
  121 + fi
  122 +fi
  123 +
  124 +# For Darwin, add options to specify how the application appears in the dock
  125 +if $darwin; then
  126 + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
  127 +fi
  128 +
  129 +# For Cygwin or MSYS, switch paths to Windows format before running java
  130 +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
  131 + APP_HOME=`cygpath --path --mixed "$APP_HOME"`
  132 + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
  133 +
  134 + JAVACMD=`cygpath --unix "$JAVACMD"`
  135 +
  136 + # We build the pattern for arguments to be converted via cygpath
  137 + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
  138 + SEP=""
  139 + for dir in $ROOTDIRSRAW ; do
  140 + ROOTDIRS="$ROOTDIRS$SEP$dir"
  141 + SEP="|"
  142 + done
  143 + OURCYGPATTERN="(^($ROOTDIRS))"
  144 + # Add a user-defined pattern to the cygpath arguments
  145 + if [ "$GRADLE_CYGPATTERN" != "" ] ; then
  146 + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
  147 + fi
  148 + # Now convert the arguments - kludge to limit ourselves to /bin/sh
  149 + i=0
  150 + for arg in "$@" ; do
  151 + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
  152 + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
  153 +
  154 + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
  155 + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
  156 + else
  157 + eval `echo args$i`="\"$arg\""
  158 + fi
  159 + i=`expr $i + 1`
  160 + done
  161 + case $i in
  162 + 0) set -- ;;
  163 + 1) set -- "$args0" ;;
  164 + 2) set -- "$args0" "$args1" ;;
  165 + 3) set -- "$args0" "$args1" "$args2" ;;
  166 + 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
  167 + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
  168 + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
  169 + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
  170 + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
  171 + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
  172 + esac
  173 +fi
  174 +
  175 +# Escape application args
  176 +save () {
  177 + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
  178 + echo " "
  179 +}
  180 +APP_ARGS=`save "$@"`
  181 +
  182 +# Collect all arguments for the java command, following the shell quoting and substitution rules
  183 +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
  184 +
  185 +exec "$JAVACMD" "$@"
  1 +@rem
  2 +@rem Copyright 2015 the original author or authors.
  3 +@rem
  4 +@rem Licensed under the Apache License, Version 2.0 (the "License");
  5 +@rem you may not use this file except in compliance with the License.
  6 +@rem You may obtain a copy of the License at
  7 +@rem
  8 +@rem https://www.apache.org/licenses/LICENSE-2.0
  9 +@rem
  10 +@rem Unless required by applicable law or agreed to in writing, software
  11 +@rem distributed under the License is distributed on an "AS IS" BASIS,
  12 +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +@rem See the License for the specific language governing permissions and
  14 +@rem limitations under the License.
  15 +@rem
  16 +
  17 +@if "%DEBUG%" == "" @echo off
  18 +@rem ##########################################################################
  19 +@rem
  20 +@rem Gradle startup script for Windows
  21 +@rem
  22 +@rem ##########################################################################
  23 +
  24 +@rem Set local scope for the variables with windows NT shell
  25 +if "%OS%"=="Windows_NT" setlocal
  26 +
  27 +set DIRNAME=%~dp0
  28 +if "%DIRNAME%" == "" set DIRNAME=.
  29 +set APP_BASE_NAME=%~n0
  30 +set APP_HOME=%DIRNAME%
  31 +
  32 +@rem Resolve any "." and ".." in APP_HOME to make it shorter.
  33 +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
  34 +
  35 +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
  36 +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
  37 +
  38 +@rem Find java.exe
  39 +if defined JAVA_HOME goto findJavaFromJavaHome
  40 +
  41 +set JAVA_EXE=java.exe
  42 +%JAVA_EXE% -version >NUL 2>&1
  43 +if "%ERRORLEVEL%" == "0" goto execute
  44 +
  45 +echo.
  46 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
  47 +echo.
  48 +echo Please set the JAVA_HOME variable in your environment to match the
  49 +echo location of your Java installation.
  50 +
  51 +goto fail
  52 +
  53 +:findJavaFromJavaHome
  54 +set JAVA_HOME=%JAVA_HOME:"=%
  55 +set JAVA_EXE=%JAVA_HOME%/bin/java.exe
  56 +
  57 +if exist "%JAVA_EXE%" goto execute
  58 +
  59 +echo.
  60 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
  61 +echo.
  62 +echo Please set the JAVA_HOME variable in your environment to match the
  63 +echo location of your Java installation.
  64 +
  65 +goto fail
  66 +
  67 +:execute
  68 +@rem Setup the command line
  69 +
  70 +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
  71 +
  72 +
  73 +@rem Execute Gradle
  74 +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
  75 +
  76 +:end
  77 +@rem End local scope for the variables with windows NT shell
  78 +if "%ERRORLEVEL%"=="0" goto mainEnd
  79 +
  80 +:fail
  81 +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
  82 +rem the _cmd.exe /c_ return code!
  83 +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
  84 +exit /b 1
  85 +
  86 +:mainEnd
  87 +if "%OS%"=="Windows_NT" endlocal
  88 +
  89 +:omega
  1 +pluginManagement {
  2 + repositories {
  3 + google {
  4 + content {
  5 + includeGroupByRegex("com\\.android.*")
  6 + includeGroupByRegex("com\\.google.*")
  7 + includeGroupByRegex("androidx.*")
  8 + }
  9 + }
  10 + mavenCentral()
  11 + gradlePluginPortal()
  12 + }
  13 +}
  14 +dependencyResolutionManagement {
  15 + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
  16 + repositories {
  17 + google()
  18 + mavenCentral()
  19 + maven { url = uri("https://jitpack.io") }
  20 + }
  21 +}
  22 +
  23 +rootProject.name = "SherpaOnnxSimulateStreamingAsrWearOs"
  24 +include(":app")
@@ -664,6 +664,23 @@ def get_models(): @@ -664,6 +664,23 @@ def get_models():
664 popd 664 popd
665 """, 665 """,
666 ), 666 ),
  667 + Model(
  668 + model_name="sherpa-onnx-zipformer-ctc-small-zh-int8-2025-07-16",
  669 + idx=39,
  670 + lang="zh",
  671 + lang2="Chinese",
  672 + short_name="zipformer_ctc_small_2025_07_16",
  673 + cmd="""
  674 + pushd $model_name
  675 +
  676 + rm -rfv test_wavs
  677 + rm -rfv bbpe.model
  678 +
  679 + ls -lh
  680 +
  681 + popd
  682 + """,
  683 + ),
667 ] 684 ]
668 return models 685 return models
669 686
@@ -667,6 +667,16 @@ fun getOfflineModelConfig(type: Int): OfflineModelConfig? { @@ -667,6 +667,16 @@ fun getOfflineModelConfig(type: Int): OfflineModelConfig? {
667 tokens = "$modelDir/tokens.txt", 667 tokens = "$modelDir/tokens.txt",
668 ) 668 )
669 } 669 }
  670 +
  671 + 39 -> {
  672 + val modelDir = "sherpa-onnx-zipformer-ctc-small-zh-int8-2025-07-16"
  673 + return OfflineModelConfig(
  674 + zipformerCtc = OfflineZipformerCtcModelConfig(
  675 + model = "$modelDir/model.int8.onnx",
  676 + ),
  677 + tokens = "$modelDir/tokens.txt",
  678 + )
  679 + }
670 } 680 }
671 return null 681 return null
672 } 682 }