cd examples/whisper.android
./gradlew assembleRelease --no-daemon
+ android_java:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Clone
+ uses: actions/checkout@v3
+
+ - name: set up JDK 11
+ uses: actions/setup-java@v3
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+ cache: gradle
+
+ - name: Setup Android SDK
+ uses: android-actions/setup-android@v2
+ with:
+ api-level: 30
+ build-tools-version: 30.0.3
+
+ - name: Build
+ run: |
+ cd examples/whisper.android.java
+ chmod +x ./gradlew
+ ./gradlew assembleRelease
+
java:
needs: [ 'windows' ]
runs-on: windows-latest
.idea/
benchmark_results.csv
+cmake-build-debug/
+.cxx/
+.gradle/
+local.properties
\ No newline at end of file
group = 'io.github.ggerganov'\r
version = '1.4.0'\r
\r
+\r
sourceCompatibility = 1.8\r
targetCompatibility = 1.8\r
\r
\r
import com.sun.jna.Native;\r
import com.sun.jna.Pointer;\r
+import io.github.ggerganov.whispercpp.bean.WhisperSegment;\r
import io.github.ggerganov.whispercpp.params.WhisperContextParams;\r
import io.github.ggerganov.whispercpp.params.WhisperFullParams;\r
import io.github.ggerganov.whispercpp.params.WhisperSamplingStrategy;\r
import java.io.File;\r
import java.io.FileNotFoundException;\r
import java.io.IOException;\r
+import java.util.ArrayList;\r
+import java.util.List;\r
\r
/**\r
* Before calling most methods, you must call `initContext(modelPath)` to initialise the `ctx` Pointer.\r
\r
return str.toString().trim();\r
}\r
+ public List<WhisperSegment> fullTranscribeWithTime(WhisperFullParams whisperParams, float[] audioData) throws IOException {\r
+ if (ctx == null) {\r
+ throw new IllegalStateException("Model not initialised");\r
+ }\r
+\r
+ if (lib.whisper_full(ctx, whisperParams, audioData, audioData.length) != 0) {\r
+ throw new IOException("Failed to process audio");\r
+ }\r
+\r
+ int nSegments = lib.whisper_full_n_segments(ctx);\r
+ List<WhisperSegment> segments= new ArrayList<>(nSegments);\r
+\r
+\r
+ for (int i = 0; i < nSegments; i++) {\r
+ long t0 = lib.whisper_full_get_segment_t0(ctx, i);\r
+ String text = lib.whisper_full_get_segment_text(ctx, i);\r
+ long t1 = lib.whisper_full_get_segment_t1(ctx, i);\r
+ segments.add(new WhisperSegment(t0,t1,text));\r
+ }\r
+\r
+ return segments;\r
+ }\r
\r
// public int getTextSegmentCount(Pointer ctx) {\r
// return lib.whisper_full_n_segments(ctx);\r
--- /dev/null
+package io.github.ggerganov.whispercpp.bean;
+
+/**
+ * Created by litonglinux@qq.com on 10/21/2023_7:48 AM
+ */
+public class WhisperSegment {
+ private long start, end;
+ private String sentence;
+
+ public WhisperSegment() {
+ }
+
+ public WhisperSegment(long start, long end, String sentence) {
+ this.start = start;
+ this.end = end;
+ this.sentence = sentence;
+ }
+
+ public long getStart() {
+ return start;
+ }
+
+ public long getEnd() {
+ return end;
+ }
+
+ public String getSentence() {
+ return sentence;
+ }
+
+ public void setStart(long start) {
+ this.start = start;
+ }
+
+ public void setEnd(long end) {
+ this.end = end;
+ }
+
+ public void setSentence(String sentence) {
+ this.sentence = sentence;
+ }
+
+ @Override
+ public String toString() {
+ return "[" + start + " --> " + end + "]:" + sentence;
+ }
+}
\r
import static org.junit.jupiter.api.Assertions.*;\r
\r
+import io.github.ggerganov.whispercpp.bean.WhisperSegment;\r
import io.github.ggerganov.whispercpp.params.CBool;\r
import io.github.ggerganov.whispercpp.params.WhisperFullParams;\r
import io.github.ggerganov.whispercpp.params.WhisperSamplingStrategy;\r
import javax.sound.sampled.AudioSystem;\r
import java.io.File;\r
import java.io.FileNotFoundException;\r
+import java.util.List;\r
\r
class WhisperCppTest {\r
private static WhisperCpp whisper = new WhisperCpp();\r
static void init() throws FileNotFoundException {\r
// By default, models are loaded from ~/.cache/whisper/ and are usually named "ggml-${name}.bin"\r
// or you can provide the absolute path to the model file.\r
- String modelName = "../../models/ggml-tiny.en.bin";\r
+ String modelName = "../../models/ggml-tiny.bin";\r
+// String modelName = "../../models/ggml-tiny.en.bin";\r
try {\r
whisper.initContext(modelName);\r
// whisper.getFullDefaultParams(WhisperSamplingStrategy.WHISPER_SAMPLING_GREEDY);\r
audioInputStream.close();\r
}\r
}\r
+\r
+ @Test\r
+ void testFullTranscribeWithTime() throws Exception {\r
+ if (!modelInitialised) {\r
+ System.out.println("Model not initialised, skipping test");\r
+ return;\r
+ }\r
+\r
+ // Given\r
+ File file = new File(System.getProperty("user.dir"), "../../samples/jfk.wav");\r
+ AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(file);\r
+\r
+ byte[] b = new byte[audioInputStream.available()];\r
+ float[] floats = new float[b.length / 2];\r
+\r
+// WhisperFullParams params = whisper.getFullDefaultParams(WhisperSamplingStrategy.WHISPER_SAMPLING_GREEDY);\r
+ WhisperFullParams params = whisper.getFullDefaultParams(WhisperSamplingStrategy.WHISPER_SAMPLING_BEAM_SEARCH);\r
+ params.setProgressCallback((ctx, state, progress, user_data) -> System.out.println("progress: " + progress));\r
+ params.print_progress = CBool.FALSE;\r
+// params.initial_prompt = "and so my fellow Americans um, like";\r
+\r
+\r
+ try {\r
+ audioInputStream.read(b);\r
+\r
+ for (int i = 0, j = 0; i < b.length; i += 2, j++) {\r
+ int intSample = (int) (b[i + 1]) << 8 | (int) (b[i]) & 0xFF;\r
+ floats[j] = intSample / 32767.0f;\r
+ }\r
+\r
+ List<WhisperSegment> segments = whisper.fullTranscribeWithTime(params, floats);\r
+ assertTrue(segments.size() > 0, "The size of segments should be greater than 0");\r
+ for (WhisperSegment segment : segments) {\r
+ System.out.println(segment);\r
+ }\r
+ } finally {\r
+ audioInputStream.close();\r
+ }\r
+ }\r
+\r
}\r
--- /dev/null
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
--- /dev/null
+A sample Android app using java code and [whisper.cpp](https://github.com/ggerganov/whisper.cpp/) to do voice-to-text transcriptions.
+
+To use:
+
+1. Select a model from the [whisper.cpp repository](https://github.com/ggerganov/whisper.cpp/tree/master/models).[^1]
+2. Copy the model to the "app/src/main/assets/models" folder.
+3. Select a sample audio file (for example, [jfk.wav](https://github.com/ggerganov/whisper.cpp/raw/master/samples/jfk.wav)).
+4. Copy the sample to the "app/src/main/assets/samples" folder.
+5. Modify the modelFilePath in the WhisperService.java
+6. Modify the sampleFilePath in the WhisperService.java
+7. Select the "release" active build variant, and use Android Studio to run and deploy to your device.
+[^1]: I recommend the tiny or base models for running on an Android device.
+
+PS:
+1. Do not move this android project folder individually to other folders, because this android project folder depends on the files of the whole project.
+2. The cpp code is compiled during the build process
+3. If you want to import a compiled cpp project in your Android project, please refer to the https://github.com/litongjava/whisper.cpp.android.java.demo
+
+
+
--- /dev/null
+/build
\ No newline at end of file
--- /dev/null
+plugins {
+ id 'com.android.application'
+}
+
+android {
+ compileSdkVersion 30
+ buildToolsVersion '30.0.3'
+
+ defaultConfig {
+ applicationId "com.litongjava.whisper.android.java"
+ minSdkVersion 21
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ externalNativeBuild {
+ cmake {
+ cppFlags ""
+ }
+ }
+ ndk {
+ abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'
+ }
+ }
+
+ buildTypes {
+ release {
+ signingConfig signingConfigs.debug
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ externalNativeBuild {
+ cmake {
+ path "src/main/jni/whisper/CMakeLists.txt"
+ }
+ }
+ ndkVersion "25.2.9519653"
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'com.google.android.material:material:1.1.0'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ testImplementation 'junit:junit:4.+'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+
+ //litongjava
+ implementation 'com.litongjava:android-view-inject:1.0'
+ implementation 'com.litongjava:jfinal-aop:1.0.1'
+ implementation 'com.litongjava:litongjava-android-utils:1.0.0'
+}
\ No newline at end of file
--- /dev/null
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
--- /dev/null
+package com.litongjava.whisper.android.java;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("com.litongjava.whisper.android.java", appContext.getPackageName());
+ }
+}
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.litongjava.whisper.android.java">
+
+ <application
+ android:allowBackup="true"
+ android:name=".app.App"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.Whisperandroidjava">
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+<configuration debug="false" xmlns="http://ch.qos.logback/xml/ns/logback"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback https://raw.githubusercontent.com/enricopulatzo/logback-XSD/master/src/main/xsd/logback.xsd
+http://ch.qos.logback/xml/ns/logback ">
+ <!--Define the storage address of the log file Do not use relative paths in the LogBack configuration. -->
+ <property name="LOG_HOME" value="logs" />
+ <!--Formatted output: %d means the date, %-6level: log level from the left display 6 characters wide, %m: log message, %n is a newline character -->
+ <property name="CONSOLE_LOG_PATTERN"
+ value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-6level%logger{0}.%M:%L - %m%n" />
+
+ <!-- console output -->
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>${CONSOLE_LOG_PATTERN}</pattern>
+ </encoder>
+ </appender>
+
+ <!-- Generate log files on a daily basis -->
+ <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>${CONSOLE_LOG_PATTERN}</pattern>
+ </encoder>
+ <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+ <!--File name for log file output -->
+ <fileNamePattern>${LOG_HOME}/project-name-%d{yyyy-MM-dd}.log</fileNamePattern>
+ <!--Maximum size of log file -->
+ <maxHistory>180</maxHistory>
+ </rollingPolicy>
+ <!--日志文件最大的大小 -->
+ <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
+ <maxFileSize>10MB</maxFileSize>
+ </triggeringPolicy>
+ </appender>
+ <!-- Log output level and source-->
+ <root level="info">
+ <appender-ref ref="STDOUT" />
+ <appender-ref ref="FILE" />
+ </root>
+</configuration>
\ No newline at end of file
--- /dev/null
+package com.litongjava.whisper.android.java;
+
+import androidx.annotation.RequiresApi;
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.View;
+import android.widget.TextView;
+
+import com.blankj.utilcode.util.ThreadUtils;
+import com.litongjava.android.view.inject.annotation.FindViewById;
+import com.litongjava.android.view.inject.annotation.FindViewByIdLayout;
+import com.litongjava.android.view.inject.annotation.OnClick;
+import com.litongjava.android.view.inject.utils.ViewInjectUtils;
+import com.litongjava.jfinal.aop.Aop;
+import com.litongjava.jfinal.aop.AopManager;
+import com.litongjava.whisper.android.java.services.WhisperService;
+import com.litongjava.whisper.android.java.task.LoadModelTask;
+import com.litongjava.whisper.android.java.task.TranscriptionTask;
+import com.litongjava.whisper.android.java.utils.AssetUtils;
+import com.whispercpp.java.whisper.WhisperLib;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+
+
+@FindViewByIdLayout(R.layout.activity_main)
+public class MainActivity extends AppCompatActivity {
+
+ @FindViewById(R.id.sample_text)
+ private TextView tv;
+
+ Logger log = LoggerFactory.getLogger(this.getClass());
+ private WhisperService whisperService = Aop.get(WhisperService.class);
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ //setContentView(R.layout.activity_main);
+ ViewInjectUtils.injectActivity(this, this);
+ initAopBean();
+ showSystemInfo();
+ }
+
+ private void initAopBean() {
+ Handler mainHandler = new Handler(Looper.getMainLooper());
+ AopManager.me().addSingletonObject(mainHandler);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ @OnClick(R.id.loadModelBtn)
+ public void loadModelBtn_OnClick(View v) {
+ Context context = getBaseContext();
+ ThreadUtils.executeByIo(new LoadModelTask(tv));
+ }
+
+ @OnClick(R.id.transcriptSampleBtn)
+ public void transcriptSampleBtn_OnClick(View v) {
+ Context context = getBaseContext();
+
+ long start = System.currentTimeMillis();
+ String sampleFilePath = "samples/jfk.wav";
+ File filesDir = context.getFilesDir();
+ File sampleFile = AssetUtils.copyFileIfNotExists(context, filesDir, sampleFilePath);
+ long end = System.currentTimeMillis();
+ String msg = "copy file:" + (end - start) + "ms";
+ outputMsg(tv, msg);
+ ThreadUtils.executeByIo(new TranscriptionTask(tv, sampleFile));
+ }
+
+ private void outputMsg(TextView tv, String msg) {
+ tv.append(msg + "\n");
+ log.info(msg);
+ }
+
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ @OnClick(R.id.systemInfoBtn)
+ public void systemInfoBtn_OnClick(View v) {
+ showSystemInfo();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public void showSystemInfo() {
+ String systemInfo = WhisperLib.getSystemInfo();
+ tv.append(systemInfo + "\n");
+ }
+
+ @OnClick(R.id.clearBtn)
+ public void clearBtn_OnClick(View v) {
+ tv.setText("");
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ whisperService.release();
+ }
+}
\ No newline at end of file
--- /dev/null
+package com.litongjava.whisper.android.java.app;
+
+import android.app.Application;
+
+import com.blankj.utilcode.util.Utils;
+
+public class App extends Application {
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Utils.init(this);
+ }
+}
--- /dev/null
+package com.litongjava.whisper.android.java.bean;
+
+/**
+ * Created by litonglinux@qq.com on 10/21/2023_7:48 AM
+ */
+public class WhisperSegment {
+ private long start, end;
+ private String sentence;
+
+ public WhisperSegment() {
+ }
+
+ public WhisperSegment(long start, long end, String sentence) {
+ this.start = start;
+ this.end = end;
+ this.sentence = sentence;
+ }
+
+ public long getStart() {
+ return start;
+ }
+
+ public long getEnd() {
+ return end;
+ }
+
+ public String getSentence() {
+ return sentence;
+ }
+
+ public void setStart(long start) {
+ this.start = start;
+ }
+
+ public void setEnd(long end) {
+ this.end = end;
+ }
+
+ public void setSentence(String sentence) {
+ this.sentence = sentence;
+ }
+
+ @Override
+ public String toString() {
+ return "["+start+" --> "+end+"]:"+sentence;
+ }
+}
--- /dev/null
+package com.litongjava.whisper.android.java.services;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.RequiresApi;
+
+import com.blankj.utilcode.util.ToastUtils;
+import com.blankj.utilcode.util.Utils;
+import com.litongjava.android.utils.dialog.AlertDialogUtils;
+import com.litongjava.jfinal.aop.Aop;
+import com.litongjava.whisper.android.java.bean.WhisperSegment;
+import com.litongjava.whisper.android.java.single.LocalWhisper;
+import com.litongjava.whisper.android.java.utils.WaveEncoder;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+public class WhisperService {
+ private Logger log = LoggerFactory.getLogger(this.getClass());
+
+ private final Object lock = new Object();
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public void loadModel(TextView tv) {
+ String modelFilePath = LocalWhisper.modelFilePath;
+ String msg = "load model from :" + modelFilePath + "\n";
+ outputMsg(tv, msg);
+
+ long start = System.currentTimeMillis();
+ LocalWhisper.INSTANCE.init();
+ long end = System.currentTimeMillis();
+ msg = "model load successful:" + (end - start) + "ms";
+ outputMsg(tv, msg);
+ ToastUtils.showLong(msg);
+
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public void transcribeSample(TextView tv, File sampleFile) {
+ String msg = "";
+ msg = "transcribe file from :" + sampleFile.getAbsolutePath();
+ outputMsg(tv, msg);
+
+ Long start = System.currentTimeMillis();
+ float[] audioData = new float[0]; // 读取音频样本
+ try {
+ audioData = WaveEncoder.decodeWaveFile(sampleFile);
+ } catch (IOException e) {
+ e.printStackTrace();
+ return;
+ }
+ long end = System.currentTimeMillis();
+ msg = "decode wave file:" + (end - start) + "ms";
+ outputMsg(tv, msg);
+
+ start = System.currentTimeMillis();
+ List<WhisperSegment> transcription = null;
+ try {
+ //transcription = LocalWhisper.INSTANCE.transcribeData(audioData);
+ transcription = LocalWhisper.INSTANCE.transcribeDataWithTime(audioData);
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ end = System.currentTimeMillis();
+ if(transcription!=null){
+ ToastUtils.showLong(transcription.toString());
+ msg = "Transcript successful:" + (end - start) + "ms";
+ outputMsg(tv, msg);
+
+ outputMsg(tv, transcription.toString());
+
+ }else{
+ msg = "Transcript failed:" + (end - start) + "ms";
+ outputMsg(tv, msg);
+ }
+
+ }
+
+ private void outputMsg(TextView tv, String msg) {
+ log.info(msg);
+ if(tv!=null){
+ Aop.get(Handler.class).post(()->{ tv.append(msg + "\n");});
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public void release() {
+ //noting to do
+ }
+}
--- /dev/null
+package com.litongjava.whisper.android.java.single;
+
+import android.app.Application;
+import android.os.Build;
+import android.os.Handler;
+
+import androidx.annotation.RequiresApi;
+
+import com.blankj.utilcode.util.ToastUtils;
+import com.blankj.utilcode.util.Utils;
+import com.litongjava.jfinal.aop.Aop;
+import com.litongjava.whisper.android.java.bean.WhisperSegment;
+import com.litongjava.whisper.android.java.utils.AssetUtils;
+import com.whispercpp.java.whisper.WhisperContext;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+
+@RequiresApi(api = Build.VERSION_CODES.O)
+public enum LocalWhisper {
+ INSTANCE;
+
+ public static final String modelFilePath = "models/ggml-tiny.bin";
+ private WhisperContext whisperContext;
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ LocalWhisper() {
+ Application context = Utils.getApp();
+ File filesDir = context.getFilesDir();
+ File modelFile = AssetUtils.copyFileIfNotExists(context, filesDir, modelFilePath);
+ String realModelFilePath = modelFile.getAbsolutePath();
+ whisperContext = WhisperContext.createContextFromFile(realModelFilePath);
+ }
+
+ public synchronized String transcribeData(float[] data) throws ExecutionException, InterruptedException {
+ if(whisperContext==null){
+ toastModelLoading();
+ return null;
+ }else{
+ return whisperContext.transcribeData(data);
+ }
+ }
+
+ private static void toastModelLoading() {
+ Aop.get(Handler.class).post(()->{
+ ToastUtils.showShort("please wait for model loading");
+ });
+ }
+
+ public List<WhisperSegment> transcribeDataWithTime(float[] audioData) throws ExecutionException, InterruptedException {
+ if(whisperContext==null){
+ toastModelLoading();
+ return null;
+ }else{
+ return whisperContext.transcribeDataWithTime(audioData);
+ }
+ }
+
+ public void init() {
+ //noting to do.but init
+ }
+
+
+}
--- /dev/null
+package com.litongjava.whisper.android.java.task;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.widget.TextView;
+
+import com.blankj.utilcode.util.ThreadUtils;
+import com.litongjava.jfinal.aop.Aop;
+import com.litongjava.whisper.android.java.services.WhisperService;
+
+import java.io.File;
+
+public class LoadModelTask extends ThreadUtils.Task<Object> {
+ private final TextView tv;
+ public LoadModelTask(TextView tv) {
+ this.tv = tv;
+ }
+
+ @Override
+ public Object doInBackground() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ Aop.get(WhisperService.class).loadModel(tv);
+ }else{
+ Aop.get(Handler.class).post(()->{
+ tv.append("not supported android devices");
+ });
+
+ }
+ return null;
+ }
+
+ @Override
+ public void onSuccess(Object result) {
+ }
+
+ @Override
+ public void onCancel() {
+ }
+
+ @Override
+ public void onFail(Throwable t) {
+ }
+}
\ No newline at end of file
--- /dev/null
+package com.litongjava.whisper.android.java.task;
+
+import android.content.Context;
+import android.os.Build;
+import android.widget.TextView;
+
+import com.blankj.utilcode.util.ThreadUtils;
+import com.litongjava.jfinal.aop.Aop;
+import com.litongjava.whisper.android.java.services.WhisperService;
+
+import java.io.File;
+
+public class TranscriptionTask extends ThreadUtils.Task<Object> {
+ private final TextView tv;
+ private final File sampleFile;
+
+ public TranscriptionTask(TextView tv, File sampleFile) {
+ this.tv = tv;
+ this.sampleFile = sampleFile;
+
+ }
+
+ @Override
+ public Object doInBackground() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ Aop.get(WhisperService.class).transcribeSample(tv, sampleFile);
+ }else{
+ tv.append("not supported android devices");
+ }
+ return null;
+ }
+
+ @Override
+ public void onSuccess(Object result) {
+ }
+
+ @Override
+ public void onCancel() {
+ }
+
+ @Override
+ public void onFail(Throwable t) {
+ }
+}
\ No newline at end of file
--- /dev/null
+package com.litongjava.whisper.android.java.utils;
+
+import android.content.Context;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class AssetUtils {
+ private static Logger log = LoggerFactory.getLogger(AssetUtils.class);
+
+ public static File copyFileIfNotExists(Context context, File distDir, String filename) {
+ File dstFile = new File(distDir, filename);
+ if (dstFile.exists()) {
+ return dstFile;
+ } else {
+ File parentFile = dstFile.getParentFile();
+ log.info("parentFile:{}", parentFile);
+ if (!parentFile.exists()) {
+ parentFile.mkdirs();
+ }
+ AssetUtils.copyFileFromAssets(context, filename, dstFile);
+ }
+ return dstFile;
+ }
+
+ public static void copyDirectoryFromAssets(Context appCtx, String srcDir, String dstDir) {
+ if (srcDir.isEmpty() || dstDir.isEmpty()) {
+ return;
+ }
+ try {
+ if (!new File(dstDir).exists()) {
+ new File(dstDir).mkdirs();
+ }
+ for (String fileName : appCtx.getAssets().list(srcDir)) {
+ String srcSubPath = srcDir + File.separator + fileName;
+ String dstSubPath = dstDir + File.separator + fileName;
+ if (new File(srcSubPath).isDirectory()) {
+ copyDirectoryFromAssets(appCtx, srcSubPath, dstSubPath);
+ } else {
+ copyFileFromAssets(appCtx, srcSubPath, dstSubPath);
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static void copyFileFromAssets(Context appCtx, String srcPath, String dstPath) {
+ File dstFile = new File(dstPath);
+ copyFileFromAssets(appCtx, srcPath, dstFile);
+ }
+
+ public static void copyFileFromAssets(Context appCtx, String srcPath, File dstFile) {
+ if (srcPath.isEmpty()) {
+ return;
+ }
+ InputStream is = null;
+ OutputStream os = null;
+ try {
+ is = new BufferedInputStream(appCtx.getAssets().open(srcPath));
+
+ os = new BufferedOutputStream(new FileOutputStream(dstFile));
+ byte[] buffer = new byte[1024];
+ int length = 0;
+ while ((length = is.read(buffer)) != -1) {
+ os.write(buffer, 0, length);
+ }
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ try {
+ os.close();
+ is.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+}
\ No newline at end of file
--- /dev/null
+package com.litongjava.whisper.android.java.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.ShortBuffer;
+
+public class WaveEncoder {
+
+ public static float[] decodeWaveFile(File file) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (FileInputStream fis = new FileInputStream(file)) {
+ byte[] buffer = new byte[1024];
+ int bytesRead;
+ while ((bytesRead = fis.read(buffer)) != -1) {
+ baos.write(buffer, 0, bytesRead);
+ }
+ }
+ ByteBuffer byteBuffer = ByteBuffer.wrap(baos.toByteArray());
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+
+ int channel = byteBuffer.getShort(22);
+ byteBuffer.position(44);
+
+ ShortBuffer shortBuffer = byteBuffer.asShortBuffer();
+ short[] shortArray = new short[shortBuffer.limit()];
+ shortBuffer.get(shortArray);
+
+ float[] output = new float[shortArray.length / channel];
+
+ for (int index = 0; index < output.length; index++) {
+ if (channel == 1) {
+ output[index] = Math.max(-1f, Math.min(1f, shortArray[index] / 32767.0f));
+ } else {
+ output[index] = Math.max(-1f, Math.min(1f, (shortArray[2 * index] + shortArray[2 * index + 1]) / 32767.0f / 2.0f));
+ }
+ }
+ return output;
+ }
+
+ public static void encodeWaveFile(File file, short[] data) throws IOException {
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ fos.write(headerBytes(data.length * 2));
+
+ ByteBuffer buffer = ByteBuffer.allocate(data.length * 2);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+ buffer.asShortBuffer().put(data);
+
+ byte[] bytes = new byte[buffer.limit()];
+ buffer.get(bytes);
+
+ fos.write(bytes);
+ }
+ }
+
+ private static byte[] headerBytes(int totalLength) {
+ if (totalLength < 44)
+ throw new IllegalArgumentException("Total length must be at least 44 bytes");
+
+ ByteBuffer buffer = ByteBuffer.allocate(44);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+
+ buffer.put((byte) 'R');
+ buffer.put((byte) 'I');
+ buffer.put((byte) 'F');
+ buffer.put((byte) 'F');
+
+ buffer.putInt(totalLength - 8);
+
+ buffer.put((byte) 'W');
+ buffer.put((byte) 'A');
+ buffer.put((byte) 'V');
+ buffer.put((byte) 'E');
+
+ buffer.put((byte) 'f');
+ buffer.put((byte) 'm');
+ buffer.put((byte) 't');
+ buffer.put((byte) ' ');
+
+ buffer.putInt(16);
+ buffer.putShort((short) 1);
+ buffer.putShort((short) 1);
+ buffer.putInt(16000);
+ buffer.putInt(32000);
+ buffer.putShort((short) 2);
+ buffer.putShort((short) 16);
+
+ buffer.put((byte) 'd');
+ buffer.put((byte) 'a');
+ buffer.put((byte) 't');
+ buffer.put((byte) 'a');
+
+ buffer.putInt(totalLength - 44);
+ buffer.position(0);
+
+ byte[] bytes = new byte[buffer.limit()];
+ buffer.get(bytes);
+
+ return bytes;
+ }
+}
\ No newline at end of file
--- /dev/null
+package com.whispercpp.java.whisper;
+
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class CpuInfo {
+ private static final String LOG_TAG = "WhisperCpuConfig";
+
+ private List<String> lines;
+
+ public CpuInfo(List<String> lines) {
+ this.lines = lines;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ public int getHighPerfCpuCount0() {
+ try {
+ return getHighPerfCpuCountByFrequencies();
+ } catch (Exception e) {
+ Log.d(LOG_TAG, "Couldn't read CPU frequencies", e);
+ return getHighPerfCpuCountByVariant();
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ private int getHighPerfCpuCountByFrequencies() {
+ List<Integer> frequencies = getCpuValues("processor", line -> {
+ try {
+ return getMaxCpuFrequency(Integer.parseInt(line.trim()));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return 0;
+ }
+ );
+ Log.d(LOG_TAG, "Binned cpu frequencies (frequency, count): " + binnedValues(frequencies));
+ return countDroppingMin(frequencies);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ private int getHighPerfCpuCountByVariant() {
+ List<Integer> variants = getCpuValues("CPU variant", line -> Integer.parseInt(line.trim().substring(line.indexOf("0x") + 2), 16));
+ Log.d(LOG_TAG, "Binned cpu variants (variant, count): " + binnedValues(variants));
+ return countKeepingMin(variants);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ private Map<Integer, Integer> binnedValues(List<Integer> values) {
+ Map<Integer, Integer> countMap = new HashMap<>();
+ for (int value : values) {
+ countMap.put(value, countMap.getOrDefault(value, 0) + 1);
+ }
+ return countMap;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ private List<Integer> getCpuValues(String property, Mapper mapper) {
+ List<Integer> values = new ArrayList<>();
+ for (String line : lines) {
+ if (line.startsWith(property)) {
+ values.add(mapper.map(line.substring(line.indexOf(':') + 1)));
+ }
+ }
+ values.sort(Integer::compareTo);
+ return values;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ private int countDroppingMin(List<Integer> values) {
+ int min = values.stream().mapToInt(i -> i).min().orElse(Integer.MAX_VALUE);
+ return (int) values.stream().filter(value -> value > min).count();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ private int countKeepingMin(List<Integer> values) {
+ int min = values.stream().mapToInt(i -> i).min().orElse(Integer.MAX_VALUE);
+ return (int) values.stream().filter(value -> value.equals(min)).count();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ public static int getHighPerfCpuCount() {
+ try {
+ return readCpuInfo().getHighPerfCpuCount0();
+ } catch (Exception e) {
+ Log.d(LOG_TAG, "Couldn't read CPU info", e);
+ return Math.max(Runtime.getRuntime().availableProcessors() - 4, 0);
+ }
+ }
+
+ private static CpuInfo readCpuInfo() throws IOException {
+ try (BufferedReader reader = new BufferedReader(new FileReader("/proc/cpuinfo"))) {
+ List<String> lines = new ArrayList<>();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ lines.add(line);
+ }
+ return new CpuInfo(lines);
+ }
+ }
+
+ private static int getMaxCpuFrequency(int cpuIndex) throws IOException {
+ String path = "/sys/devices/system/cpu/cpu" + cpuIndex + "/cpufreq/cpuinfo_max_freq";
+ try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
+ return Integer.parseInt(reader.readLine());
+ }
+ }
+
+ private interface Mapper {
+ int map(String line);
+ }
+}
--- /dev/null
+package com.whispercpp.java.whisper;
+
+import android.content.res.AssetManager;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+import com.litongjava.whisper.android.java.bean.WhisperSegment;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class WhisperContext {
+
+ private static final String LOG_TAG = "LibWhisper";
+ private long ptr;
+ private final ExecutorService executorService;
+
+ private WhisperContext(long ptr) {
+ this.ptr = ptr;
+ this.executorService = Executors.newSingleThreadExecutor();
+ }
+
+ public String transcribeData(float[] data) throws ExecutionException, InterruptedException {
+ return executorService.submit(new Callable<String>() {
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ @Override
+ public String call() throws Exception {
+ if (ptr == 0L) {
+ throw new IllegalStateException();
+ }
+ int numThreads = WhisperCpuConfig.getPreferredThreadCount();
+ Log.d(LOG_TAG, "Selecting " + numThreads + " threads");
+
+ StringBuilder result = new StringBuilder();
+ synchronized (this) {
+
+ WhisperLib.fullTranscribe(ptr, numThreads, data);
+ int textCount = WhisperLib.getTextSegmentCount(ptr);
+ for (int i = 0; i < textCount; i++) {
+ String sentence = WhisperLib.getTextSegment(ptr, i);
+ result.append(sentence);
+ }
+ }
+ return result.toString();
+ }
+ }).get();
+ }
+
+ public List<WhisperSegment> transcribeDataWithTime(float[] data) throws ExecutionException, InterruptedException {
+ return executorService.submit(new Callable<List<WhisperSegment>>() {
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ @Override
+ public List<WhisperSegment> call() throws Exception {
+ if (ptr == 0L) {
+ throw new IllegalStateException();
+ }
+ int numThreads = WhisperCpuConfig.getPreferredThreadCount();
+ Log.d(LOG_TAG, "Selecting " + numThreads + " threads");
+
+ List<WhisperSegment> segments = new ArrayList<>();
+ synchronized (this) {
+// StringBuilder result = new StringBuilder();
+ WhisperLib.fullTranscribe(ptr, numThreads, data);
+ int textCount = WhisperLib.getTextSegmentCount(ptr);
+ for (int i = 0; i < textCount; i++) {
+ long start = WhisperLib.getTextSegmentT0(ptr, i);
+ String sentence = WhisperLib.getTextSegment(ptr, i);
+ long end = WhisperLib.getTextSegmentT1(ptr, i);
+// result.append();
+ segments.add(new WhisperSegment(start, end, sentence));
+
+ }
+// return result.toString();
+ }
+ return segments;
+ }
+ }).get();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public String benchMemory(int nthreads) throws ExecutionException, InterruptedException {
+ return executorService.submit(() -> WhisperLib.benchMemcpy(nthreads)).get();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public String benchGgmlMulMat(int nthreads) throws ExecutionException, InterruptedException {
+ return executorService.submit(() -> WhisperLib.benchGgmlMulMat(nthreads)).get();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public void release() throws ExecutionException, InterruptedException {
+ executorService.submit(() -> {
+ if (ptr != 0L) {
+ WhisperLib.freeContext(ptr);
+ ptr = 0;
+ }
+ }).get();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public static WhisperContext createContextFromFile(String filePath) {
+ long ptr = WhisperLib.initContext(filePath);
+ if (ptr == 0L) {
+ throw new RuntimeException("Couldn't create context with path " + filePath);
+ }
+ return new WhisperContext(ptr);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public static WhisperContext createContextFromInputStream(InputStream stream) {
+ long ptr = WhisperLib.initContextFromInputStream(stream);
+ if (ptr == 0L) {
+ throw new RuntimeException("Couldn't create context from input stream");
+ }
+ return new WhisperContext(ptr);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public static WhisperContext createContextFromAsset(AssetManager assetManager, String assetPath) {
+ long ptr = WhisperLib.initContextFromAsset(assetManager, assetPath);
+ if (ptr == 0L) {
+ throw new RuntimeException("Couldn't create context from asset " + assetPath);
+ }
+ return new WhisperContext(ptr);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public static String getSystemInfo() {
+ return WhisperLib.getSystemInfo();
+ }
+}
--- /dev/null
+package com.whispercpp.java.whisper;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+public class WhisperCpuConfig {
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ public static int getPreferredThreadCount() {
+ return Math.max(CpuInfo.getHighPerfCpuCount(), 2);
+ }
+}
--- /dev/null
+package com.whispercpp.java.whisper;
+
+import android.content.res.AssetManager;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+import java.io.InputStream;
+
+@RequiresApi(api = Build.VERSION_CODES.O)
+public class WhisperLib {
+ private static final String LOG_TAG = "LibWhisper";
+
+ static {
+
+ Log.d(LOG_TAG, "Primary ABI: " + Build.SUPPORTED_ABIS[0]);
+ boolean loadVfpv4 = false;
+ boolean loadV8fp16 = false;
+ if (WhisperUtils.isArmEabiV7a()) {
+ String cpuInfo = WhisperUtils.cpuInfo();
+ if (cpuInfo != null) {
+ Log.d(LOG_TAG, "CPU info: " + cpuInfo);
+ if (cpuInfo.contains("vfpv4")) {
+ Log.d(LOG_TAG, "CPU supports vfpv4");
+ loadVfpv4 = true;
+ }
+ }
+ } else if (WhisperUtils.isArmEabiV8a()) {
+ String cpuInfo = WhisperUtils.cpuInfo();
+ if (cpuInfo != null) {
+ 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");
+ }
+ }
+
+ public static native long initContextFromInputStream(InputStream inputStream);
+
+ public static native long initContextFromAsset(AssetManager assetManager, String assetPath);
+
+ public static native long initContext(String modelPath);
+
+ public static native void freeContext(long contextPtr);
+
+ public static native void fullTranscribe(long contextPtr, int numThreads, float[] audioData);
+
+ public static native int getTextSegmentCount(long contextPtr);
+
+ public static native String getTextSegment(long contextPtr, int index);
+
+ public static native long getTextSegmentT0(long contextPtr, int index);
+
+ public static native long getTextSegmentT1(long contextPtr, int index);
+
+ public static native String getSystemInfo();
+
+ public static native String benchMemcpy(int nthread);
+
+ public static native String benchGgmlMulMat(int nthread);
+}
\ No newline at end of file
--- /dev/null
+package com.whispercpp.java.whisper;
+
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+import java.io.File;
+import java.nio.file.Path;
+
+public class WhisperUtils {
+ private static final String LOG_TAG = "LibWhisper";
+
+
+ public static boolean isArmEabiV7a() {
+ return Build.SUPPORTED_ABIS[0].equals("armeabi-v7a");
+ }
+
+ public static boolean isArmEabiV8a() {
+ return Build.SUPPORTED_ABIS[0].equals("arm64-v8a");
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public static String cpuInfo() {
+ try {
+ Path path = new File("/proc/cpuinfo").toPath();
+ return new String(java.nio.file.Files.readAllBytes(path));
+ } catch (Exception e) {
+ Log.w(LOG_TAG, "Couldn't read /proc/cpuinfo", e);
+ return null;
+ }
+
+ }
+}
\ No newline at end of file
--- /dev/null
+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})
--- /dev/null
+#include <jni.h>
+#include <android/asset_manager.h>
+#include <android/asset_manager_jni.h>
+#include <android/log.h>
+#include <stdlib.h>
+#include <sys/sysinfo.h>
+#include <string.h>
+#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_whispercpp_java_whisper_WhisperLib_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(&loader);
+}
+
+JNIEXPORT jlong JNICALL
+Java_com_whispercpp_java_whisper_WhisperLib_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_java_whisper_WhisperLib_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(model_path_chars);
+ (*env)->ReleaseStringUTFChars(env, model_path_str, model_path_chars);
+ return (jlong) context;
+}
+
+JNIEXPORT void JNICALL
+Java_com_whispercpp_java_whisper_WhisperLib_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_java_whisper_WhisperLib_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_java_whisper_WhisperLib_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_java_whisper_WhisperLib_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 jlong JNICALL
+Java_com_whispercpp_java_whisper_WhisperLib_getTextSegmentT0(JNIEnv *env, jobject thiz,jlong context_ptr, jint index) {
+ UNUSED(thiz);
+ struct whisper_context *context = (struct whisper_context *) context_ptr;
+ const int64_t t0 = whisper_full_get_segment_t0(context, index);
+ return (jlong)t0;
+}
+
+JNIEXPORT jlong JNICALL
+Java_com_whispercpp_java_whisper_WhisperLib_getTextSegmentT1(JNIEnv *env, jobject thiz,jlong context_ptr, jint index) {
+ UNUSED(thiz);
+ struct whisper_context *context = (struct whisper_context *) context_ptr;
+ const int64_t t1 = whisper_full_get_segment_t1(context, index);
+ return (jlong)t1;
+}
+
+JNIEXPORT jstring JNICALL
+Java_com_whispercpp_java_whisper_WhisperLib_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_java_whisper_WhisperLib_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_java_whisper_WhisperLib_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);
+}
+
--- /dev/null
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="85.84757"
+ android:endY="92.4963"
+ android:startX="42.9492"
+ android:startY="49.59793"
+ android:type="linear">
+ <item
+ android:color="#44000000"
+ android:offset="0.0" />
+ <item
+ android:color="#00000000"
+ android:offset="1.0" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+ android:strokeWidth="1"
+ android:strokeColor="#00000000" />
+</vector>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:fillColor="#3DDC84"
+ android:pathData="M0,0h108v108h-108z" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M9,0L9,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,0L19,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,0L29,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,0L39,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,0L49,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,0L59,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,0L69,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,0L79,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M89,0L89,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M99,0L99,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,9L108,9"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,19L108,19"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,29L108,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,39L108,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,49L108,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,59L108,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,69L108,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,79L108,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,89L108,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,99L108,99"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,29L89,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,39L89,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,49L89,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,59L89,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,69L89,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,79L89,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,19L29,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,19L39,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,19L49,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,19L59,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,19L69,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,19L79,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+</vector>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ tools:context=".MainActivity">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <Button
+ android:id="@+id/systemInfoBtn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="System Info" />
+
+ <Button
+ android:id="@+id/loadModelBtn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Load model" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <Button
+ android:id="@+id/transcriptSampleBtn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Transcribe sample" />
+
+ <Button
+ android:id="@+id/clearBtn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Clear" />
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/sample_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Hello World!"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ android:scrollbarAlwaysDrawHorizontalTrack="true"
+ android:maxLines="999"/>
+
+</LinearLayout>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
--- /dev/null
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <!-- Base application theme. -->
+ <style name="Theme.Whisperandroidjava" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+ <!-- Primary brand color. -->
+ <item name="colorPrimary">@color/purple_200</item>
+ <item name="colorPrimaryVariant">@color/purple_700</item>
+ <item name="colorOnPrimary">@color/black</item>
+ <!-- Secondary brand color. -->
+ <item name="colorSecondary">@color/teal_200</item>
+ <item name="colorSecondaryVariant">@color/teal_200</item>
+ <item name="colorOnSecondary">@color/black</item>
+ <!-- Status bar color. -->
+ <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
+ <!-- Customize your theme here. -->
+ </style>
+</resources>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="purple_200">#FFBB86FC</color>
+ <color name="purple_500">#FF6200EE</color>
+ <color name="purple_700">#FF3700B3</color>
+ <color name="teal_200">#FF03DAC5</color>
+ <color name="teal_700">#FF018786</color>
+ <color name="black">#FF000000</color>
+ <color name="white">#FFFFFFFF</color>
+</resources>
\ No newline at end of file
--- /dev/null
+<resources>
+ <string name="app_name">whisper.android.java</string>
+</resources>
\ No newline at end of file
--- /dev/null
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <!-- Base application theme. -->
+ <style name="Theme.Whisperandroidjava" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+ <!-- Primary brand color. -->
+ <item name="colorPrimary">@color/purple_500</item>
+ <item name="colorPrimaryVariant">@color/purple_700</item>
+ <item name="colorOnPrimary">@color/white</item>
+ <!-- Secondary brand color. -->
+ <item name="colorSecondary">@color/teal_200</item>
+ <item name="colorSecondaryVariant">@color/teal_700</item>
+ <item name="colorOnSecondary">@color/black</item>
+ <!-- Status bar color. -->
+ <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
+ <!-- Customize your theme here. -->
+ </style>
+</resources>
\ No newline at end of file
--- /dev/null
+package com.litongjava.whisper.android.java;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
--- /dev/null
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath "com.android.tools.build:gradle:4.1.3"
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
--- /dev/null
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
\ No newline at end of file
--- /dev/null
+#Fri Oct 20 11:07:15 HST 2023
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
--- /dev/null
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
--- /dev/null
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
--- /dev/null
+include ':app'
+rootProject.name = "whisper.android.java"
\ No newline at end of file
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
+ <option name="testRunner" value="GRADLE" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
+ <option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>