From: Tobrun Date: Tue, 7 Nov 2023 12:27:33 +0000 (+0100) Subject: android : decouple example into a library and app module (#1445) X-Git-Tag: upstream/1.7.4~1280 X-Git-Url: https://git.djapps.eu/?a=commitdiff_plain;h=973111088b1847687ff48dc5b9e3907d5eb44d4b;p=pkg%2Fggml%2Fsources%2Fwhisper.cpp android : decouple example into a library and app module (#1445) --- diff --git a/examples/whisper.android/app/build.gradle b/examples/whisper.android/app/build.gradle index 7134d9f1..9f407998 100644 --- a/examples/whisper.android/app/build.gradle +++ b/examples/whisper.android/app/build.gradle @@ -18,9 +18,7 @@ android { vectorDrawables { useSupportLibrary true } - ndk { - abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64' - } + } buildTypes { @@ -43,20 +41,10 @@ android { composeOptions { kotlinCompilerExtensionVersion '1.5.0' } - ndkVersion "25.2.9519653" - externalNativeBuild { - cmake { - path = file("src/main/jni/whisper/CMakeLists.txt") - } - } - packagingOptions { - resources { - excludes += '/META-INF/{AL2.0,LGPL2.1}' - } - } } dependencies { + implementation project(':lib') implementation 'androidx.activity:activity-compose:1.7.2' implementation 'androidx.compose.material:material-icons-core:1.5.0' implementation 'androidx.compose.material3:material3:1.1.1' diff --git a/examples/whisper.android/app/src/main/java/com/whispercppdemo/ui/main/MainScreenViewModel.kt b/examples/whisper.android/app/src/main/java/com/whispercppdemo/ui/main/MainScreenViewModel.kt index bd477932..d614ce33 100644 --- a/examples/whisper.android/app/src/main/java/com/whispercppdemo/ui/main/MainScreenViewModel.kt +++ b/examples/whisper.android/app/src/main/java/com/whispercppdemo/ui/main/MainScreenViewModel.kt @@ -15,7 +15,7 @@ import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.whispercppdemo.media.decodeWaveFile import com.whispercppdemo.recorder.Recorder -import com.whispercppdemo.whisper.WhisperContext +import com.whispercpp.whisper.WhisperContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -35,7 +35,7 @@ class MainScreenViewModel(private val application: Application) : ViewModel() { private val modelsPath = File(application.filesDir, "models") private val samplesPath = File(application.filesDir, "samples") private var recorder: Recorder = Recorder() - private var whisperContext: WhisperContext? = null + private var whisperContext: com.whispercpp.whisper.WhisperContext? = null private var mediaPlayer: MediaPlayer? = null private var recordedFile: File? = null @@ -47,7 +47,7 @@ class MainScreenViewModel(private val application: Application) : ViewModel() { } private suspend fun printSystemInfo() { - printMessage(String.format("System Info: %s\n", WhisperContext.getSystemInfo())) + printMessage(String.format("System Info: %s\n", com.whispercpp.whisper.WhisperContext.getSystemInfo())) } private suspend fun loadData() { @@ -78,7 +78,7 @@ class MainScreenViewModel(private val application: Application) : ViewModel() { printMessage("Loading model...\n") val models = application.assets.list("models/") if (models != null) { - whisperContext = WhisperContext.createContextFromAsset(application.assets, "models/" + models[0]) + whisperContext = com.whispercpp.whisper.WhisperContext.createContextFromAsset(application.assets, "models/" + models[0]) printMessage("Loaded model ${models[0]}.\n") } diff --git a/examples/whisper.android/app/src/main/java/com/whispercppdemo/whisper/LibWhisper.kt b/examples/whisper.android/app/src/main/java/com/whispercppdemo/whisper/LibWhisper.kt deleted file mode 100644 index b0d67037..00000000 --- a/examples/whisper.android/app/src/main/java/com/whispercppdemo/whisper/LibWhisper.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.whispercppdemo.whisper - -import android.content.res.AssetManager -import android.os.Build -import android.util.Log -import kotlinx.coroutines.* -import java.io.File -import java.io.InputStream -import java.util.concurrent.Executors - -private const val LOG_TAG = "LibWhisper" - -class WhisperContext private constructor(private var ptr: Long) { - // Meet Whisper C++ constraint: Don't access from more than one thread at a time. - private val scope: CoroutineScope = CoroutineScope( - Executors.newSingleThreadExecutor().asCoroutineDispatcher() - ) - - suspend fun transcribeData(data: FloatArray): String = withContext(scope.coroutineContext) { - require(ptr != 0L) - val numThreads = WhisperCpuConfig.preferredThreadCount - Log.d(LOG_TAG, "Selecting $numThreads threads") - WhisperLib.fullTranscribe(ptr, numThreads, data) - val textCount = WhisperLib.getTextSegmentCount(ptr) - return@withContext buildString { - for (i in 0 until textCount) { - append(WhisperLib.getTextSegment(ptr, i)) - } - } - } - - suspend fun benchMemory(nthreads: Int): String = withContext(scope.coroutineContext) { - return@withContext WhisperLib.benchMemcpy(nthreads) - } - - suspend fun benchGgmlMulMat(nthreads: Int): String = withContext(scope.coroutineContext) { - return@withContext WhisperLib.benchGgmlMulMat(nthreads) - } - - suspend fun release() = withContext(scope.coroutineContext) { - if (ptr != 0L) { - WhisperLib.freeContext(ptr) - ptr = 0 - } - } - - protected fun finalize() { - runBlocking { - release() - } - } - - companion object { - fun createContextFromFile(filePath: String): WhisperContext { - val ptr = WhisperLib.initContext(filePath) - if (ptr == 0L) { - throw java.lang.RuntimeException("Couldn't create context with path $filePath") - } - return WhisperContext(ptr) - } - - fun createContextFromInputStream(stream: InputStream): WhisperContext { - val ptr = WhisperLib.initContextFromInputStream(stream) - - if (ptr == 0L) { - throw java.lang.RuntimeException("Couldn't create context from input stream") - } - return WhisperContext(ptr) - } - - fun createContextFromAsset(assetManager: AssetManager, assetPath: String): WhisperContext { - val ptr = WhisperLib.initContextFromAsset(assetManager, assetPath) - - if (ptr == 0L) { - throw java.lang.RuntimeException("Couldn't create context from asset $assetPath") - } - return WhisperContext(ptr) - } - - fun getSystemInfo(): String { - return WhisperLib.getSystemInfo() - } - } -} - -private class WhisperLib { - companion object { - init { - Log.d(LOG_TAG, "Primary ABI: ${Build.SUPPORTED_ABIS[0]}") - var loadVfpv4 = false - var loadV8fp16 = false - if (isArmEabiV7a()) { - // armeabi-v7a needs runtime detection support - val cpuInfo = cpuInfo() - cpuInfo?.let { - Log.d(LOG_TAG, "CPU info: $cpuInfo") - if (cpuInfo.contains("vfpv4")) { - Log.d(LOG_TAG, "CPU supports vfpv4") - loadVfpv4 = true - } - } - } else if (isArmEabiV8a()) { - // ARMv8.2a needs runtime detection support - val cpuInfo = cpuInfo() - cpuInfo?.let { - Log.d(LOG_TAG, "CPU info: $cpuInfo") - if (cpuInfo.contains("fphp")) { - Log.d(LOG_TAG, "CPU supports fp16 arithmetic") - loadV8fp16 = true - } - } - } - - if (loadVfpv4) { - Log.d(LOG_TAG, "Loading libwhisper_vfpv4.so") - System.loadLibrary("whisper_vfpv4") - } else if (loadV8fp16) { - Log.d(LOG_TAG, "Loading libwhisper_v8fp16_va.so") - System.loadLibrary("whisper_v8fp16_va") - } else { - Log.d(LOG_TAG, "Loading libwhisper.so") - System.loadLibrary("whisper") - } - } - - // JNI methods - external fun initContextFromInputStream(inputStream: InputStream): Long - external fun initContextFromAsset(assetManager: AssetManager, assetPath: String): Long - external fun initContext(modelPath: String): Long - external fun freeContext(contextPtr: Long) - external fun fullTranscribe(contextPtr: Long, numThreads: Int, audioData: FloatArray) - external fun getTextSegmentCount(contextPtr: Long): Int - external fun getTextSegment(contextPtr: Long, index: Int): String - external fun getSystemInfo(): String - external fun benchMemcpy(nthread: Int): String - external fun benchGgmlMulMat(nthread: Int): String - } -} - -private fun isArmEabiV7a(): Boolean { - return Build.SUPPORTED_ABIS[0].equals("armeabi-v7a") -} - -private fun isArmEabiV8a(): Boolean { - return Build.SUPPORTED_ABIS[0].equals("arm64-v8a") -} - -private fun cpuInfo(): String? { - return try { - File("/proc/cpuinfo").inputStream().bufferedReader().use { - it.readText() - } - } catch (e: Exception) { - Log.w(LOG_TAG, "Couldn't read /proc/cpuinfo", e) - null - } -} \ No newline at end of file diff --git a/examples/whisper.android/app/src/main/java/com/whispercppdemo/whisper/WhisperCpuConfig.kt b/examples/whisper.android/app/src/main/java/com/whispercppdemo/whisper/WhisperCpuConfig.kt deleted file mode 100644 index 5fa9a4e4..00000000 --- a/examples/whisper.android/app/src/main/java/com/whispercppdemo/whisper/WhisperCpuConfig.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.whispercppdemo.whisper - -import android.util.Log -import java.io.BufferedReader -import java.io.FileReader - -object WhisperCpuConfig { - val preferredThreadCount: Int - // Always use at least 2 threads: - get() = CpuInfo.getHighPerfCpuCount().coerceAtLeast(2) -} - -private class CpuInfo(private val lines: List) { - private fun getHighPerfCpuCount(): Int = try { - getHighPerfCpuCountByFrequencies() - } catch (e: Exception) { - Log.d(LOG_TAG, "Couldn't read CPU frequencies", e) - getHighPerfCpuCountByVariant() - } - - private fun getHighPerfCpuCountByFrequencies(): Int = - getCpuValues(property = "processor") { getMaxCpuFrequency(it.toInt()) } - .also { Log.d(LOG_TAG, "Binned cpu frequencies (frequency, count): ${it.binnedValues()}") } - .countDroppingMin() - - private fun getHighPerfCpuCountByVariant(): Int = - getCpuValues(property = "CPU variant") { it.substringAfter("0x").toInt(radix = 16) } - .also { Log.d(LOG_TAG, "Binned cpu variants (variant, count): ${it.binnedValues()}") } - .countKeepingMin() - - private fun List.binnedValues() = groupingBy { it }.eachCount() - - private fun getCpuValues(property: String, mapper: (String) -> Int) = lines - .asSequence() - .filter { it.startsWith(property) } - .map { mapper(it.substringAfter(':').trim()) } - .sorted() - .toList() - - - private fun List.countDroppingMin(): Int { - val min = min() - return count { it > min } - } - - private fun List.countKeepingMin(): Int { - val min = min() - return count { it == min } - } - - companion object { - private const val LOG_TAG = "WhisperCpuConfig" - - fun getHighPerfCpuCount(): Int = try { - readCpuInfo().getHighPerfCpuCount() - } catch (e: Exception) { - Log.d(LOG_TAG, "Couldn't read CPU info", e) - // Our best guess -- just return the # of CPUs minus 4. - (Runtime.getRuntime().availableProcessors() - 4).coerceAtLeast(0) - } - - private fun readCpuInfo() = CpuInfo( - BufferedReader(FileReader("/proc/cpuinfo")) - .useLines { it.toList() } - ) - - private fun getMaxCpuFrequency(cpuIndex: Int): Int { - val path = "/sys/devices/system/cpu/cpu${cpuIndex}/cpufreq/cpuinfo_max_freq" - val maxFreq = BufferedReader(FileReader(path)).use { it.readLine() } - return maxFreq.toInt() - } - } -} \ No newline at end of file diff --git a/examples/whisper.android/app/src/main/jni/whisper/CMakeLists.txt b/examples/whisper.android/app/src/main/jni/whisper/CMakeLists.txt deleted file mode 100644 index 390fd196..00000000 --- a/examples/whisper.android/app/src/main/jni/whisper/CMakeLists.txt +++ /dev/null @@ -1,56 +0,0 @@ -cmake_minimum_required(VERSION 3.10) - -project(whisper.cpp) - -set(CMAKE_CXX_STANDARD 11) -set(WHISPER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../../../../../../) - -set( - SOURCE_FILES - ${WHISPER_LIB_DIR}/ggml.c - ${WHISPER_LIB_DIR}/ggml-alloc.c - ${WHISPER_LIB_DIR}/ggml-backend.c - ${WHISPER_LIB_DIR}/ggml-quants.c - ${WHISPER_LIB_DIR}/whisper.cpp - ${CMAKE_SOURCE_DIR}/jni.c -) - -find_library(LOG_LIB log) - -function(build_library target_name) - add_library( - ${target_name} - SHARED - ${SOURCE_FILES} - ) - - target_link_libraries(${target_name} ${LOG_LIB} android) - - if (${target_name} STREQUAL "whisper_v8fp16_va") - target_compile_options(${target_name} PRIVATE -march=armv8.2-a+fp16) - elseif (${target_name} STREQUAL "whisper_vfpv4") - target_compile_options(${target_name} PRIVATE -mfpu=neon-vfpv4) - endif () - - if (NOT ${CMAKE_BUILD_TYPE} STREQUAL "Debug") - - target_compile_options(${target_name} PRIVATE -O3) - target_compile_options(${target_name} PRIVATE -fvisibility=hidden -fvisibility-inlines-hidden) - target_compile_options(${target_name} PRIVATE -ffunction-sections -fdata-sections) - - target_link_options(${target_name} PRIVATE -Wl,--gc-sections) - target_link_options(${target_name} PRIVATE -Wl,--exclude-libs,ALL) - target_link_options(${target_name} PRIVATE -flto) - - endif () -endfunction() - -build_library("whisper") # Default target - -if (${ANDROID_ABI} STREQUAL "arm64-v8a") - build_library("whisper_v8fp16_va") -elseif (${ANDROID_ABI} STREQUAL "armeabi-v7a") - build_library("whisper_vfpv4") -endif () - -include_directories(${WHISPER_LIB_DIR}) diff --git a/examples/whisper.android/app/src/main/jni/whisper/jni.c b/examples/whisper.android/app/src/main/jni/whisper/jni.c deleted file mode 100644 index a8b3ded4..00000000 --- a/examples/whisper.android/app/src/main/jni/whisper/jni.c +++ /dev/null @@ -1,239 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include "whisper.h" -#include "ggml.h" - -#define UNUSED(x) (void)(x) -#define TAG "JNI" - -#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) -#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__) - -static inline int min(int a, int b) { - return (a < b) ? a : b; -} - -static inline int max(int a, int b) { - return (a > b) ? a : b; -} - -struct input_stream_context { - size_t offset; - JNIEnv * env; - jobject thiz; - jobject input_stream; - - jmethodID mid_available; - jmethodID mid_read; -}; - -size_t inputStreamRead(void * ctx, void * output, size_t read_size) { - struct input_stream_context* is = (struct input_stream_context*)ctx; - - jint avail_size = (*is->env)->CallIntMethod(is->env, is->input_stream, is->mid_available); - jint size_to_copy = read_size < avail_size ? (jint)read_size : avail_size; - - jbyteArray byte_array = (*is->env)->NewByteArray(is->env, size_to_copy); - - jint n_read = (*is->env)->CallIntMethod(is->env, is->input_stream, is->mid_read, byte_array, 0, size_to_copy); - - if (size_to_copy != read_size || size_to_copy != n_read) { - LOGI("Insufficient Read: Req=%zu, ToCopy=%d, Available=%d", read_size, size_to_copy, n_read); - } - - jbyte* byte_array_elements = (*is->env)->GetByteArrayElements(is->env, byte_array, NULL); - memcpy(output, byte_array_elements, size_to_copy); - (*is->env)->ReleaseByteArrayElements(is->env, byte_array, byte_array_elements, JNI_ABORT); - - (*is->env)->DeleteLocalRef(is->env, byte_array); - - is->offset += size_to_copy; - - return size_to_copy; -} -bool inputStreamEof(void * ctx) { - struct input_stream_context* is = (struct input_stream_context*)ctx; - - jint result = (*is->env)->CallIntMethod(is->env, is->input_stream, is->mid_available); - return result <= 0; -} -void inputStreamClose(void * ctx) { - -} - -JNIEXPORT jlong JNICALL -Java_com_whispercppdemo_whisper_WhisperLib_00024Companion_initContextFromInputStream( - JNIEnv *env, jobject thiz, jobject input_stream) { - UNUSED(thiz); - - struct whisper_context *context = NULL; - struct whisper_model_loader loader = {}; - struct input_stream_context inp_ctx = {}; - - inp_ctx.offset = 0; - inp_ctx.env = env; - inp_ctx.thiz = thiz; - inp_ctx.input_stream = input_stream; - - jclass cls = (*env)->GetObjectClass(env, input_stream); - inp_ctx.mid_available = (*env)->GetMethodID(env, cls, "available", "()I"); - inp_ctx.mid_read = (*env)->GetMethodID(env, cls, "read", "([BII)I"); - - loader.context = &inp_ctx; - loader.read = inputStreamRead; - loader.eof = inputStreamEof; - loader.close = inputStreamClose; - - loader.eof(loader.context); - - context = whisper_init(&loader); - return (jlong) context; -} - -static size_t asset_read(void *ctx, void *output, size_t read_size) { - return AAsset_read((AAsset *) ctx, output, read_size); -} - -static bool asset_is_eof(void *ctx) { - return AAsset_getRemainingLength64((AAsset *) ctx) <= 0; -} - -static void asset_close(void *ctx) { - AAsset_close((AAsset *) ctx); -} - -static struct whisper_context *whisper_init_from_asset( - JNIEnv *env, - jobject assetManager, - const char *asset_path -) { - LOGI("Loading model from asset '%s'\n", asset_path); - AAssetManager *asset_manager = AAssetManager_fromJava(env, assetManager); - AAsset *asset = AAssetManager_open(asset_manager, asset_path, AASSET_MODE_STREAMING); - if (!asset) { - LOGW("Failed to open '%s'\n", asset_path); - return NULL; - } - - whisper_model_loader loader = { - .context = asset, - .read = &asset_read, - .eof = &asset_is_eof, - .close = &asset_close - }; - - return whisper_init_with_params(&loader, whisper_context_default_params()); -} - -JNIEXPORT jlong JNICALL -Java_com_whispercppdemo_whisper_WhisperLib_00024Companion_initContextFromAsset( - JNIEnv *env, jobject thiz, jobject assetManager, jstring asset_path_str) { - UNUSED(thiz); - struct whisper_context *context = NULL; - const char *asset_path_chars = (*env)->GetStringUTFChars(env, asset_path_str, NULL); - context = whisper_init_from_asset(env, assetManager, asset_path_chars); - (*env)->ReleaseStringUTFChars(env, asset_path_str, asset_path_chars); - return (jlong) context; -} - -JNIEXPORT jlong JNICALL -Java_com_whispercppdemo_whisper_WhisperLib_00024Companion_initContext( - JNIEnv *env, jobject thiz, jstring model_path_str) { - UNUSED(thiz); - struct whisper_context *context = NULL; - const char *model_path_chars = (*env)->GetStringUTFChars(env, model_path_str, NULL); - context = whisper_init_from_file_with_params(model_path_chars, whisper_context_default_params()); - (*env)->ReleaseStringUTFChars(env, model_path_str, model_path_chars); - return (jlong) context; -} - -JNIEXPORT void JNICALL -Java_com_whispercppdemo_whisper_WhisperLib_00024Companion_freeContext( - JNIEnv *env, jobject thiz, jlong context_ptr) { - UNUSED(env); - UNUSED(thiz); - struct whisper_context *context = (struct whisper_context *) context_ptr; - whisper_free(context); -} - -JNIEXPORT void JNICALL -Java_com_whispercppdemo_whisper_WhisperLib_00024Companion_fullTranscribe( - JNIEnv *env, jobject thiz, jlong context_ptr, jint num_threads, jfloatArray audio_data) { - UNUSED(thiz); - struct whisper_context *context = (struct whisper_context *) context_ptr; - jfloat *audio_data_arr = (*env)->GetFloatArrayElements(env, audio_data, NULL); - const jsize audio_data_length = (*env)->GetArrayLength(env, audio_data); - - // The below adapted from the Objective-C iOS sample - struct whisper_full_params params = whisper_full_default_params(WHISPER_SAMPLING_GREEDY); - params.print_realtime = true; - params.print_progress = false; - params.print_timestamps = true; - params.print_special = false; - params.translate = false; - params.language = "en"; - params.n_threads = num_threads; - params.offset_ms = 0; - params.no_context = true; - params.single_segment = false; - - whisper_reset_timings(context); - - LOGI("About to run whisper_full"); - if (whisper_full(context, params, audio_data_arr, audio_data_length) != 0) { - LOGI("Failed to run the model"); - } else { - whisper_print_timings(context); - } - (*env)->ReleaseFloatArrayElements(env, audio_data, audio_data_arr, JNI_ABORT); -} - -JNIEXPORT jint JNICALL -Java_com_whispercppdemo_whisper_WhisperLib_00024Companion_getTextSegmentCount( - JNIEnv *env, jobject thiz, jlong context_ptr) { - UNUSED(env); - UNUSED(thiz); - struct whisper_context *context = (struct whisper_context *) context_ptr; - return whisper_full_n_segments(context); -} - -JNIEXPORT jstring JNICALL -Java_com_whispercppdemo_whisper_WhisperLib_00024Companion_getTextSegment( - JNIEnv *env, jobject thiz, jlong context_ptr, jint index) { - UNUSED(thiz); - struct whisper_context *context = (struct whisper_context *) context_ptr; - const char *text = whisper_full_get_segment_text(context, index); - jstring string = (*env)->NewStringUTF(env, text); - return string; -} - -JNIEXPORT jstring JNICALL -Java_com_whispercppdemo_whisper_WhisperLib_00024Companion_getSystemInfo( - JNIEnv *env, jobject thiz -) { - UNUSED(thiz); - const char *sysinfo = whisper_print_system_info(); - jstring string = (*env)->NewStringUTF(env, sysinfo); - return string; -} - -JNIEXPORT jstring JNICALL -Java_com_whispercppdemo_whisper_WhisperLib_00024Companion_benchMemcpy(JNIEnv *env, jobject thiz, - jint n_threads) { - UNUSED(thiz); - const char *bench_ggml_memcpy = whisper_bench_memcpy_str(n_threads); - jstring string = (*env)->NewStringUTF(env, bench_ggml_memcpy); -} - -JNIEXPORT jstring JNICALL -Java_com_whispercppdemo_whisper_WhisperLib_00024Companion_benchGgmlMulMat(JNIEnv *env, jobject thiz, - jint n_threads) { - UNUSED(thiz); - const char *bench_ggml_mul_mat = whisper_bench_ggml_mul_mat_str(n_threads); - jstring string = (*env)->NewStringUTF(env, bench_ggml_mul_mat); -} diff --git a/examples/whisper.android/lib/.gitignore b/examples/whisper.android/lib/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/examples/whisper.android/lib/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/examples/whisper.android/lib/build.gradle b/examples/whisper.android/lib/build.gradle new file mode 100644 index 00000000..c32a6899 --- /dev/null +++ b/examples/whisper.android/lib/build.gradle @@ -0,0 +1,51 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.whispercpp' + compileSdk 34 + + defaultConfig { + minSdk 26 + targetSdk 34 + versionCode 1 + versionName "1.0" + + ndk { + abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64' + } + } + + buildTypes { + release { + minifyEnabled false + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + + ndkVersion "25.2.9519653" + externalNativeBuild { + cmake { + path = file("src/main/jni/whisper/CMakeLists.txt") + } + } + packagingOptions { + resources { + excludes += '/META-INF/{AL2.0,LGPL2.1}' + } + } +} + +dependencies { + implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.8.0' +} \ No newline at end of file diff --git a/examples/whisper.android/lib/src/main/AndroidManifest.xml b/examples/whisper.android/lib/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/examples/whisper.android/lib/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/examples/whisper.android/lib/src/main/java/com/whispercpp/whisper/LibWhisper.kt b/examples/whisper.android/lib/src/main/java/com/whispercpp/whisper/LibWhisper.kt new file mode 100644 index 00000000..513202fa --- /dev/null +++ b/examples/whisper.android/lib/src/main/java/com/whispercpp/whisper/LibWhisper.kt @@ -0,0 +1,157 @@ +package com.whispercpp.whisper + +import android.content.res.AssetManager +import android.os.Build +import android.util.Log +import kotlinx.coroutines.* +import java.io.File +import java.io.InputStream +import java.util.concurrent.Executors + +private const val LOG_TAG = "LibWhisper" + +class WhisperContext private constructor(private var ptr: Long) { + // Meet Whisper C++ constraint: Don't access from more than one thread at a time. + private val scope: CoroutineScope = CoroutineScope( + Executors.newSingleThreadExecutor().asCoroutineDispatcher() + ) + + suspend fun transcribeData(data: FloatArray): String = withContext(scope.coroutineContext) { + require(ptr != 0L) + val numThreads = WhisperCpuConfig.preferredThreadCount + Log.d(LOG_TAG, "Selecting $numThreads threads") + WhisperLib.fullTranscribe(ptr, numThreads, data) + val textCount = WhisperLib.getTextSegmentCount(ptr) + return@withContext buildString { + for (i in 0 until textCount) { + append(WhisperLib.getTextSegment(ptr, i)) + } + } + } + + suspend fun benchMemory(nthreads: Int): String = withContext(scope.coroutineContext) { + return@withContext WhisperLib.benchMemcpy(nthreads) + } + + suspend fun benchGgmlMulMat(nthreads: Int): String = withContext(scope.coroutineContext) { + return@withContext WhisperLib.benchGgmlMulMat(nthreads) + } + + suspend fun release() = withContext(scope.coroutineContext) { + if (ptr != 0L) { + WhisperLib.freeContext(ptr) + ptr = 0 + } + } + + protected fun finalize() { + runBlocking { + release() + } + } + + companion object { + fun createContextFromFile(filePath: String): WhisperContext { + val ptr = WhisperLib.initContext(filePath) + if (ptr == 0L) { + throw java.lang.RuntimeException("Couldn't create context with path $filePath") + } + return WhisperContext(ptr) + } + + fun createContextFromInputStream(stream: InputStream): WhisperContext { + val ptr = WhisperLib.initContextFromInputStream(stream) + + if (ptr == 0L) { + throw java.lang.RuntimeException("Couldn't create context from input stream") + } + return WhisperContext(ptr) + } + + fun createContextFromAsset(assetManager: AssetManager, assetPath: String): WhisperContext { + val ptr = WhisperLib.initContextFromAsset(assetManager, assetPath) + + if (ptr == 0L) { + throw java.lang.RuntimeException("Couldn't create context from asset $assetPath") + } + return WhisperContext(ptr) + } + + fun getSystemInfo(): String { + return WhisperLib.getSystemInfo() + } + } +} + +private class WhisperLib { + companion object { + init { + Log.d(LOG_TAG, "Primary ABI: ${Build.SUPPORTED_ABIS[0]}") + var loadVfpv4 = false + var loadV8fp16 = false + if (isArmEabiV7a()) { + // armeabi-v7a needs runtime detection support + val cpuInfo = cpuInfo() + cpuInfo?.let { + Log.d(LOG_TAG, "CPU info: $cpuInfo") + if (cpuInfo.contains("vfpv4")) { + Log.d(LOG_TAG, "CPU supports vfpv4") + loadVfpv4 = true + } + } + } else if (isArmEabiV8a()) { + // ARMv8.2a needs runtime detection support + val cpuInfo = cpuInfo() + cpuInfo?.let { + Log.d(LOG_TAG, "CPU info: $cpuInfo") + if (cpuInfo.contains("fphp")) { + Log.d(LOG_TAG, "CPU supports fp16 arithmetic") + loadV8fp16 = true + } + } + } + + if (loadVfpv4) { + Log.d(LOG_TAG, "Loading libwhisper_vfpv4.so") + System.loadLibrary("whisper_vfpv4") + } else if (loadV8fp16) { + Log.d(LOG_TAG, "Loading libwhisper_v8fp16_va.so") + System.loadLibrary("whisper_v8fp16_va") + } else { + Log.d(LOG_TAG, "Loading libwhisper.so") + System.loadLibrary("whisper") + } + } + + // JNI methods + external fun initContextFromInputStream(inputStream: InputStream): Long + external fun initContextFromAsset(assetManager: AssetManager, assetPath: String): Long + external fun initContext(modelPath: String): Long + external fun freeContext(contextPtr: Long) + external fun fullTranscribe(contextPtr: Long, numThreads: Int, audioData: FloatArray) + external fun getTextSegmentCount(contextPtr: Long): Int + external fun getTextSegment(contextPtr: Long, index: Int): String + external fun getSystemInfo(): String + external fun benchMemcpy(nthread: Int): String + external fun benchGgmlMulMat(nthread: Int): String + } +} + +private fun isArmEabiV7a(): Boolean { + return Build.SUPPORTED_ABIS[0].equals("armeabi-v7a") +} + +private fun isArmEabiV8a(): Boolean { + return Build.SUPPORTED_ABIS[0].equals("arm64-v8a") +} + +private fun cpuInfo(): String? { + return try { + File("/proc/cpuinfo").inputStream().bufferedReader().use { + it.readText() + } + } catch (e: Exception) { + Log.w(LOG_TAG, "Couldn't read /proc/cpuinfo", e) + null + } +} \ No newline at end of file diff --git a/examples/whisper.android/lib/src/main/java/com/whispercpp/whisper/WhisperCpuConfig.kt b/examples/whisper.android/lib/src/main/java/com/whispercpp/whisper/WhisperCpuConfig.kt new file mode 100644 index 00000000..edfb415f --- /dev/null +++ b/examples/whisper.android/lib/src/main/java/com/whispercpp/whisper/WhisperCpuConfig.kt @@ -0,0 +1,73 @@ +package com.whispercpp.whisper + +import android.util.Log +import java.io.BufferedReader +import java.io.FileReader + +object WhisperCpuConfig { + val preferredThreadCount: Int + // Always use at least 2 threads: + get() = CpuInfo.getHighPerfCpuCount().coerceAtLeast(2) +} + +private class CpuInfo(private val lines: List) { + private fun getHighPerfCpuCount(): Int = try { + getHighPerfCpuCountByFrequencies() + } catch (e: Exception) { + Log.d(LOG_TAG, "Couldn't read CPU frequencies", e) + getHighPerfCpuCountByVariant() + } + + private fun getHighPerfCpuCountByFrequencies(): Int = + getCpuValues(property = "processor") { getMaxCpuFrequency(it.toInt()) } + .also { Log.d(LOG_TAG, "Binned cpu frequencies (frequency, count): ${it.binnedValues()}") } + .countDroppingMin() + + private fun getHighPerfCpuCountByVariant(): Int = + getCpuValues(property = "CPU variant") { it.substringAfter("0x").toInt(radix = 16) } + .also { Log.d(LOG_TAG, "Binned cpu variants (variant, count): ${it.binnedValues()}") } + .countKeepingMin() + + private fun List.binnedValues() = groupingBy { it }.eachCount() + + private fun getCpuValues(property: String, mapper: (String) -> Int) = lines + .asSequence() + .filter { it.startsWith(property) } + .map { mapper(it.substringAfter(':').trim()) } + .sorted() + .toList() + + + private fun List.countDroppingMin(): Int { + val min = min() + return count { it > min } + } + + private fun List.countKeepingMin(): Int { + val min = min() + return count { it == min } + } + + companion object { + private const val LOG_TAG = "WhisperCpuConfig" + + fun getHighPerfCpuCount(): Int = try { + readCpuInfo().getHighPerfCpuCount() + } catch (e: Exception) { + Log.d(LOG_TAG, "Couldn't read CPU info", e) + // Our best guess -- just return the # of CPUs minus 4. + (Runtime.getRuntime().availableProcessors() - 4).coerceAtLeast(0) + } + + private fun readCpuInfo() = CpuInfo( + BufferedReader(FileReader("/proc/cpuinfo")) + .useLines { it.toList() } + ) + + private fun getMaxCpuFrequency(cpuIndex: Int): Int { + val path = "/sys/devices/system/cpu/cpu${cpuIndex}/cpufreq/cpuinfo_max_freq" + val maxFreq = BufferedReader(FileReader(path)).use { it.readLine() } + return maxFreq.toInt() + } + } +} \ No newline at end of file diff --git a/examples/whisper.android/lib/src/main/jni/whisper/CMakeLists.txt b/examples/whisper.android/lib/src/main/jni/whisper/CMakeLists.txt new file mode 100644 index 00000000..390fd196 --- /dev/null +++ b/examples/whisper.android/lib/src/main/jni/whisper/CMakeLists.txt @@ -0,0 +1,56 @@ +cmake_minimum_required(VERSION 3.10) + +project(whisper.cpp) + +set(CMAKE_CXX_STANDARD 11) +set(WHISPER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../../../../../../) + +set( + SOURCE_FILES + ${WHISPER_LIB_DIR}/ggml.c + ${WHISPER_LIB_DIR}/ggml-alloc.c + ${WHISPER_LIB_DIR}/ggml-backend.c + ${WHISPER_LIB_DIR}/ggml-quants.c + ${WHISPER_LIB_DIR}/whisper.cpp + ${CMAKE_SOURCE_DIR}/jni.c +) + +find_library(LOG_LIB log) + +function(build_library target_name) + add_library( + ${target_name} + SHARED + ${SOURCE_FILES} + ) + + target_link_libraries(${target_name} ${LOG_LIB} android) + + if (${target_name} STREQUAL "whisper_v8fp16_va") + target_compile_options(${target_name} PRIVATE -march=armv8.2-a+fp16) + elseif (${target_name} STREQUAL "whisper_vfpv4") + target_compile_options(${target_name} PRIVATE -mfpu=neon-vfpv4) + endif () + + if (NOT ${CMAKE_BUILD_TYPE} STREQUAL "Debug") + + target_compile_options(${target_name} PRIVATE -O3) + target_compile_options(${target_name} PRIVATE -fvisibility=hidden -fvisibility-inlines-hidden) + target_compile_options(${target_name} PRIVATE -ffunction-sections -fdata-sections) + + target_link_options(${target_name} PRIVATE -Wl,--gc-sections) + target_link_options(${target_name} PRIVATE -Wl,--exclude-libs,ALL) + target_link_options(${target_name} PRIVATE -flto) + + endif () +endfunction() + +build_library("whisper") # Default target + +if (${ANDROID_ABI} STREQUAL "arm64-v8a") + build_library("whisper_v8fp16_va") +elseif (${ANDROID_ABI} STREQUAL "armeabi-v7a") + build_library("whisper_vfpv4") +endif () + +include_directories(${WHISPER_LIB_DIR}) diff --git a/examples/whisper.android/lib/src/main/jni/whisper/jni.c b/examples/whisper.android/lib/src/main/jni/whisper/jni.c new file mode 100644 index 00000000..08825ed9 --- /dev/null +++ b/examples/whisper.android/lib/src/main/jni/whisper/jni.c @@ -0,0 +1,239 @@ +#include +#include +#include +#include +#include +#include +#include +#include "whisper.h" +#include "ggml.h" + +#define UNUSED(x) (void)(x) +#define TAG "JNI" + +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__) + +static inline int min(int a, int b) { + return (a < b) ? a : b; +} + +static inline int max(int a, int b) { + return (a > b) ? a : b; +} + +struct input_stream_context { + size_t offset; + JNIEnv * env; + jobject thiz; + jobject input_stream; + + jmethodID mid_available; + jmethodID mid_read; +}; + +size_t inputStreamRead(void * ctx, void * output, size_t read_size) { + struct input_stream_context* is = (struct input_stream_context*)ctx; + + jint avail_size = (*is->env)->CallIntMethod(is->env, is->input_stream, is->mid_available); + jint size_to_copy = read_size < avail_size ? (jint)read_size : avail_size; + + jbyteArray byte_array = (*is->env)->NewByteArray(is->env, size_to_copy); + + jint n_read = (*is->env)->CallIntMethod(is->env, is->input_stream, is->mid_read, byte_array, 0, size_to_copy); + + if (size_to_copy != read_size || size_to_copy != n_read) { + LOGI("Insufficient Read: Req=%zu, ToCopy=%d, Available=%d", read_size, size_to_copy, n_read); + } + + jbyte* byte_array_elements = (*is->env)->GetByteArrayElements(is->env, byte_array, NULL); + memcpy(output, byte_array_elements, size_to_copy); + (*is->env)->ReleaseByteArrayElements(is->env, byte_array, byte_array_elements, JNI_ABORT); + + (*is->env)->DeleteLocalRef(is->env, byte_array); + + is->offset += size_to_copy; + + return size_to_copy; +} +bool inputStreamEof(void * ctx) { + struct input_stream_context* is = (struct input_stream_context*)ctx; + + jint result = (*is->env)->CallIntMethod(is->env, is->input_stream, is->mid_available); + return result <= 0; +} +void inputStreamClose(void * ctx) { + +} + +JNIEXPORT jlong JNICALL +Java_com_whispercppdemo_whisper_WhisperLib_00024Companion_initContextFromInputStream( + JNIEnv *env, jobject thiz, jobject input_stream) { + UNUSED(thiz); + + struct whisper_context *context = NULL; + struct whisper_model_loader loader = {}; + struct input_stream_context inp_ctx = {}; + + inp_ctx.offset = 0; + inp_ctx.env = env; + inp_ctx.thiz = thiz; + inp_ctx.input_stream = input_stream; + + jclass cls = (*env)->GetObjectClass(env, input_stream); + inp_ctx.mid_available = (*env)->GetMethodID(env, cls, "available", "()I"); + inp_ctx.mid_read = (*env)->GetMethodID(env, cls, "read", "([BII)I"); + + loader.context = &inp_ctx; + loader.read = inputStreamRead; + loader.eof = inputStreamEof; + loader.close = inputStreamClose; + + loader.eof(loader.context); + + context = whisper_init(&loader); + return (jlong) context; +} + +static size_t asset_read(void *ctx, void *output, size_t read_size) { + return AAsset_read((AAsset *) ctx, output, read_size); +} + +static bool asset_is_eof(void *ctx) { + return AAsset_getRemainingLength64((AAsset *) ctx) <= 0; +} + +static void asset_close(void *ctx) { + AAsset_close((AAsset *) ctx); +} + +static struct whisper_context *whisper_init_from_asset( + JNIEnv *env, + jobject assetManager, + const char *asset_path +) { + LOGI("Loading model from asset '%s'\n", asset_path); + AAssetManager *asset_manager = AAssetManager_fromJava(env, assetManager); + AAsset *asset = AAssetManager_open(asset_manager, asset_path, AASSET_MODE_STREAMING); + if (!asset) { + LOGW("Failed to open '%s'\n", asset_path); + return NULL; + } + + whisper_model_loader loader = { + .context = asset, + .read = &asset_read, + .eof = &asset_is_eof, + .close = &asset_close + }; + + return whisper_init_with_params(&loader, whisper_context_default_params()); +} + +JNIEXPORT jlong JNICALL +Java_com_whispercpp_whisper_WhisperLib_00024Companion_initContextFromAsset( + JNIEnv *env, jobject thiz, jobject assetManager, jstring asset_path_str) { + UNUSED(thiz); + struct whisper_context *context = NULL; + const char *asset_path_chars = (*env)->GetStringUTFChars(env, asset_path_str, NULL); + context = whisper_init_from_asset(env, assetManager, asset_path_chars); + (*env)->ReleaseStringUTFChars(env, asset_path_str, asset_path_chars); + return (jlong) context; +} + +JNIEXPORT jlong JNICALL +Java_com_whispercpp_whisper_WhisperLib_00024Companion_initContext( + JNIEnv *env, jobject thiz, jstring model_path_str) { + UNUSED(thiz); + struct whisper_context *context = NULL; + const char *model_path_chars = (*env)->GetStringUTFChars(env, model_path_str, NULL); + context = whisper_init_from_file_with_params(model_path_chars, whisper_context_default_params()); + (*env)->ReleaseStringUTFChars(env, model_path_str, model_path_chars); + return (jlong) context; +} + +JNIEXPORT void JNICALL +Java_com_whispercpp_whisper_WhisperLib_00024Companion_freeContext( + JNIEnv *env, jobject thiz, jlong context_ptr) { + UNUSED(env); + UNUSED(thiz); + struct whisper_context *context = (struct whisper_context *) context_ptr; + whisper_free(context); +} + +JNIEXPORT void JNICALL +Java_com_whispercpp_whisper_WhisperLib_00024Companion_fullTranscribe( + JNIEnv *env, jobject thiz, jlong context_ptr, jint num_threads, jfloatArray audio_data) { + UNUSED(thiz); + struct whisper_context *context = (struct whisper_context *) context_ptr; + jfloat *audio_data_arr = (*env)->GetFloatArrayElements(env, audio_data, NULL); + const jsize audio_data_length = (*env)->GetArrayLength(env, audio_data); + + // The below adapted from the Objective-C iOS sample + struct whisper_full_params params = whisper_full_default_params(WHISPER_SAMPLING_GREEDY); + params.print_realtime = true; + params.print_progress = false; + params.print_timestamps = true; + params.print_special = false; + params.translate = false; + params.language = "en"; + params.n_threads = num_threads; + params.offset_ms = 0; + params.no_context = true; + params.single_segment = false; + + whisper_reset_timings(context); + + LOGI("About to run whisper_full"); + if (whisper_full(context, params, audio_data_arr, audio_data_length) != 0) { + LOGI("Failed to run the model"); + } else { + whisper_print_timings(context); + } + (*env)->ReleaseFloatArrayElements(env, audio_data, audio_data_arr, JNI_ABORT); +} + +JNIEXPORT jint JNICALL +Java_com_whispercpp_whisper_WhisperLib_00024Companion_getTextSegmentCount( + JNIEnv *env, jobject thiz, jlong context_ptr) { + UNUSED(env); + UNUSED(thiz); + struct whisper_context *context = (struct whisper_context *) context_ptr; + return whisper_full_n_segments(context); +} + +JNIEXPORT jstring JNICALL +Java_com_whispercpp_whisper_WhisperLib_00024Companion_getTextSegment( + JNIEnv *env, jobject thiz, jlong context_ptr, jint index) { + UNUSED(thiz); + struct whisper_context *context = (struct whisper_context *) context_ptr; + const char *text = whisper_full_get_segment_text(context, index); + jstring string = (*env)->NewStringUTF(env, text); + return string; +} + +JNIEXPORT jstring JNICALL +Java_com_whispercpp_whisper_WhisperLib_00024Companion_getSystemInfo( + JNIEnv *env, jobject thiz +) { + UNUSED(thiz); + const char *sysinfo = whisper_print_system_info(); + jstring string = (*env)->NewStringUTF(env, sysinfo); + return string; +} + +JNIEXPORT jstring JNICALL +Java_com_whispercpp_whisper_WhisperLib_00024Companion_benchMemcpy(JNIEnv *env, jobject thiz, + jint n_threads) { + UNUSED(thiz); + const char *bench_ggml_memcpy = whisper_bench_memcpy_str(n_threads); + jstring string = (*env)->NewStringUTF(env, bench_ggml_memcpy); +} + +JNIEXPORT jstring JNICALL +Java_com_whispercpp_whisper_WhisperLib_00024Companion_benchGgmlMulMat(JNIEnv *env, jobject thiz, + jint n_threads) { + UNUSED(thiz); + const char *bench_ggml_mul_mat = whisper_bench_ggml_mul_mat_str(n_threads); + jstring string = (*env)->NewStringUTF(env, bench_ggml_mul_mat); +} diff --git a/examples/whisper.android/settings.gradle b/examples/whisper.android/settings.gradle index 3deecee6..dbd8508e 100644 --- a/examples/whisper.android/settings.gradle +++ b/examples/whisper.android/settings.gradle @@ -14,3 +14,4 @@ dependencyResolutionManagement { } rootProject.name = "WhisperCppDemo" include ':app' +include ':lib'