-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Add Android demo for streaming zipformer transducer ASR with QNN #3654
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| name: apk-qnn-asr | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - apk | ||
| - android-qnn-asr | ||
|
|
||
| workflow_dispatch: | ||
|
|
||
| concurrency: | ||
| group: apk-qnn-asr-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: | ||
| contents: write | ||
|
|
||
| env: | ||
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | ||
|
|
||
| jobs: | ||
| asr: | ||
| if: github.repository_owner == 'csukuangfj' || github.repository_owner == 'k2-fsa' | ||
| runs-on: ${{ matrix.os }} | ||
| name: ${{ matrix.index }}/${{ matrix.total }} | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| os: [ubuntu-latest] | ||
| total: ["1"] | ||
| index: ["0"] | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -n '^\s*-\s*uses:\s+[^@]+@([^0-9a-f]{40}|[0-9a-f]{1,39})\s*$' .github/workflows/apk-qnn-asr.yamlRepository: k2-fsa/sherpa-onnx Length of output: 44 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE=".github/workflows/apk-qnn-asr.yaml"
echo "== File = $FILE =="
rg -n '^\s*-\s*uses:\s+' "$FILE" || true
echo
echo "== Non-SHA refs (anything not 40-hex) =="
rg -n '^\s*-\s*uses:\s+[^@]+@(?!(?:[0-9a-f]{40})$)[^\s]+' "$FILE" || true
echo
echo "== SHA refs =="
rg -n '^\s*-\s*uses:\s+[^@]+@[0-9a-f]{40}$' "$FILE" || trueRepository: k2-fsa/sherpa-onnx Length of output: 631 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE=".github/workflows/apk-qnn-asr.yaml"
echo "== Total uses occurrences =="
rg -n 'uses:' "$FILE" || true
echo
echo "== Pinned to 40-hex SHAs? =="
rg -n '^\s*-\s*uses:\s+[^@]+@[0-9a-f]{40}\s*$' "$FILE" || true
echo
echo "== Non-SHA action refs (not 40-hex at end) =="
rg -n '^\s*-\s*uses:\s+[^@]+@\S+\s*$' "$FILE" || trueRepository: k2-fsa/sherpa-onnx Length of output: 595 Pin all GitHub Actions This workflow produces signed/uploaded APK artifacts, but it uses mutable action tags (
Pin each 🧰 Tools🪛 zizmor (1.25.2)[warning] 34-36: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false (artipacked) [error] 34-34: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) 🤖 Prompt for AI Agents |
||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Update version | ||
| shell: bash | ||
| run: | | ||
| ./new-release.sh | ||
| git diff . | ||
|
|
||
| - uses: actions/setup-java@v4 | ||
| with: | ||
| distribution: 'temurin' | ||
| java-version: '21' | ||
|
|
||
| - name: ccache | ||
| uses: hendrikmuhs/ccache-action@v1.2 | ||
| with: | ||
| key: ${{ matrix.os }}-android-qnn-asr | ||
|
|
||
| - name: Display NDK HOME | ||
| shell: bash | ||
| run: | | ||
| echo "ANDROID_NDK_LATEST_HOME: ${ANDROID_NDK_LATEST_HOME}" | ||
| ls -lh ${ANDROID_NDK_LATEST_HOME} | ||
|
|
||
| - name: Install Python dependencies | ||
| shell: bash | ||
| run: | | ||
| python3 -m pip install --upgrade pip jinja2 | ||
|
|
||
| - name: Setup build tool version variable | ||
| shell: bash | ||
| run: | | ||
| BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1) | ||
| echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV | ||
| echo "Last build tool version is: $BUILD_TOOL_VERSION" | ||
|
|
||
| - name: Generate build script | ||
| shell: bash | ||
| run: | | ||
| cd scripts/apk | ||
|
|
||
| total=${{ matrix.total }} | ||
| index=${{ matrix.index }} | ||
|
|
||
| python3 ./generate-qnn-asr-apk-script.py --total $total --index $index | ||
|
|
||
| chmod +x build-apk-qnn-asr.sh | ||
| mv -v ./build-apk-qnn-asr.sh ../.. | ||
|
|
||
| - uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: build-script-${{ matrix.total }}-${{ matrix.index }} | ||
| path: ./build-apk-qnn-asr.sh | ||
|
|
||
| - name: build APK | ||
| shell: bash | ||
| run: | | ||
| export CMAKE_CXX_COMPILER_LAUNCHER=ccache | ||
| export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" | ||
| cmake --version | ||
|
|
||
| export ANDROID_NDK=$ANDROID_NDK_LATEST_HOME | ||
| ./build-apk-qnn-asr.sh | ||
|
|
||
| - name: Display APK | ||
| shell: bash | ||
| run: | | ||
| ls -lh ./apks/ | ||
| du -h -d1 . | ||
|
|
||
| - uses: r0adkll/sign-android-release@v1 | ||
| name: Sign app APK | ||
| with: | ||
| releaseDirectory: ./apks | ||
| signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }} | ||
| alias: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }} | ||
| keyStorePassword: ${{ secrets.ANDROID_SIGNING_KEY_STORE_PASSWORD }} | ||
| env: | ||
| BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }} | ||
|
|
||
| - name: Display APK after signing | ||
| shell: bash | ||
| run: | | ||
| ls -lh ./apks/ | ||
| du -h -d1 . | ||
|
|
||
| - name: Rename APK after signing | ||
| shell: bash | ||
| run: | | ||
| cd apks | ||
| rm -fv signingKey.jks | ||
| rm -fv *.apk.idsig | ||
| rm -fv *-aligned.apk | ||
|
|
||
| all_apks=$(ls -1 *-signed.apk) | ||
| for apk in ${all_apks[@]}; do | ||
| n=$(echo $apk | sed -e s/-signed//) | ||
| mv -v $apk $n | ||
| done | ||
|
|
||
| cd .. | ||
|
|
||
| - name: Publish to huggingface | ||
| env: | ||
| HF_TOKEN: ${{ secrets.HF_TOKEN }} | ||
| uses: nick-fields/retry@v3 | ||
| with: | ||
| max_attempts: 20 | ||
| timeout_seconds: 200 | ||
| shell: bash | ||
| command: | | ||
| git config --global user.email "csukuangfj@gmail.com" | ||
| git config --global user.name "Fangjun Kuang" | ||
|
|
||
| rm -rf huggingface | ||
| export GIT_LFS_SKIP_SMUDGE=1 | ||
| export GIT_CLONE_PROTECTION_ACTIVE=false | ||
|
|
||
| SHERPA_ONNX_VERSION=$(grep "SHERPA_ONNX_VERSION" ./CMakeLists.txt | cut -d " " -f 2 | cut -d '"' -f 2) | ||
|
|
||
| git clone https://csukuangfj2:$HF_TOKEN@huggingface.co/csukuangfj2/sherpa-onnx-apk huggingface | ||
| cd huggingface | ||
| git fetch | ||
| git pull | ||
| git merge -m "merge remote" --ff origin main | ||
|
|
||
| d=qnn-asr/$SHERPA_ONNX_VERSION | ||
| mkdir -p $d | ||
| cp -v ../apks/*.apk $d/ | ||
| git lfs track "*.apk" | ||
| git add . | ||
| git commit -m "add more qnn asr apks" | ||
| git push https://csukuangfj2:$HF_TOKEN@huggingface.co/csukuangfj2/sherpa-onnx-apk main | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,8 @@ | ||||||||||||||||||||||||||||||||||||||||
| package com.k2fsa.sherpa.onnx | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import android.Manifest | ||||||||||||||||||||||||||||||||||||||||
| import android.content.Context | ||||||||||||||||||||||||||||||||||||||||
| import android.content.res.AssetManager | ||||||||||||||||||||||||||||||||||||||||
| import android.content.pm.PackageManager | ||||||||||||||||||||||||||||||||||||||||
| import android.media.AudioFormat | ||||||||||||||||||||||||||||||||||||||||
| import android.media.AudioRecord | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -14,7 +16,8 @@ import androidx.appcompat.app.AppCompatActivity | |||||||||||||||||||||||||||||||||||||||
| import androidx.core.app.ActivityCompat | ||||||||||||||||||||||||||||||||||||||||
| import java.io.File | ||||||||||||||||||||||||||||||||||||||||
| import java.io.FileOutputStream | ||||||||||||||||||||||||||||||||||||||||
| import java.io.IOException | ||||||||||||||||||||||||||||||||||||||||
| import java.io.InputStream | ||||||||||||||||||||||||||||||||||||||||
| import java.io.OutputStream | ||||||||||||||||||||||||||||||||||||||||
| import kotlin.concurrent.thread | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private const val TAG = "sherpa-onnx" | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -24,6 +27,54 @@ private const val REQUEST_RECORD_AUDIO_PERMISSION = 200 | |||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||
| // adb emu avd hostmicon | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private fun assetExists(assetManager: AssetManager, path: String): Boolean { | ||||||||||||||||||||||||||||||||||||||||
| val dir = path.substringBeforeLast('/', "") | ||||||||||||||||||||||||||||||||||||||||
| val fileName = path.substringAfterLast('/') | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| val files = assetManager.list(dir) ?: return false | ||||||||||||||||||||||||||||||||||||||||
| return files.contains(fileName) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+30
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using private fun assetExists(assetManager: AssetManager, path: String): Boolean {
if (path.isEmpty()) return false
return try {
assetManager.open(path).use { }
true
} catch (e: Exception) {
false
}
} |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private fun copyAssetToInternalStorage(path: String, context: Context): String { | ||||||||||||||||||||||||||||||||||||||||
| val targetRoot = context.filesDir | ||||||||||||||||||||||||||||||||||||||||
| val outFile = File(targetRoot, path) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (!assetExists(context.assets, path = path)) { | ||||||||||||||||||||||||||||||||||||||||
| outFile.parentFile?.mkdirs() | ||||||||||||||||||||||||||||||||||||||||
| Log.i(TAG, "$path does not exist, return ${outFile.absolutePath}") | ||||||||||||||||||||||||||||||||||||||||
| return outFile.absolutePath | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+38
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If
Suggested change
Comment on lines
+42
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't treat missing required assets as success. Returning 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (outFile.exists()) { | ||||||||||||||||||||||||||||||||||||||||
| val assetSize = context.assets.open(path).use { it.available() } | ||||||||||||||||||||||||||||||||||||||||
| if (outFile.length() == assetSize.toLong()) { | ||||||||||||||||||||||||||||||||||||||||
| Log.i(TAG, "$targetRoot/$path already exists, skip copying, return $targetRoot/$path") | ||||||||||||||||||||||||||||||||||||||||
| return outFile.absolutePath | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| outFile.parentFile?.mkdirs() | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| context.assets.open(path).use { input: InputStream -> | ||||||||||||||||||||||||||||||||||||||||
| FileOutputStream(outFile).use { output: OutputStream -> | ||||||||||||||||||||||||||||||||||||||||
| input.copyTo(output) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| Log.i(TAG, "Copied $path to $targetRoot/$path") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return outFile.absolutePath | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private fun copyAssetListToInternalStorage(paths: String, context: Context): String { | ||||||||||||||||||||||||||||||||||||||||
| if (paths.isBlank()) return paths | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return paths.split(",") | ||||||||||||||||||||||||||||||||||||||||
| .map { it.trim() } | ||||||||||||||||||||||||||||||||||||||||
| .filter { it.isNotEmpty() } | ||||||||||||||||||||||||||||||||||||||||
| .map { copyAssetToInternalStorage(it, context) } | ||||||||||||||||||||||||||||||||||||||||
| .joinToString(",") | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| class MainActivity : AppCompatActivity() { | ||||||||||||||||||||||||||||||||||||||||
| private val permissions: Array<String> = arrayOf(Manifest.permission.RECORD_AUDIO) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
@@ -232,8 +283,54 @@ class MainActivity : AppCompatActivity() { | |||||||||||||||||||||||||||||||||||||||
| config.hr = hr | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| var assetManager: AssetManager? = application.assets | ||||||||||||||||||||||||||||||||||||||||
| if (config.modelConfig.provider == "qnn") { | ||||||||||||||||||||||||||||||||||||||||
| Log.i(TAG, "nativelibdir: ${applicationInfo.nativeLibraryDir}") | ||||||||||||||||||||||||||||||||||||||||
| OnlineRecognizer.prependAdspLibraryPath(applicationInfo.nativeLibraryDir) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| val transducer = config.modelConfig.transducer | ||||||||||||||||||||||||||||||||||||||||
| val qnnConfig = transducer.qnnConfig | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (qnnConfig.backendLib.isEmpty()) { | ||||||||||||||||||||||||||||||||||||||||
| throw IllegalArgumentException("You should provide libQnnHtp.so for qnn") | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| config.modelConfig.tokens = | ||||||||||||||||||||||||||||||||||||||||
| copyAssetToInternalStorage(config.modelConfig.tokens, this) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (transducer.encoder.isNotEmpty()) { | ||||||||||||||||||||||||||||||||||||||||
| transducer.encoder = | ||||||||||||||||||||||||||||||||||||||||
| copyAssetToInternalStorage(transducer.encoder, this) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (transducer.decoder.isNotEmpty()) { | ||||||||||||||||||||||||||||||||||||||||
| transducer.decoder = | ||||||||||||||||||||||||||||||||||||||||
| copyAssetToInternalStorage(transducer.decoder, this) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (transducer.joiner.isNotEmpty()) { | ||||||||||||||||||||||||||||||||||||||||
| transducer.joiner = | ||||||||||||||||||||||||||||||||||||||||
| copyAssetToInternalStorage(transducer.joiner, this) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (qnnConfig.contextBinary.isNotEmpty()) { | ||||||||||||||||||||||||||||||||||||||||
| qnnConfig.contextBinary = | ||||||||||||||||||||||||||||||||||||||||
| copyAssetListToInternalStorage(qnnConfig.contextBinary, this) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (config.hr.lexicon.isNotEmpty()) { | ||||||||||||||||||||||||||||||||||||||||
| config.hr.lexicon = copyAssetToInternalStorage(config.hr.lexicon, this) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (config.hr.ruleFsts.isNotEmpty()) { | ||||||||||||||||||||||||||||||||||||||||
| config.hr.ruleFsts = copyAssetToInternalStorage(config.hr.ruleFsts, this) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| assetManager = null | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| recognizer = OnlineRecognizer( | ||||||||||||||||||||||||||||||||||||||||
| assetManager = application.assets, | ||||||||||||||||||||||||||||||||||||||||
| assetManager = assetManager, | ||||||||||||||||||||||||||||||||||||||||
| config = config, | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ../../../../../../../../../../sherpa-onnx/kotlin-api/QnnConfig.kt |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: k2-fsa/sherpa-onnx
Length of output: 281
🏁 Script executed:
Repository: k2-fsa/sherpa-onnx
Length of output: 5590
🏁 Script executed:
Repository: k2-fsa/sherpa-onnx
Length of output: 4841
Drop GitHub write access and disable persisted checkout credentials.
.github/workflows/apk-qnn-asr.yamlsets workflow-levelpermissions: contents: write, but it only publishes to Hugging Face viaHF_TOKEN(no steps push back to GitHub). Also,actions/checkout@v4doesn’t setpersist-credentials: false, so the write-capable token can remain in the repo for later steps/actions.Minimal tightening
🤖 Prompt for AI Agents