diff --git a/.github/workflows/apk-qnn-asr.yaml b/.github/workflows/apk-qnn-asr.yaml new file mode 100644 index 0000000000..4025e50b6d --- /dev/null +++ b/.github/workflows/apk-qnn-asr.yaml @@ -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 + 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 diff --git a/android/SherpaOnnx/app/src/main/AndroidManifest.xml b/android/SherpaOnnx/app/src/main/AndroidManifest.xml index c0c79ddd3b..8fe6cb451f 100644 --- a/android/SherpaOnnx/app/src/main/AndroidManifest.xml +++ b/android/SherpaOnnx/app/src/main/AndroidManifest.xml @@ -14,6 +14,17 @@ android:supportsRtl="true" android:theme="@style/Theme.SherpaOnnx" tools:targetApi="31"> + + + + 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 = 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, ) } diff --git a/android/SherpaOnnx/app/src/main/java/com/k2fsa/sherpa/onnx/QnnConfig.kt b/android/SherpaOnnx/app/src/main/java/com/k2fsa/sherpa/onnx/QnnConfig.kt new file mode 120000 index 0000000000..cfd1fc18fa --- /dev/null +++ b/android/SherpaOnnx/app/src/main/java/com/k2fsa/sherpa/onnx/QnnConfig.kt @@ -0,0 +1 @@ +../../../../../../../../../../sherpa-onnx/kotlin-api/QnnConfig.kt \ No newline at end of file diff --git a/scripts/apk/build-apk-qnn-asr.sh.in b/scripts/apk/build-apk-qnn-asr.sh.in new file mode 100644 index 0000000000..9f739865d2 --- /dev/null +++ b/scripts/apk/build-apk-qnn-asr.sh.in @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# +# Auto generated! Please DO NOT EDIT! + +# Please set the environment variable ANDROID_NDK +# before running this script + +# Inside the $ANDROID_NDK directory, you can find a binary ndk-build +# and some other files like the file "build/cmake/android.toolchain.cmake" + +set -ex + +log() { + # This function is from espnet + local fname=${BASH_SOURCE[1]##*/} + echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" +} + +SHERPA_ONNX_VERSION=$(grep "SHERPA_ONNX_VERSION" ./CMakeLists.txt | cut -d " " -f 2 | cut -d '"' -f 2) + +log "Building streaming ASR APK + QNN for sherpa-onnx v${SHERPA_ONNX_VERSION}" + +export SHERPA_ONNX_ENABLE_TTS=OFF +export SHERPA_ONNX_ENABLE_SPEAKER_DIARIZATION=OFF +export SHERPA_ONNX_ENABLE_QNN=ON +export SHERPA_ONNX_ENABLE_BINARY=OFF + +log "Download qnn header files" + +curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models-qnn/qnn-include-2.40.0.251030.tar.bz2 +tar xf qnn-include-2.40.0.251030.tar.bz2 +rm qnn-include-2.40.0.251030.tar.bz2 +ls -lh qnn-include-2.40.0.251030 + +export QNN_SDK_ROOT=$PWD/qnn-include-2.40.0.251030 + +log "====================arm64-v8a=================" +./build-android-arm64-v8a.sh + +cp -v ./build-android-arm64-v8a/install/lib/*.so ./android/SherpaOnnx/app/src/main/jniLibs/arm64-v8a/ + +log "=======Download qnn libs============" +curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models-qnn/qnn-libs-2.40.0.251030.tar.bz2 +tar xvf qnn-libs-2.40.0.251030.tar.bz2 +rm qnn-libs-2.40.0.251030.tar.bz2 +cp -v qnn-libs-2.40.0.251030/*.so ./android/SherpaOnnx/app/src/main/jniLibs/arm64-v8a/ + +rm -rf qnn-libs-2.40.0.251030 + +ls -lh ./android/SherpaOnnx/app/src/main/jniLibs/arm64-v8a/ + +mkdir -p apks + +{% for model in model_list %} +pushd ./android/SherpaOnnx/app/src/main/assets/ +model_name={{ model.model_name }} +type={{ model.idx }} +lang={{ model.lang }} +short_name={{ model.short_name }} + +curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models-qnn/${model_name}.tar.bz2 +tar xvf ${model_name}.tar.bz2 + +{{ model.cmd }} + +rm -rf *.tar.bz2 +ls -lh $model_name + +popd +# Now we are at the project root directory + +git checkout . +pushd android/SherpaOnnx/app/src/main/java/com/k2fsa/sherpa/onnx +sed -i.bak s/"type = 0/type = $type/" ./MainActivity.kt + +git diff +popd + +for arch in arm64-v8a; do + log "------------------------------------------------------------" + log "build ASR apk for $arch" + log "------------------------------------------------------------" + + pushd ./android/SherpaOnnx + sed -i.bak s/2048/9012/g ./gradle.properties + git diff ./gradle.properties + ./gradlew assembleRelease + popd + + mv android/SherpaOnnx/app/build/outputs/apk/release/app-release-unsigned.apk ./apks/sherpa-onnx-${SHERPA_ONNX_VERSION}-qnn-$arch-asr-$lang-$short_name.apk + ls -lh apks +done + +rm -rf ./android/SherpaOnnx/app/src/main/assets/$model_name +{% endfor %} + +rm -v ./android/SherpaOnnx/app/src/main/jniLibs/arm64-v8a/*.so +git checkout . + +ls -lh apks/ diff --git a/scripts/apk/generate-qnn-asr-apk-script.py b/scripts/apk/generate-qnn-asr-apk-script.py new file mode 100644 index 0000000000..937480ac0a --- /dev/null +++ b/scripts/apk/generate-qnn-asr-apk-script.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 + +import argparse +from dataclasses import dataclass +from pathlib import Path + +import jinja2 + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--total", + type=int, + default=1, + help="Number of runners", + ) + parser.add_argument( + "--index", + type=int, + default=0, + help="Index of the current runner", + ) + return parser.parse_args() + + +@dataclass +class Model: + model_name: str + idx: int + lang: str + short_name: str = "" + cmd: str = "" + + +def get_models(): + return [ + Model( + model_name="sherpa-onnx-qnn-streaming-zipformer-transducer-zh-en-2023-03-20-chunk-size-32-android-aarch64", + idx=9025, + lang="zh_en", + short_name="streaming_zipformer_transducer_2023_03_20_chunk_32", + cmd=""" + pushd $model_name + + rm -rfv test_wavs + + ls -lh + + popd + """, + ), + ] + + +def main(): + args = get_args() + index = args.index + total = args.total + assert 0 <= index < total, (index, total) + + all_model_list = get_models() + num_models = len(all_model_list) + + num_per_runner = num_models // total + if num_per_runner <= 0: + raise ValueError(f"num_models: {num_models}, num_runners: {total}") + + start = index * num_per_runner + end = start + num_per_runner + remaining = num_models - args.total * num_per_runner + + print(f"{index}/{total}: {start}-{end}/{num_models}") + + d = {"model_list": all_model_list[start:end]} + if index < remaining: + s = args.total * num_per_runner + index + d["model_list"].append(all_model_list[s]) + print(f"{s}/{num_models}") + + filename_list = [ + "./build-apk-qnn-asr.sh", + ] + for filename in filename_list: + environment = jinja2.Environment() + if not Path(f"{filename}.in").is_file(): + print(f"skip {filename}") + continue + + with open(f"{filename}.in") as f: + s = f.read() + template = environment.from_string(s) + + s = template.render(**d) + with open(filename, "w") as f: + print(s, file=f) + + +if __name__ == "__main__": + main() diff --git a/sherpa-onnx/csrc/qnn/utils.cc b/sherpa-onnx/csrc/qnn/utils.cc index 48a41150db..0158ee8bc6 100644 --- a/sherpa-onnx/csrc/qnn/utils.cc +++ b/sherpa-onnx/csrc/qnn/utils.cc @@ -49,8 +49,9 @@ std::string QuantizationEncodingToString(Qnn_QuantizationEncoding_t q) { SHERPA_ONNX_TO_STRING(QNN_QUANTIZATION_ENCODING_BLOCKWISE_EXPANSION); SHERPA_ONNX_TO_STRING(QNN_QUANTIZATION_ENCODING_VECTOR); SHERPA_ONNX_TO_STRING(QNN_QUANTIZATION_ENCODING_UNDEFINED); + default: + return "Unknown"; } - return "Unknown"; } std::string TensorDataTypeToString(Qnn_DataType_t t) { diff --git a/sherpa-onnx/java-api/src/main/java/com/k2fsa/sherpa/onnx/OnlineTransducerModelConfig.java b/sherpa-onnx/java-api/src/main/java/com/k2fsa/sherpa/onnx/OnlineTransducerModelConfig.java index 29ba0f87e8..6649b2c31e 100644 --- a/sherpa-onnx/java-api/src/main/java/com/k2fsa/sherpa/onnx/OnlineTransducerModelConfig.java +++ b/sherpa-onnx/java-api/src/main/java/com/k2fsa/sherpa/onnx/OnlineTransducerModelConfig.java @@ -7,11 +7,13 @@ public class OnlineTransducerModelConfig { private final String encoder; private final String decoder; private final String joiner; + private final QnnConfig qnnConfig; private OnlineTransducerModelConfig(Builder builder) { this.encoder = builder.encoder; this.decoder = builder.decoder; this.joiner = builder.joiner; + this.qnnConfig = builder.qnnConfig; } public static Builder builder() { @@ -30,10 +32,15 @@ public String getJoiner() { return joiner; } + public QnnConfig getQnnConfig() { + return qnnConfig; + } + public static class Builder { private String encoder = ""; private String decoder = ""; private String joiner = ""; + private QnnConfig qnnConfig = QnnConfig.builder().build(); public OnlineTransducerModelConfig build() { return new OnlineTransducerModelConfig(this); @@ -53,5 +60,10 @@ public Builder setJoiner(String joiner) { this.joiner = joiner; return this; } + + public Builder setQnnConfig(QnnConfig qnnConfig) { + this.qnnConfig = qnnConfig; + return this; + } } } diff --git a/sherpa-onnx/jni/online-recognizer.cc b/sherpa-onnx/jni/online-recognizer.cc index 9e9cddb61a..40f7ac10b1 100644 --- a/sherpa-onnx/jni/online-recognizer.cc +++ b/sherpa-onnx/jni/online-recognizer.cc @@ -31,6 +31,20 @@ OnlineModelConfig GetOnlineModelConfig(JNIEnv *env, jclass model_config_cls, SHERPA_ONNX_JNI_READ_STRING(ans.transducer.joiner, joiner, transducer_config_cls, transducer_config); + fid = env->GetFieldID(transducer_config_cls, "qnnConfig", + "Lcom/k2fsa/sherpa/onnx/QnnConfig;"); + jobject qnn_config = env->GetObjectField(transducer_config, fid); + jclass qnn_config_cls = env->GetObjectClass(qnn_config); + + SHERPA_ONNX_JNI_READ_STRING(ans.transducer.qnn_config.backend_lib, backendLib, + qnn_config_cls, qnn_config); + + SHERPA_ONNX_JNI_READ_STRING(ans.transducer.qnn_config.context_binary, + contextBinary, qnn_config_cls, qnn_config); + + SHERPA_ONNX_JNI_READ_STRING(ans.transducer.qnn_config.system_lib, systemLib, + qnn_config_cls, qnn_config); + fid = env->GetFieldID(model_config_cls, "paraformer", "Lcom/k2fsa/sherpa/onnx/OnlineParaformerModelConfig;"); jobject paraformer_config = env->GetObjectField(model_config, fid); @@ -300,6 +314,16 @@ JNIEXPORT jlong JNICALL Java_com_k2fsa_sherpa_onnx_OnlineRecognizer_newFromFile( return (jlong)recognizer; } +SHERPA_ONNX_EXTERN_C +JNIEXPORT void JNICALL +Java_com_k2fsa_sherpa_onnx_OnlineRecognizer_prependAdspLibraryPath( + JNIEnv *env, jclass /*cls*/, jstring new_path) { + const char *p = env->GetStringUTFChars(new_path, nullptr); + sherpa_onnx::PrependAdspLibraryPath(p); + + env->ReleaseStringUTFChars(new_path, p); +} + SHERPA_ONNX_EXTERN_C JNIEXPORT void JNICALL Java_com_k2fsa_sherpa_onnx_OnlineRecognizer_delete( JNIEnv * /*env*/, jobject /*obj*/, jlong ptr) { diff --git a/sherpa-onnx/jni/sherpa-onnx-symbols.exp b/sherpa-onnx/jni/sherpa-onnx-symbols.exp index 38d6dd5251..bb3ef6c313 100644 --- a/sherpa-onnx/jni/sherpa-onnx-symbols.exp +++ b/sherpa-onnx/jni/sherpa-onnx-symbols.exp @@ -65,6 +65,7 @@ _Java_com_k2fsa_sherpa_onnx_OnlineRecognizer_isEndpoint _Java_com_k2fsa_sherpa_onnx_OnlineRecognizer_isReady _Java_com_k2fsa_sherpa_onnx_OnlineRecognizer_newFromAsset _Java_com_k2fsa_sherpa_onnx_OnlineRecognizer_newFromFile +_Java_com_k2fsa_sherpa_onnx_OnlineRecognizer_prependAdspLibraryPath _Java_com_k2fsa_sherpa_onnx_OnlineRecognizer_reset _Java_com_k2fsa_sherpa_onnx_OnlineSpeechDenoiser_delete _Java_com_k2fsa_sherpa_onnx_OnlineSpeechDenoiser_flush diff --git a/sherpa-onnx/kotlin-api/OnlineRecognizer.kt b/sherpa-onnx/kotlin-api/OnlineRecognizer.kt index c0a1873e08..8bb9ede7da 100644 --- a/sherpa-onnx/kotlin-api/OnlineRecognizer.kt +++ b/sherpa-onnx/kotlin-api/OnlineRecognizer.kt @@ -18,6 +18,7 @@ data class OnlineTransducerModelConfig( var encoder: String = "", var decoder: String = "", var joiner: String = "", + var qnnConfig: QnnConfig = QnnConfig(), ) data class OnlineParaformerModelConfig( @@ -145,6 +146,9 @@ class OnlineRecognizer( init { System.loadLibrary("sherpa-onnx-jni") } + + @JvmStatic + external fun prependAdspLibraryPath(newPath: String) // for qnn } } @@ -187,6 +191,14 @@ by following the code) https://huggingface.co/csukuangfj/sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20 encoder int8, decoder/joiner float32 +9025 - sherpa-onnx-qnn-streaming-zipformer-transducer-zh-en-2023-03-20-chunk-size-32-android-aarch64 + QNN model libs with writable binary context files + libencoder.so, libdecoder.so, libjoiner.so + +9026 - sherpa-onnx-qnn-SM8850-binary-streaming-zipformer-transducer-zh-en-2023-03-20-chunk-size-32 + QNN binary context files only + encoder.bin, decoder.bin, joiner.bin + */ fun getModelConfig(type: Int): OnlineModelConfig? { when (type) { @@ -601,6 +613,46 @@ fun getModelConfig(type: Int): OnlineModelConfig? { ) } + 9025 -> { + val modelDir = + "sherpa-onnx-qnn-streaming-zipformer-transducer-zh-en-2023-03-20-chunk-size-32-android-aarch64" + return OnlineModelConfig( + transducer = OnlineTransducerModelConfig( + encoder = "$modelDir/libencoder.so", + decoder = "$modelDir/libdecoder.so", + joiner = "$modelDir/libjoiner.so", + qnnConfig = QnnConfig( + backendLib = "libQnnHtp.so", + systemLib = "libQnnSystem.so", + // The following three *.bin files are generated during the first run + // and are used to replace the corresponding *.so files in later runs + contextBinary = "$modelDir/encoder.bin,$modelDir/decoder.bin,$modelDir/joiner.bin", + ), + ), + tokens = "$modelDir/tokens.txt", + provider = "qnn", + modelType = "zipformer", + ) + } + + 9026 -> { + // for Xiaomi 17 Pro + val modelDir = + "sherpa-onnx-qnn-SM8850-binary-streaming-zipformer-transducer-zh-en-2023-03-20-chunk-size-32" + return OnlineModelConfig( + transducer = OnlineTransducerModelConfig( + qnnConfig = QnnConfig( + backendLib = "libQnnHtp.so", + systemLib = "libQnnSystem.so", + contextBinary = "$modelDir/encoder.bin,$modelDir/decoder.bin,$modelDir/joiner.bin", + ), + ), + tokens = "$modelDir/tokens.txt", + provider = "qnn", + modelType = "zipformer", + ) + } + 1000 -> { val modelDir = "sherpa-onnx-rk3588-streaming-zipformer-bilingual-zh-en-2023-02-20" return OnlineModelConfig( @@ -666,4 +718,3 @@ fun getEndpointConfig(): EndpointConfig { rule3 = EndpointRule(false, 0.0f, 20.0f) ) } -