diff --git a/.github/workflows/android-demos.yml b/.github/workflows/android-demos.yml new file mode 100644 index 0000000..e66dd5a --- /dev/null +++ b/.github/workflows/android-demos.yml @@ -0,0 +1,39 @@ +name: Android Demos + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - '.github/workflows/android-demos.yml' + - 'demo/android/**' + - '!demo/android/Activity/README.md' + pull_request: + branches: [ main, 'v[0-9]+.[0-9]+' ] + paths: + - '.github/workflows/android-demos.yml' + - 'demo/android/**' + - '!demo/android/Activity/README.md' + +jobs: + build-activity-demo: + runs-on: ubuntu-latest + defaults: + run: + working-directory: demo/android/Activity + + steps: + - uses: actions/checkout@v3 + + - name: set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + + - name: Build binding + run: ./gradlew assembleRelease + working-directory: binding/android/PicoLLM + + - name: Build + run: ./gradlew assembleDebug diff --git a/.github/workflows/android-tests.yml b/.github/workflows/android-tests.yml new file mode 100644 index 0000000..e2ed1ce --- /dev/null +++ b/.github/workflows/android-tests.yml @@ -0,0 +1,52 @@ +name: Android Tests + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - '.github/workflows/android-tests.yml' + - 'binding/android/PicoLLM/**' + - '!binding/android/PicoLLM/README.md' + - 'binding/android/PicoLLMTestApp/**' + pull_request: + branches: [ main, 'v[0-9]+.[0-9]+'] + paths: + - '.github/workflows/android-tests.yml' + - 'binding/android/PicoLLM/**' + - '!binding/android/PicoLLM/README.md' + - 'binding/android/PicoLLMTestApp/**' + +defaults: + run: + working-directory: binding/android/PicoLLMTestApp + +jobs: + build: + name: Run Android Tests + runs-on: pv-android + + steps: + - uses: actions/checkout@v3 + + - name: Build binding + run: ./gradlew assembleRelease + working-directory: binding/android/PicoLLM + + - name: Download resource files + run: curl http://${{secrets.PV_CICD_RES_SERVER_AUTHORITY}}/github/picollm/res/phi2-290.bin/latest/phi2-290.bin -o phi2-290.bin + + - name: Copy resource files + run: ~/Android/Sdk/platform-tools/adb push --sync phi2-290.bin /sdcard/Android/data/ai.picovoice.picollm.testapp/files/external/test_resources/phi2-290.bin + + - name: Inject AccessKey + run: echo pvTestingAccessKey="${{secrets.PV_VALID_ACCESS_KEY}}" >> local.properties + + - name: Inject Android keystore variables + run: echo pvTestingModelName="phi2-290.bin" >> local.properties + + - name: Build androidTest + run: ./gradlew assembleDebugAndroidTest + + - name: Run tests + run: ./gradlew connectedAndroidTest --info diff --git a/.github/workflows/java-codestyle.yml b/.github/workflows/java-codestyle.yml new file mode 100644 index 0000000..7bf8f57 --- /dev/null +++ b/.github/workflows/java-codestyle.yml @@ -0,0 +1,28 @@ +name: Java CodeStyle + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - '**/*.java' + pull_request: + branches: [ main, 'v[0-9]+.[0-9]+' ] + paths: + - '**/*.java' + +jobs: + check-java-codestyle: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + + - name: Check Java CodeStyle + run: java -Dconfig_loc=resources/.lint/java/ -jar resources/.lint/java/checkstyle-10.5.0-all.jar -c resources/.lint/java/checkstyle.xml binding/android/ demo/android/ diff --git a/binding/android/PicoLLM/.gitignore b/binding/android/PicoLLM/.gitignore new file mode 100644 index 0000000..f70b309 --- /dev/null +++ b/binding/android/PicoLLM/.gitignore @@ -0,0 +1,12 @@ +*.iml +.gradle +/local.properties +/.idea/ +.DS_Store +/build +/captures +test_resources +.settings +.classpath +.project +publish-mavencentral.gradle \ No newline at end of file diff --git a/binding/android/PicoLLM/build.gradle b/binding/android/PicoLLM/build.gradle new file mode 100644 index 0000000..5c50934 --- /dev/null +++ b/binding/android/PicoLLM/build.gradle @@ -0,0 +1,25 @@ +ext { + defaultTargetSdkVersion = 31 +} + +buildscript { + repositories { + maven { url "https://plugins.gradle.org/m2/" } + google() + mavenCentral() + } + dependencies { + classpath "com.android.tools.build:gradle:4.2.2" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +tasks.register('clean', Delete) { + delete rootProject.buildDir +} diff --git a/binding/android/PicoLLM/gradle.properties b/binding/android/PicoLLM/gradle.properties new file mode 100644 index 0000000..6826e61 --- /dev/null +++ b/binding/android/PicoLLM/gradle.properties @@ -0,0 +1,17 @@ +# 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 \ No newline at end of file diff --git a/binding/android/PicoLLM/gradle/wrapper/gradle-wrapper.jar b/binding/android/PicoLLM/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/binding/android/PicoLLM/gradle/wrapper/gradle-wrapper.jar differ diff --git a/binding/android/PicoLLM/gradle/wrapper/gradle-wrapper.properties b/binding/android/PicoLLM/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ffed3a2 --- /dev/null +++ b/binding/android/PicoLLM/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/binding/android/PicoLLM/gradlew b/binding/android/PicoLLM/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/binding/android/PicoLLM/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${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='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | 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" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/binding/android/PicoLLM/gradlew.bat b/binding/android/PicoLLM/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/binding/android/PicoLLM/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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 execute + +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 execute + +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 + +: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 %* + +: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 diff --git a/binding/android/PicoLLM/picollm/.gitignore b/binding/android/PicoLLM/picollm/.gitignore new file mode 100644 index 0000000..d4681e4 --- /dev/null +++ b/binding/android/PicoLLM/picollm/.gitignore @@ -0,0 +1,3 @@ +/build +src/main/jniLibs/**/*.so +src/main/res/**/*.pv diff --git a/binding/android/PicoLLM/picollm/build.gradle b/binding/android/PicoLLM/picollm/build.gradle new file mode 100644 index 0000000..f93be49 --- /dev/null +++ b/binding/android/PicoLLM/picollm/build.gradle @@ -0,0 +1,47 @@ +apply plugin: 'com.android.library' + +ext { + PUBLISH_GROUP_ID = 'ai.picovoice' + PUBLISH_VERSION = '0.0.1' + PUBLISH_ARTIFACT_ID = 'picollm-android' +} + +android { + compileSdkVersion defaultTargetSdkVersion + buildToolsVersion "30.0.3" + + defaultConfig { + minSdkVersion 21 + targetSdkVersion defaultTargetSdkVersion + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +if (file("${rootDir}/publish-mavencentral.gradle").exists()) { + apply from: "${rootDir}/publish-mavencentral.gradle" +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) +} + +tasks.register('copyLibs', Copy) { + from("${rootDir}/../../../lib/android") + into("${rootDir}/picollm/src/main/jniLibs") +} + +preBuild.dependsOn copyLibs diff --git a/binding/android/PicoLLM/picollm/consumer-rules.pro b/binding/android/PicoLLM/picollm/consumer-rules.pro new file mode 100644 index 0000000..6cef974 --- /dev/null +++ b/binding/android/PicoLLM/picollm/consumer-rules.pro @@ -0,0 +1 @@ +-keep class ai.picovoice.mvm.*Exception { (...); } diff --git a/binding/android/PicoLLM/picollm/proguard-rules.pro b/binding/android/PicoLLM/picollm/proguard-rules.pro new file mode 100644 index 0000000..6cef974 --- /dev/null +++ b/binding/android/PicoLLM/picollm/proguard-rules.pro @@ -0,0 +1 @@ +-keep class ai.picovoice.mvm.*Exception { (...); } diff --git a/binding/android/PicoLLM/picollm/src/main/AndroidManifest.xml b/binding/android/PicoLLM/picollm/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9ebf782 --- /dev/null +++ b/binding/android/PicoLLM/picollm/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/PicoLLM.java b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/PicoLLM.java new file mode 100644 index 0000000..6da99c9 --- /dev/null +++ b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/PicoLLM.java @@ -0,0 +1,248 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm; + +import java.io.File; + +/** + * Android binding for PicoLLM. + */ +public class PicoLLM { + + static { + System.loadLibrary("pv_picollm"); + } + + private long handle; + + public static String getVersion() throws PicoLLMException { + return PicoLLMNative.getVersion(); + } + + public static int getMaxTopChoices() throws PicoLLMException { + return PicoLLMNative.getMaxTopChoices(); + } + + public static String[] listHardwareDevices() throws PicoLLMException { + return PicoLLMNative.listHardwareDevices(); + } + + private PicoLLM( + String accessKey, + String modelPath, + String device) throws PicoLLMException { + handle = PicoLLMNative.init(accessKey, modelPath, device); + } + + /** + * Function. + */ + public void delete() { + if (handle != 0) { + PicoLLMNative.delete(handle); + handle = 0; + } + } + + /** + * Function. + */ + public PicoLLMCompletion generate( + String prompt, + int completionTokenLimit, + String[] stopPhrases, + int seed, + float presencePenalty, + float frequencyPenalty, + float temperature, + float topP, + int numTopChoices, + PicoLLMStreamCallback streamCallback) throws PicoLLMException { + return PicoLLMNative.generate( + handle, + prompt, + completionTokenLimit, + stopPhrases, + seed, + presencePenalty, + frequencyPenalty, + temperature, + topP, + numTopChoices, + streamCallback); + } + + /** + * Function. + */ + public int[] tokenize( + String text, + boolean bos, + boolean eos) throws PicoLLMException { + return PicoLLMNative.tokenize( + handle, + text, + bos, + eos); + } + + /** + * Function. + */ + public float[] forward(int token) throws PicoLLMException { + return PicoLLMNative.forward(handle, token); + } + + /** + * Function. + */ + public void reset() throws PicoLLMException { + PicoLLMNative.reset(handle); + } + + /** + * Function. + */ + public String getModel() throws PicoLLMException { + return PicoLLMNative.getModel(handle); + } + + /** + * Function. + */ + public int getContextLength() throws PicoLLMException { + return PicoLLMNative.getContextLength(handle); + } + + /** + * Class. + */ + public static class Builder { + + private String accessKey = null; + private String modelPath = null; + private String device = "best:0"; + + public Builder setAccessKey(String accessKey) { + this.accessKey = accessKey; + return this; + } + + public Builder setModelPath(String modelPath) { + this.modelPath = modelPath; + return this; + } + + public Builder setDevice(String device) { + this.device = device; + return this; + } + + /** + * Function. + */ + public PicoLLM build() throws PicoLLMException { + if (accessKey == null || accessKey.equals("")) { + throw new PicoLLMInvalidArgumentException("No accessKey provided to PicoLLM."); + } + + if (modelPath == null || modelPath.equals("")) { + throw new PicoLLMInvalidArgumentException("No modelPath provided to PicoLLM."); + } + + if (device == null || device.equals("")) { + throw new PicoLLMInvalidArgumentException("No device provided to PicoLLM."); + } + + return new PicoLLM(accessKey, modelPath, device); + } + } + + /** + * Class. + */ + public static class GenerateBuilder { + + private int completionTokenLimit = -1; + private String[] stopPhrases = null; + private int seed = -1; + private float presencePenalty = 0; + private float frequencyPenalty = 0; + private float temperature = 0; + private float topP = 1; + private int numTopChoices = 0; + private PicoLLMStreamCallback streamCallback = null; + + public GenerateBuilder setCompletionTokenLimit(int completionTokenLimit) { + this.completionTokenLimit = completionTokenLimit; + return this; + } + + public GenerateBuilder setStopPhrases(String[] stopPhrases) { + this.stopPhrases = stopPhrases; + return this; + } + + public GenerateBuilder setSeed(int seed) { + this.seed = seed; + return this; + } + + public GenerateBuilder setPresencePenalty(float presencePenalty) { + this.presencePenalty = presencePenalty; + return this; + } + + public GenerateBuilder setFrequencyPenalty(float frequencyPenalty) { + this.frequencyPenalty = frequencyPenalty; + return this; + } + + public GenerateBuilder setTemperature(float temperature) { + this.temperature = temperature; + return this; + } + + public GenerateBuilder setTopP(float topP) { + this.topP = topP; + return this; + } + + public GenerateBuilder setNumTopChoices(int numTopChoices) { + this.numTopChoices = numTopChoices; + return this; + } + + public GenerateBuilder setStreamCallback(PicoLLMStreamCallback streamCallback) { + this.streamCallback = streamCallback; + return this; + } + + /** + * Function. + */ + public PicoLLMCompletion generate(PicoLLM object, String prompt) throws PicoLLMException { + return object.generate( + prompt, + completionTokenLimit, + stopPhrases, + seed, + presencePenalty, + frequencyPenalty, + temperature, + topP, + numTopChoices, + streamCallback); + } + } +} diff --git a/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/PicoLLMCompletion.java b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/PicoLLMCompletion.java new file mode 100644 index 0000000..8988116 --- /dev/null +++ b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/PicoLLMCompletion.java @@ -0,0 +1,186 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm; + +/** + * Class. + */ +public class PicoLLMCompletion { + + private final Usage usage; + private final Endpoint endpoint; + private final CompletionToken[] completionTokens; + private final String completion; + + /** + * Constructor. + * + */ + public PicoLLMCompletion( + Usage usage, + Endpoint endpoint, + CompletionToken[] completionTokens, + String completion) { + this.usage = usage; + this.endpoint = endpoint; + this.completionTokens = completionTokens; + this.completion = completion; + } + + /** + * Getter for the inferred transcription. + * + * @return Inferred transcription. + */ + public Usage getUsage() { + return usage; + } + + /** + * Getter for the inferred transcription. + * + * @return Inferred transcription. + */ + public Endpoint getEndpoint() { + return endpoint; + } + + /** + * Getter for transcribed words and their associated metadata. + * + * @return Transcribed words and their associated metadata. + */ + public CompletionToken[] getCompletionTokens() { + return completionTokens; + } + + /** + * Getter for the inferred transcription. + * + * @return Inferred transcription. + */ + public String getCompletion() { + return completion; + } + + /** + * Class. + */ + public static class Usage { + private final int promptTokens; + private final int completionTokens; + + /** + * Constructor. + * + */ + public Usage(int promptTokens, int completionTokens) { + this.promptTokens = promptTokens; + this.completionTokens = completionTokens; + } + + /** + * Getter for the transcribed word. + * + * @return Transcribed word. + */ + public int getPromptTokens() { + return promptTokens; + } + + /** + * Getter for the transcription confidence. + * + * @return Transcription confidence. It is a number within [0, 1]. + */ + public int getCompletionTokens() { + return completionTokens; + } + } + + enum Endpoint { + END_OF_SENTENCE, + COMPLETION_TOKEN_LIMIT_REACHED, + STOP_PHRASE_ENCOUNTERED + } + + /** + * Class. + */ + public static class Token { + private final String token; + private final float logProb; + + /** + * Constructor. + * + */ + public Token(String token, float logProb) { + this.token = token; + this.logProb = logProb; + } + + /** + * Getter for the transcribed word. + * + * @return Transcribed word. + */ + public String getToken() { + return token; + } + + /** + * Getter for the transcription confidence. + * + * @return Transcription confidence. It is a number within [0, 1]. + */ + public float getLogProb() { + return logProb; + } + } + + /** + * Class. + */ + public static class CompletionToken { + private final Token token; + private final Token[] topChoices; + + /** + * Constructor. + * + */ + public CompletionToken(Token token, Token[] topChoices) { + this.token = token; + this.topChoices = topChoices; + } + + /** + * Getter for the transcribed word. + * + * @return Transcribed word. + */ + public Token getToken() { + return token; + } + + /** + * Getter for the transcription confidence. + * + * @return Transcription confidence. It is a number within [0, 1]. + */ + public Token[] getTopChoices() { + return topChoices; + } + } +} diff --git a/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/PicoLLMNative.java b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/PicoLLMNative.java new file mode 100644 index 0000000..e190341 --- /dev/null +++ b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/PicoLLMNative.java @@ -0,0 +1,58 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm; + +class PicoLLMNative { + + static native String getVersion(); + + static native int getMaxTopChoices(); + + static native String[] listHardwareDevices(); + + static native void setSdk(String sdk); + + static native long init( + String accessKey, + String modelPath, + String device) throws PicoLLMException; + + static native void delete(long object); + + static native PicoLLMCompletion generate( + long object, + String prompt, + int completionTokenLimit, + String[] stopPhrases, + int seed, + float presencePenalty, + float frequencyPenalty, + float temperature, + float topP, + int numTopChoices, + PicoLLMStreamCallback streamCallback) throws PicoLLMException; + + static native int[] tokenize( + long object, + String text, + boolean bos, + boolean eos) throws PicoLLMException; + + static native float[] forward(long object, int token) throws PicoLLMException; + + static native void reset(long object) throws PicoLLMException; + + static native String getModel(long object) throws PicoLLMException; + + static native int getContextLength(long object) throws PicoLLMException; +} diff --git a/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/PicoLLMStreamCallback.java b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/PicoLLMStreamCallback.java new file mode 100644 index 0000000..e8b0693 --- /dev/null +++ b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/PicoLLMStreamCallback.java @@ -0,0 +1,17 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm; + +public interface PicoLLMStreamCallback { + void callback(String completion); +} diff --git a/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMActivationException.java b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMActivationException.java new file mode 100644 index 0000000..04276ce --- /dev/null +++ b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMActivationException.java @@ -0,0 +1,27 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm; + +public class PicoLLMActivationException extends PicoLLMException { + public PicoLLMActivationException(Throwable cause) { + super(cause); + } + + public PicoLLMActivationException(String message) { + super(message); + } + + public PicoLLMActivationException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMActivationLimitException.java b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMActivationLimitException.java new file mode 100644 index 0000000..76de79d --- /dev/null +++ b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMActivationLimitException.java @@ -0,0 +1,27 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm; + +public class PicoLLMActivationLimitException extends PicoLLMException { + public PicoLLMActivationLimitException(Throwable cause) { + super(cause); + } + + public PicoLLMActivationLimitException(String message) { + super(message); + } + + public PicoLLMActivationLimitException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMActivationRefusedException.java b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMActivationRefusedException.java new file mode 100644 index 0000000..4a2aa29 --- /dev/null +++ b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMActivationRefusedException.java @@ -0,0 +1,27 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm; + +public class PicoLLMActivationRefusedException extends PicoLLMException { + public PicoLLMActivationRefusedException(Throwable cause) { + super(cause); + } + + public PicoLLMActivationRefusedException(String message) { + super(message); + } + + public PicoLLMActivationRefusedException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMActivationThrottledException.java b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMActivationThrottledException.java new file mode 100644 index 0000000..26c2f60 --- /dev/null +++ b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMActivationThrottledException.java @@ -0,0 +1,27 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm; + +public class PicoLLMActivationThrottledException extends PicoLLMException { + public PicoLLMActivationThrottledException(Throwable cause) { + super(cause); + } + + public PicoLLMActivationThrottledException(String message) { + super(message); + } + + public PicoLLMActivationThrottledException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMException.java b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMException.java new file mode 100644 index 0000000..6b0dcc9 --- /dev/null +++ b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMException.java @@ -0,0 +1,54 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm; + +public class PicoLLMException extends Exception { + private final String message; + private final String[] messageStack; + + public PicoLLMException(Throwable cause) { + super(cause); + this.message = cause.getMessage(); + this.messageStack = null; + } + + public PicoLLMException(String message) { + super(message); + this.message = message; + this.messageStack = null; + } + + public PicoLLMException(String message, String[] messageStack) { + super(message); + this.message = message; + this.messageStack = messageStack; + } + + public String[] getMessageStack() { + return this.messageStack; + } + + @Override + public String getMessage() { + StringBuilder sb = new StringBuilder(message); + if (messageStack != null) { + if (messageStack.length > 0) { + sb.append(":"); + for (int i = 0; i < messageStack.length; i++) { + sb.append(String.format("\n [%d] %s", i, messageStack[i])); + } + } + } + return sb.toString(); + } +} diff --git a/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMIOException.java b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMIOException.java new file mode 100644 index 0000000..6d19171 --- /dev/null +++ b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMIOException.java @@ -0,0 +1,27 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm; + +public class PicoLLMIOException extends PicoLLMException { + public PicoLLMIOException(Throwable cause) { + super(cause); + } + + public PicoLLMIOException(String message) { + super(message); + } + + public PicoLLMIOException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMInvalidArgumentException.java b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMInvalidArgumentException.java new file mode 100644 index 0000000..26e4900 --- /dev/null +++ b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMInvalidArgumentException.java @@ -0,0 +1,27 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm; + +public class PicoLLMInvalidArgumentException extends PicoLLMException { + public PicoLLMInvalidArgumentException(Throwable cause) { + super(cause); + } + + public PicoLLMInvalidArgumentException(String message) { + super(message); + } + + public PicoLLMInvalidArgumentException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMInvalidStateException.java b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMInvalidStateException.java new file mode 100644 index 0000000..95eff7a --- /dev/null +++ b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMInvalidStateException.java @@ -0,0 +1,27 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm; + +public class PicoLLMInvalidStateException extends PicoLLMException { + public PicoLLMInvalidStateException(Throwable cause) { + super(cause); + } + + public PicoLLMInvalidStateException(String message) { + super(message); + } + + public PicoLLMInvalidStateException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMKeyException.java b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMKeyException.java new file mode 100644 index 0000000..2d25eef --- /dev/null +++ b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMKeyException.java @@ -0,0 +1,27 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm; + +public class PicoLLMKeyException extends PicoLLMException { + public PicoLLMKeyException(Throwable cause) { + super(cause); + } + + public PicoLLMKeyException(String message) { + super(message); + } + + public PicoLLMKeyException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMMemoryException.java b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMMemoryException.java new file mode 100644 index 0000000..5d20a1f --- /dev/null +++ b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMMemoryException.java @@ -0,0 +1,27 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm; + +public class PicoLLMMemoryException extends PicoLLMException { + public PicoLLMMemoryException(Throwable cause) { + super(cause); + } + + public PicoLLMMemoryException(String message) { + super(message); + } + + public PicoLLMMemoryException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMRuntimeException.java b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMRuntimeException.java new file mode 100644 index 0000000..1c7cfe9 --- /dev/null +++ b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMRuntimeException.java @@ -0,0 +1,27 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm; + +public class PicoLLMRuntimeException extends PicoLLMException { + public PicoLLMRuntimeException(Throwable cause) { + super(cause); + } + + public PicoLLMRuntimeException(String message) { + super(message); + } + + public PicoLLMRuntimeException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMStopIterationException.java b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMStopIterationException.java new file mode 100644 index 0000000..a2aa88b --- /dev/null +++ b/binding/android/PicoLLM/picollm/src/main/java/ai/picovoice/picollm/exception/PicoLLMStopIterationException.java @@ -0,0 +1,27 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm; + +public class PicoLLMStopIterationException extends PicoLLMException { + public PicoLLMStopIterationException(Throwable cause) { + super(cause); + } + + public PicoLLMStopIterationException(String message) { + super(message); + } + + public PicoLLMStopIterationException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/PicoLLM/settings.gradle b/binding/android/PicoLLM/settings.gradle new file mode 100644 index 0000000..8376943 --- /dev/null +++ b/binding/android/PicoLLM/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = "PicoLLM" +include ':picollm' diff --git a/binding/android/PicoLLMTestApp/.gitignore b/binding/android/PicoLLMTestApp/.gitignore new file mode 100644 index 0000000..f208563 --- /dev/null +++ b/binding/android/PicoLLMTestApp/.gitignore @@ -0,0 +1,12 @@ +*.iml +.gradle +/local.properties +/.idea/ +.DS_Store +/build +/captures +.externalNativeBuild +release +test_resources +*.bin +*.jks diff --git a/binding/android/PicoLLMTestApp/build.gradle b/binding/android/PicoLLMTestApp/build.gradle new file mode 100644 index 0000000..d1001bd --- /dev/null +++ b/binding/android/PicoLLMTestApp/build.gradle @@ -0,0 +1,24 @@ +ext { + defaultTargetSdkVersion = 33 +} + +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:7.2.2' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/binding/android/PicoLLMTestApp/gradle.properties b/binding/android/PicoLLMTestApp/gradle.properties new file mode 100644 index 0000000..c09e1e3 --- /dev/null +++ b/binding/android/PicoLLMTestApp/gradle.properties @@ -0,0 +1,17 @@ +# 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 diff --git a/binding/android/PicoLLMTestApp/gradle/wrapper/gradle-wrapper.jar b/binding/android/PicoLLMTestApp/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f6b961f Binary files /dev/null and b/binding/android/PicoLLMTestApp/gradle/wrapper/gradle-wrapper.jar differ diff --git a/binding/android/PicoLLMTestApp/gradle/wrapper/gradle-wrapper.properties b/binding/android/PicoLLMTestApp/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..740ab48 --- /dev/null +++ b/binding/android/PicoLLMTestApp/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Jun 29 23:02:09 PDT 2021 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/binding/android/PicoLLMTestApp/gradlew b/binding/android/PicoLLMTestApp/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/binding/android/PicoLLMTestApp/gradlew @@ -0,0 +1,172 @@ +#!/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" "$@" diff --git a/binding/android/PicoLLMTestApp/gradlew.bat b/binding/android/PicoLLMTestApp/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/binding/android/PicoLLMTestApp/gradlew.bat @@ -0,0 +1,84 @@ +@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 diff --git a/binding/android/PicoLLMTestApp/picollm-test-app/.gitignore b/binding/android/PicoLLMTestApp/picollm-test-app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/binding/android/PicoLLMTestApp/picollm-test-app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/binding/android/PicoLLMTestApp/picollm-test-app/build.gradle b/binding/android/PicoLLMTestApp/picollm-test-app/build.gradle new file mode 100644 index 0000000..0e185a5 --- /dev/null +++ b/binding/android/PicoLLMTestApp/picollm-test-app/build.gradle @@ -0,0 +1,167 @@ +apply plugin: 'com.android.application' + +Properties properties = new Properties() +if (rootProject.file("local.properties").exists()) { + properties.load(rootProject.file("local.properties").newDataInputStream()) + if (project.hasProperty("pvTestingAccessKey")) { + properties.put("pvTestingAccessKey", project.getProperty("pvTestingAccessKey")) + } + if (project.hasProperty("pvTestingModelName")) { + properties.put("pvTestingModelName", project.getProperty("pvTestingModelName")) + } + if (project.hasProperty("numTestIterations")) { + properties.put("numTestIterations", project.getProperty("numTestIterations")) + } + if (project.hasProperty("initPerformanceThresholdSec")) { + properties.put("initPerformanceThresholdSec", project.getProperty("initPerformanceThresholdSec")) + } + if (project.hasProperty("procPerformanceThresholdSec")) { + properties.put("procPerformanceThresholdSec", project.getProperty("procPerformanceThresholdSec")) + } + + if (project.hasProperty("storePassword")) { + properties.put("storePassword", project.getProperty("storePassword")) + } + if (project.hasProperty("storeFile")) { + properties.put("storeFile", project.getProperty("storeFile")) + } + if (project.hasProperty("keyAlias")) { + properties.put("keyAlias", project.getProperty("keyAlias")) + } + if (project.hasProperty("keyPassword")) { + properties.put("keyPassword", project.getProperty("keyPassword")) + } +} + +android { + compileSdkVersion defaultTargetSdkVersion + + defaultConfig { + applicationId "ai.picovoice.picollm.testapp" + minSdkVersion 21 + targetSdkVersion defaultTargetSdkVersion + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + resValue 'string', 'pvTestingAccessKey', properties.getProperty("pvTestingAccessKey", "") + resValue 'string', 'pvTestingModelName', properties.getProperty("pvTestingModelName", "") + resValue 'string', 'numTestIterations', properties.getProperty("numTestIterations", "") + resValue 'string', 'initPerformanceThresholdSec', properties.getProperty("initPerformanceThresholdSec", "") + resValue 'string', 'procPerformanceThresholdSec', properties.getProperty("procPerformanceThresholdSec", "") + } + + // signingConfigs { + // release { + // storePassword properties.getProperty("storePassword") + // storeFile file(properties.getProperty("storeFile", ".dummy.jks")) + // keyAlias properties.getProperty("keyAlias") + // keyPassword properties.getProperty("keyPassword") + // } + // } + + buildTypes { + debug { + // signingConfig signingConfigs.release + } + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + // signingConfig signingConfigs.release + } + } + + if (System.getProperty("testBuildType", "debug") == "integ") { + testBuildType("release") + } + + // def testDataFile = file('../../../../resources/.test/test_data.json') + // def parsedJson = new groovy.json.JsonSlurper().parseText(testDataFile.text) + // def languages = [] + // parsedJson.tests.parameters.each { a -> + // languages.add(a.language) + // } + + // flavorDimensions "language" + // productFlavors { + // en { + // getIsDefault().set(true) + // } + + // languages.each { language -> + // "$language" { + // applicationIdSuffix ".$language" + + // } + // } + + // all { flavor -> + // delete fileTree("$projectDir/src/main/assets") { + // exclude '**/.gitkeep' + // } + // String suffix = (flavor.name != "en") ? "_${flavor.name}" : "" + // task("${flavor.name}CopyParams", type: Copy) { + // from("$projectDir/../../../../lib/common/") + // include("picollm_params${suffix}.pv") + // into("$projectDir/src/main/assets/models") + // } + // task("${flavor.name}CopyAudio", type: Copy) { + // description = "Copy ${flavor.name} audio resources" + // from("$projectDir/../../../../resources/audio_samples/") + // include("test${suffix}.wav") + // into("$projectDir/src/main/assets/audio_samples") + // } + // } + // } + sourceSets { + androidTest { + java { + if (System.getProperty("testBuildType", "debug") == "perf") { + exclude "**/PicoLLMTest.java" + exclude "**/IntegrationTest.java" + } else if (System.getProperty("testBuildType", "debug") == "integ") { + exclude "**/PicoLLMTest.java" + exclude "**/PerformanceTest.java" + } else { + exclude "**/IntegrationTest.java" + exclude "**/PerformanceTest.java" + } + } + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + lint { + abortOnError false + } +} + +dependencies { + + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.8.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + // implementation 'ai.picovoice:picollm-android:0.0.1' + implementation files('../../../../binding/android/PicoLLM/picollm/build/outputs/aar/picollm-release.aar') + + // Espresso UI Testing + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation('androidx.test.espresso:espresso-core:3.2.0', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + androidTestImplementation('com.microsoft.appcenter:espresso-test-extension:1.4') + androidTestImplementation('androidx.test.espresso:espresso-intents:3.5.1') +} + +// afterEvaluate { +// android.productFlavors.all { +// flavor -> +// tasks."merge${flavor.name.capitalize()}DebugAssets".dependsOn "${flavor.name}CopyParams" +// tasks."merge${flavor.name.capitalize()}ReleaseAssets".dependsOn "${flavor.name}CopyParams" +// tasks."merge${flavor.name.capitalize()}DebugAssets".dependsOn "${flavor.name}CopyAudio" +// tasks."merge${flavor.name.capitalize()}ReleaseAssets".dependsOn "${flavor.name}CopyAudio" +// } +// } diff --git a/binding/android/PicoLLMTestApp/picollm-test-app/proguard-rules.pro b/binding/android/PicoLLMTestApp/picollm-test-app/proguard-rules.pro new file mode 100644 index 0000000..158caf3 --- /dev/null +++ b/binding/android/PicoLLMTestApp/picollm-test-app/proguard-rules.pro @@ -0,0 +1,23 @@ +# 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 +-keep class com.google.** { *; } +-keep class com.microsoft.** { *; } \ No newline at end of file diff --git a/demo/c/test/requirements.txt b/binding/android/PicoLLMTestApp/picollm-test-app/src/androidTest/assets/.gitkeep similarity index 100% rename from demo/c/test/requirements.txt rename to binding/android/PicoLLMTestApp/picollm-test-app/src/androidTest/assets/.gitkeep diff --git a/binding/android/PicoLLMTestApp/picollm-test-app/src/androidTest/java/ai/picovoice/picollm/testapp/BaseTest.java b/binding/android/PicoLLMTestApp/picollm-test-app/src/androidTest/java/ai/picovoice/picollm/testapp/BaseTest.java new file mode 100644 index 0000000..20ca37e --- /dev/null +++ b/binding/android/PicoLLMTestApp/picollm-test-app/src/androidTest/java/ai/picovoice/picollm/testapp/BaseTest.java @@ -0,0 +1,167 @@ +/* + Copyright 2022-2023 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm.testapp; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; + +import android.content.Context; +import android.content.res.AssetManager; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.microsoft.appcenter.espresso.Factory; +import com.microsoft.appcenter.espresso.ReportHelper; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +// import ai.picovoice.picollm.PicoLLMTranscript; + +public class BaseTest { + + @Rule + public ReportHelper reportHelper = Factory.getReportHelper(); + + Context testContext; + Context appContext; + AssetManager assetManager; + File externalFilesDir; + File filesDir; + + String testResourcesPath; + String defaultModelPath; + + String accessKey; + + @After + public void TearDown() { + reportHelper.label("Stopping App"); + } + + @Before + public void Setup() throws IOException { + testContext = InstrumentationRegistry.getInstrumentation().getContext(); + appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assetManager = testContext.getAssets(); + externalFilesDir = appContext.getExternalFilesDir("external"); + filesDir = appContext.getFilesDir(); + + extractExternalAssestsRecursively("test_resources"); + testResourcesPath = new File(appContext.getFilesDir(), "test_resources").getAbsolutePath(); + defaultModelPath = new File(testResourcesPath, appContext.getString(R.string.pvTestingModelName)) + .getAbsolutePath(); + + accessKey = appContext.getString(R.string.pvTestingAccessKey); + } + + public static String getTestDataString() throws IOException { + Context testContext = InstrumentationRegistry.getInstrumentation().getContext(); + AssetManager assetManager = testContext.getAssets(); + + InputStream is = new BufferedInputStream(assetManager.open("test_resources/test_data.json"), 256); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + + byte[] buffer = new byte[256]; + int bytesRead; + while ((bytesRead = is.read(buffer)) != -1) { + result.write(buffer, 0, bytesRead); + } + + return result.toString("UTF-8"); + } + + private void extractAssetsRecursively(String path) throws IOException { + String[] list = assetManager.list(path); + if (list.length > 0) { + File outputFile = new File(filesDir, path); + if (!outputFile.exists()) { + outputFile.mkdirs(); + } + + for (String file : list) { + String filepath = path + "/" + file; + extractAssetsRecursively(filepath); + } + } else { + extractTestFile(path); + } + } + + private void extractTestFile(String filepath) throws IOException { + InputStream is = new BufferedInputStream(assetManager.open(filepath), 256); + File absPath = new File(filesDir, filepath); + OutputStream os = new BufferedOutputStream(new FileOutputStream(absPath), 256); + int r; + while ((r = is.read()) != -1) { + os.write(r); + } + os.flush(); + + is.close(); + os.close(); + } + + private void extractExternalAssestsRecursively(String path) throws IOException { + File absPath = new File(externalFilesDir, path); + + if (absPath.isDirectory()) { + String[] list = absPath.list(); + + File outputFile = new File(filesDir, path); + if (!outputFile.exists()) { + outputFile.mkdirs(); + } + + for (String file : list) { + String filepath = path + "/" + file; + extractExternalAssestsRecursively(filepath); + } + } else if (absPath.isFile()) { + extractExternalTestFile(path); + } + } + + private void extractExternalTestFile(String filepath) throws IOException { + // Log.i("ZooDevTestRunner", String.format("Extracting external %s ...", filepath)); + File absInputPath = new File(externalFilesDir, filepath); + File absOutputPath = new File(filesDir, filepath); + InputStream is = new BufferedInputStream(new FileInputStream(absInputPath), 256); + OutputStream os = new BufferedOutputStream(new FileOutputStream(absOutputPath), 256); + + int numBytesRead; + byte[] buffer = new byte[256]; + while ((numBytesRead = is.read(buffer)) != -1) { + os.write(buffer, 0, numBytesRead); + } + + os.flush(); + is.close(); + os.close(); + } + +} diff --git a/binding/android/PicoLLMTestApp/picollm-test-app/src/androidTest/java/ai/picovoice/picollm/testapp/IntegrationTest.java b/binding/android/PicoLLMTestApp/picollm-test-app/src/androidTest/java/ai/picovoice/picollm/testapp/IntegrationTest.java new file mode 100644 index 0000000..3a86cc5 --- /dev/null +++ b/binding/android/PicoLLMTestApp/picollm-test-app/src/androidTest/java/ai/picovoice/picollm/testapp/IntegrationTest.java @@ -0,0 +1,108 @@ +package ai.picovoice.picollm.testapp; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom; +import static androidx.test.espresso.matcher.ViewMatchers.withId; + +import android.view.View; +import android.widget.TextView; + +import androidx.test.espresso.PerformException; +import androidx.test.espresso.UiController; +import androidx.test.espresso.ViewAction; +import androidx.test.espresso.intent.Intents; +import androidx.test.espresso.util.HumanReadables; +import androidx.test.ext.junit.rules.ActivityScenarioRule; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.microsoft.appcenter.espresso.Factory; +import com.microsoft.appcenter.espresso.ReportHelper; + +import org.hamcrest.Matcher; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.TimeoutException; + +class WaitForTextAction implements ViewAction { + private final String text; + private final long timeout; + + public WaitForTextAction(String text, long timeout) { + this.text = text; + this.timeout = timeout; + } + + @Override + public String getDescription() { + return String.format( + "Wait for '%d' milliseconds for the view to have text '%s'", + this.timeout, + this.text + ); + } + + @Override + public Matcher getConstraints() { + return isAssignableFrom(TextView.class); + } + + @Override + public void perform(UiController uiController, View view) { + long endTime = System.currentTimeMillis() + this.timeout; + + while (System.currentTimeMillis() < endTime) { + TextView textView = (TextView) view; + if (textView.getText().equals(this.text)) { + return; + } + uiController.loopMainThreadForAtLeast(50); + } + + throw new PerformException.Builder() + .withActionDescription(this.getDescription()) + .withCause(new TimeoutException(String.format("Waited for '%d' milliseconds", this.timeout))) + .withViewDescription(HumanReadables.describe(view)) + .build(); + } +} + +@RunWith(AndroidJUnit4.class) +public class IntegrationTest { + + @Rule + public ReportHelper reportHelper = Factory.getReportHelper(); + + @Rule + public ActivityScenarioRule activityScenarioRule = + new ActivityScenarioRule<>(MainActivity.class); + + @Before + public void intentsInit() { + Intents.init(); + } + + @After + public void intentsTeardown() { + Intents.release(); + } + + @After + public void TearDown() { + reportHelper.label("Stopping App"); + } + + @Test + public void testPorcupine() { + onView(withId(R.id.testButton)).perform(click()); + onView(withId(R.id.testResult)).perform(waitForText("Passed", 60000)); + } + + private ViewAction waitForText(String text, long timeout) { + return new WaitForTextAction(text, timeout); + } +} diff --git a/binding/android/PicoLLMTestApp/picollm-test-app/src/androidTest/java/ai/picovoice/picollm/testapp/PerformanceTest.java b/binding/android/PicoLLMTestApp/picollm-test-app/src/androidTest/java/ai/picovoice/picollm/testapp/PerformanceTest.java new file mode 100644 index 0000000..4de59c5 --- /dev/null +++ b/binding/android/PicoLLMTestApp/picollm-test-app/src/androidTest/java/ai/picovoice/picollm/testapp/PerformanceTest.java @@ -0,0 +1,109 @@ +/* + Copyright 2022 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm.testapp; + +import static org.junit.Assert.assertTrue; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.IOException; + +import ai.picovoice.picollm.PicoLLM; + +@RunWith(AndroidJUnit4.class) +public class PerformanceTest extends BaseTest { + + int numTestIterations = 30; + + @Before + public void Setup() throws IOException { + super.Setup(); + String iterationString = appContext.getString(R.string.numTestIterations); + + try { + numTestIterations = Integer.parseInt(iterationString); + } catch (NumberFormatException ignored) { } + } + + @Test + public void testInitPerformance() throws Exception { + String initThresholdString = appContext.getString(R.string.initPerformanceThresholdSec); + Assume.assumeNotNull(initThresholdString); + Assume.assumeFalse(initThresholdString.equals("")); + double initPerformanceThresholdSec = Double.parseDouble(initThresholdString); + + long totalNSec = 0; + for (int i = 0; i < numTestIterations + 1; i++) { + long before = System.nanoTime(); + PicoLLM picollm = new PicoLLM.Builder().setAccessKey(accessKey) + .setModelPath(defaultModelPath) + .build(appContext); + long after = System.nanoTime(); + + // throw away first run to account for cold start + if (i > 0) { + totalNSec += (after - before); + } + + picollm.delete(); + } + + double avgNSec = totalNSec / (double) numTestIterations; + double avgSec = ((double) Math.round(avgNSec * 1e-6)) / 1000.0; + assertTrue( + String.format("Expected threshold (%.3fs), init took (%.3fs)", initPerformanceThresholdSec, avgSec), + avgSec <= initPerformanceThresholdSec + ); + } + + @Test + public void testProcPerformance() throws Exception { + String procThresholdString = appContext.getString(R.string.procPerformanceThresholdSec); + Assume.assumeNotNull(procThresholdString); + Assume.assumeFalse(procThresholdString.equals("")); + + double procPerformanceThresholdSec = Double.parseDouble(procThresholdString); + + PicoLLM picollm = new PicoLLM.Builder().setAccessKey(accessKey) + .setModelPath(defaultModelPath) + .build(appContext); + + File audioFile = new File(testResourcesPath, "audio_samples/test.wav"); + + long totalNSec = 0; + for (int i = 0; i < numTestIterations + 1; i++) { + long before = System.nanoTime(); + picollm.processFile(audioFile.getAbsolutePath()); + long after = System.nanoTime(); + + // throw away first run to account for cold start + if (i > 0) { + totalNSec += (after - before); + } + } + picollm.delete(); + + double avgNSec = totalNSec / (double) numTestIterations; + double avgSec = ((double) Math.round(avgNSec * 1e-6)) / 1000.0; + assertTrue( + String.format("Expected threshold (%.3fs), process took (%.3fs)", procPerformanceThresholdSec, avgSec), + avgSec <= procPerformanceThresholdSec + ); + } +} diff --git a/binding/android/PicoLLMTestApp/picollm-test-app/src/androidTest/java/ai/picovoice/picollm/testapp/PicoLLMTest.java b/binding/android/PicoLLMTestApp/picollm-test-app/src/androidTest/java/ai/picovoice/picollm/testapp/PicoLLMTest.java new file mode 100644 index 0000000..c2760f0 --- /dev/null +++ b/binding/android/PicoLLMTestApp/picollm-test-app/src/androidTest/java/ai/picovoice/picollm/testapp/PicoLLMTest.java @@ -0,0 +1,85 @@ +/* + Copyright 2022-2023 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm.testapp; + +import static org.junit.Assert.*; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import ai.picovoice.picollm.PicoLLM; +import ai.picovoice.picollm.PicoLLMCompletion; +import ai.picovoice.picollm.PicoLLMException; + + +@RunWith(Enclosed.class) +public class PicoLLMTest { + + public static class StandardTests extends BaseTest { + + @Test + public void getVersion() throws PicoLLMException { + PicoLLM picollm = new PicoLLM.Builder() + .setAccessKey(accessKey) + .setModelPath(defaultModelPath) + .build(); + + assertTrue(picollm.getVersion() != null && !picollm.getVersion().equals("")); + + picollm.delete(); + } + + @Test + public void testInitFailWithInvalidAccessKey() { + boolean didFail = false; + try { + new PicoLLM.Builder() + .setAccessKey("") + .setModelPath(defaultModelPath) + .build(); + } catch (PicoLLMException e) { + didFail = true; + } + + assertTrue(didFail); + } + + @Test + public void generate() throws PicoLLMException { + PicoLLM picollm = new PicoLLM.Builder() + .setAccessKey(accessKey) + .setModelPath(defaultModelPath) + .build(); + + PicoLLMCompletion result = new PicoLLM.GenerateBuilder() + .setCompletionTokenLimit(10) + .generate(picollm, "Hello my name is"); + + assertTrue(result.getCompletion().equals(" John and I am a student at XYZ school")); + + picollm.delete(); + } + } +} diff --git a/binding/android/PicoLLMTestApp/picollm-test-app/src/main/AndroidManifest.xml b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bdc2902 --- /dev/null +++ b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/binding/android/PicoLLMTestApp/picollm-test-app/src/main/assets/.gitkeep b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/binding/android/PicoLLMTestApp/picollm-test-app/src/main/ic_launcher-playstore.png b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..0e7d9f6 Binary files /dev/null and b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/ic_launcher-playstore.png differ diff --git a/binding/android/PicoLLMTestApp/picollm-test-app/src/main/java/ai/picovoice/picollm/testapp/MainActivity.java b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/java/ai/picovoice/picollm/testapp/MainActivity.java new file mode 100644 index 0000000..e7fa86f --- /dev/null +++ b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/java/ai/picovoice/picollm/testapp/MainActivity.java @@ -0,0 +1,224 @@ +/* + Copyright 2022-2023 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is + located in the "LICENSE" file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + express or implied. See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ai.picovoice.picollm.testapp; + +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.ListView; +import android.widget.SimpleAdapter; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; + +import ai.picovoice.picollm.PicoLLM; +import ai.picovoice.picollm.PicoLLMException; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + } + + @Override + protected void onStop() { + super.onStop(); + } + + public void startTest(View view) { + Button testButton = findViewById(R.id.testButton); + testButton.setBackground(ContextCompat.getDrawable( + getApplicationContext(), + R.drawable.button_disabled)); + // runTest(); + + testButton.setBackground(ContextCompat.getDrawable( + getApplicationContext(), + R.drawable.button_background)); + } + + // public void runTest() { + // String accessKey = getApplicationContext().getString(R.string.pvTestingAccessKey); + + // ArrayList results = new ArrayList<>(); + + // String modelFile = getModelFile(); + + // TestResult result = new TestResult(); + // result.testName = "Test Init"; + // PicoLLM picollm = null; + // try { + // picollm = new PicoLLM.Builder() + // .setAccessKey(accessKey) + // .setModelPath(modelFile) + // .build(getApplicationContext()); + // result.success = true; + // } catch (PicoLLMException e) { + // result.success = false; + // result.errorMessage = String.format("Failed to init picollm with '%s'", e); + // } finally { + // results.add(result); + // } + + // result = new TestResult(); + // result.testName = "Test Process"; + // try { + // String suffix = "_" + BuildConfig.FLAVOR; + // if (BuildConfig.FLAVOR == "en") { + // suffix = ""; + // } + + // String audioPath = "audio_samples/test" + suffix + ".wav"; + + // PicoLLMTranscript processResult = processTestAudio(picollm, audioPath); + // if (processResult != null) { + // result.success = true; + // } else { + // result.success = false; + // result.errorMessage = "Process returned invalid result."; + // } + // } catch (Exception e) { + // result.success = false; + // result.errorMessage = String.format("Failed to process with '%s'", e); + // } finally { + // results.add(result); + // } + + // result = new TestResult(); + // result.testName = "Test Exception"; + // try { + // new PicoLLM.Builder() + // .setAccessKey("") + // .setModelPath(modelFile) + // .build(getApplicationContext()); + // result.success = false; + // result.errorMessage = "Init should have throw an exception"; + // } catch (PicoLLMException e) { + // result.success = true; + // } finally { + // results.add(result); + // } + + // displayTestResults(results); + // } + + // private void displayTestResults(ArrayList results) { + // ListView resultList = findViewById(R.id.resultList); + + // int passed = 0; + // int failed = 0; + + // ArrayList> list = new ArrayList<>(); + // for (TestResult result : results) { + // HashMap map = new HashMap<>(); + // map.put("testName", result.testName); + + // String message; + // if (result.success) { + // message = "Test Passed"; + // passed += 1; + // } else { + // message = String.format("Test Failed: %s", result.errorMessage); + // failed += 1; + // } + + // map.put("testMessage", message); + // list.add(map); + // } + + // SimpleAdapter adapter = new SimpleAdapter( + // getApplicationContext(), + // list, + // R.layout.list_view, + // new String[]{"testName", "testMessage"}, + // new int[]{R.id.testName, R.id.testMessage}); + + // resultList.setAdapter(adapter); + + // TextView passedView = findViewById(R.id.testNumPassed); + // TextView failedView = findViewById(R.id.testNumFailed); + + // passedView.setText(String.valueOf(passed)); + // failedView.setText(String.valueOf(failed)); + + // TextView resultView = findViewById(R.id.testResult); + // if (passed == 0 || failed > 0) { + // resultView.setText("Failed"); + // } else { + // resultView.setText("Passed"); + // } + // } + + // private String getModelFile() { + // String suffix = (!BuildConfig.FLAVOR.equals("en")) ? String.format("_%s", BuildConfig.FLAVOR) : ""; + // return String.format("models/picollm_params%s.pv", suffix); + // } + + // private PicoLLMTranscript processTestAudio(@NonNull PicoLLM l, String audioPath) throws Exception { + // File testAudio = new File(getApplicationContext().getFilesDir(), audioPath); + + // if (!testAudio.exists()) { + // testAudio.getParentFile().mkdirs(); + // extractFile(audioPath); + // } + + // FileInputStream audioInputStream = new FileInputStream(testAudio); + // ByteArrayOutputStream audioByteBuffer = new ByteArrayOutputStream(); + // byte[] buffer = new byte[1024]; + // for (int length; (length = audioInputStream.read(buffer)) != -1; ) { + // audioByteBuffer.write(buffer, 0, length); + // } + // byte[] rawData = audioByteBuffer.toByteArray(); + + // short[] pcm = new short[rawData.length / 2]; + // ByteBuffer pcmBuff = ByteBuffer.wrap(rawData).order(ByteOrder.LITTLE_ENDIAN); + // pcmBuff.asShortBuffer().get(pcm); + // pcm = Arrays.copyOfRange(pcm, 44, pcm.length); + + // return l.process(pcm); + // } + + // private void extractFile(String filepath) throws IOException { + // System.out.println(filepath); + // InputStream is = new BufferedInputStream(getAssets().open(filepath), 256); + // File absPath = new File(getApplicationContext().getFilesDir(), filepath); + // OutputStream os = new BufferedOutputStream(new FileOutputStream(absPath), 256); + // int r; + // while ((r = is.read()) != -1) { + // os.write(r); + // } + // os.flush(); + + // is.close(); + // os.close(); + // } +} diff --git a/binding/android/PicoLLMTestApp/picollm-test-app/src/main/java/ai/picovoice/picollm/testapp/TestResult.java b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/java/ai/picovoice/picollm/testapp/TestResult.java new file mode 100644 index 0000000..6176d9d --- /dev/null +++ b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/java/ai/picovoice/picollm/testapp/TestResult.java @@ -0,0 +1,8 @@ +package ai.picovoice.picollm.testapp; + +public class TestResult { + public String testName; + public boolean success; + public String errorMessage; +} + diff --git a/binding/android/PicoLLMTestApp/picollm-test-app/src/main/res/drawable/button_background.xml b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/res/drawable/button_background.xml new file mode 100644 index 0000000..7e73f1a --- /dev/null +++ b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/res/drawable/button_background.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/binding/android/PicoLLMTestApp/picollm-test-app/src/main/res/drawable/button_disabled.xml b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/res/drawable/button_disabled.xml new file mode 100644 index 0000000..ffe1c93 --- /dev/null +++ b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/res/drawable/button_disabled.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/binding/android/PicoLLMTestApp/picollm-test-app/src/main/res/drawable/ic_launcher_background.xml b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..196e181 --- /dev/null +++ b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/binding/android/PicoLLMTestApp/picollm-test-app/src/main/res/drawable/ic_launcher_foreground.xml b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..d2923ae --- /dev/null +++ b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/binding/android/PicoLLMTestApp/picollm-test-app/src/main/res/layout/activity_main.xml b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..ecd0d06 --- /dev/null +++ b/binding/android/PicoLLMTestApp/picollm-test-app/src/main/res/layout/activity_main.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +