From 001e68c226fd6f0483063bf486d205ee5ce69eca Mon Sep 17 00:00:00 2001 From: Albert Ho Date: Wed, 28 Feb 2024 13:11:05 -0800 Subject: [PATCH 01/13] init --- binding/android/Falcon/.gitignore | 12 + binding/android/Falcon/build.gradle | 25 + binding/android/Falcon/falcon/.gitignore | 3 + binding/android/Falcon/falcon/build.gradle | 45 ++ .../android/Falcon/falcon/consumer-rules.pro | 2 + .../android/Falcon/falcon/proguard-rules.pro | 2 + .../falcon/src/main/AndroidManifest.xml | 3 + .../src/main/java/ai/picovoice/Falcon.java | 236 ++++++++++ .../main/java/ai/picovoice/FalconNative.java | 37 ++ .../java/ai/picovoice/FalconSegments.java | 92 ++++ .../exception/FalconActivationException.java | 27 ++ .../FalconActivationLimitException.java | 27 ++ .../FalconActivationRefusedException.java | 27 ++ .../FalconActivationThrottledException.java | 27 ++ .../picovoice/exception/FalconException.java | 54 +++ .../exception/FalconIOException.java | 27 ++ .../FalconInvalidArgumentException.java | 27 ++ .../FalconInvalidStateException.java | 27 ++ .../exception/FalconKeyException.java | 27 ++ .../exception/FalconMemoryException.java | 27 ++ .../exception/FalconRuntimeException.java | 27 ++ .../FalconStopIterationException.java | 27 ++ binding/android/Falcon/gradle.properties | 17 + .../Falcon/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + binding/android/Falcon/gradlew | 172 +++++++ binding/android/Falcon/gradlew.bat | 84 ++++ binding/android/Falcon/settings.gradle | 2 + binding/android/FalconTestApp/.gitignore | 14 + binding/android/FalconTestApp/build.gradle | 24 + .../FalconTestApp/copy_test_resources.sh | 20 + .../FalconTestApp/falcon-test-app/.gitignore | 1 + .../falcon-test-app/build.gradle | 162 +++++++ .../falcon-test-app/proguard-rules.pro | 0 .../src/androidTest/assets/.gitkeep | 0 .../ai/picovoice/falcon/testapp/BaseTest.java | 182 ++++++++ .../picovoice/falcon/testapp/FalconTest.java | 426 ++++++++++++++++++ .../falcon/testapp/IntegrationTest.java | 108 +++++ .../falcon/testapp/PerformanceTest.java | 109 +++++ .../src/main/AndroidManifest.xml | 23 + .../falcon-test-app/src/main/assets/.gitkeep | 0 .../src/main/ic_launcher-playstore.png | Bin 0 -> 27403 bytes .../falcon/testapp/MainActivity.java | 225 +++++++++ .../picovoice/falcon/testapp/TestResult.java | 8 + .../main/res/drawable/button_background.xml | 5 + .../src/main/res/drawable/button_disabled.xml | 5 + .../res/drawable/ic_launcher_background.xml | 10 + .../res/drawable/ic_launcher_foreground.xml | 19 + .../src/main/res/layout/activity_main.xml | 131 ++++++ .../src/main/res/layout/list_view.xml | 24 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/values/colors.xml | 7 + .../res/values/ic_launcher_background.xml | 4 + .../src/main/res/values/strings.xml | 4 + .../src/main/res/values/styles.xml | 11 + .../android/FalconTestApp/gradle.properties | 17 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + binding/android/FalconTestApp/gradlew | 172 +++++++ binding/android/FalconTestApp/gradlew.bat | 84 ++++ binding/android/FalconTestApp/settings.gradle | 2 + binding/android/README.md | 94 ++++ 63 files changed, 2996 insertions(+) create mode 100644 binding/android/Falcon/.gitignore create mode 100644 binding/android/Falcon/build.gradle create mode 100644 binding/android/Falcon/falcon/.gitignore create mode 100644 binding/android/Falcon/falcon/build.gradle create mode 100644 binding/android/Falcon/falcon/consumer-rules.pro create mode 100644 binding/android/Falcon/falcon/proguard-rules.pro create mode 100644 binding/android/Falcon/falcon/src/main/AndroidManifest.xml create mode 100644 binding/android/Falcon/falcon/src/main/java/ai/picovoice/Falcon.java create mode 100644 binding/android/Falcon/falcon/src/main/java/ai/picovoice/FalconNative.java create mode 100644 binding/android/Falcon/falcon/src/main/java/ai/picovoice/FalconSegments.java create mode 100644 binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconActivationException.java create mode 100644 binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconActivationLimitException.java create mode 100644 binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconActivationRefusedException.java create mode 100644 binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconActivationThrottledException.java create mode 100644 binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconException.java create mode 100644 binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconIOException.java create mode 100644 binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconInvalidArgumentException.java create mode 100644 binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconInvalidStateException.java create mode 100644 binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconKeyException.java create mode 100644 binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconMemoryException.java create mode 100644 binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconRuntimeException.java create mode 100644 binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconStopIterationException.java create mode 100644 binding/android/Falcon/gradle.properties create mode 100644 binding/android/Falcon/gradle/wrapper/gradle-wrapper.jar create mode 100644 binding/android/Falcon/gradle/wrapper/gradle-wrapper.properties create mode 100755 binding/android/Falcon/gradlew create mode 100644 binding/android/Falcon/gradlew.bat create mode 100644 binding/android/Falcon/settings.gradle create mode 100644 binding/android/FalconTestApp/.gitignore create mode 100644 binding/android/FalconTestApp/build.gradle create mode 100644 binding/android/FalconTestApp/copy_test_resources.sh create mode 100644 binding/android/FalconTestApp/falcon-test-app/.gitignore create mode 100644 binding/android/FalconTestApp/falcon-test-app/build.gradle create mode 100644 binding/android/FalconTestApp/falcon-test-app/proguard-rules.pro create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/androidTest/assets/.gitkeep create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/androidTest/java/ai/picovoice/falcon/testapp/BaseTest.java create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/androidTest/java/ai/picovoice/falcon/testapp/FalconTest.java create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/androidTest/java/ai/picovoice/falcon/testapp/IntegrationTest.java create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/androidTest/java/ai/picovoice/falcon/testapp/PerformanceTest.java create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/main/AndroidManifest.xml create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/main/assets/.gitkeep create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/main/ic_launcher-playstore.png create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/main/java/ai/picovoice/falcon/testapp/MainActivity.java create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/main/java/ai/picovoice/falcon/testapp/TestResult.java create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/main/res/drawable/button_background.xml create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/main/res/drawable/button_disabled.xml create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/main/res/layout/activity_main.xml create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/main/res/layout/list_view.xml create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/main/res/values/colors.xml create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/main/res/values/ic_launcher_background.xml create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/main/res/values/strings.xml create mode 100644 binding/android/FalconTestApp/falcon-test-app/src/main/res/values/styles.xml create mode 100644 binding/android/FalconTestApp/gradle.properties create mode 100644 binding/android/FalconTestApp/gradle/wrapper/gradle-wrapper.jar create mode 100644 binding/android/FalconTestApp/gradle/wrapper/gradle-wrapper.properties create mode 100755 binding/android/FalconTestApp/gradlew create mode 100644 binding/android/FalconTestApp/gradlew.bat create mode 100644 binding/android/FalconTestApp/settings.gradle create mode 100644 binding/android/README.md diff --git a/binding/android/Falcon/.gitignore b/binding/android/Falcon/.gitignore new file mode 100644 index 0000000..d72d354 --- /dev/null +++ b/binding/android/Falcon/.gitignore @@ -0,0 +1,12 @@ +*.iml +.gradle +/local.properties +/.idea/ +.DS_Store +/build +/captures +test_resources +.settings +.classpath +.project +publish-mavencentral.gradle diff --git a/binding/android/Falcon/build.gradle b/binding/android/Falcon/build.gradle new file mode 100644 index 0000000..073fc63 --- /dev/null +++ b/binding/android/Falcon/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() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/binding/android/Falcon/falcon/.gitignore b/binding/android/Falcon/falcon/.gitignore new file mode 100644 index 0000000..f6e8c4c --- /dev/null +++ b/binding/android/Falcon/falcon/.gitignore @@ -0,0 +1,3 @@ +/build +src/main/jniLibs/** +src/main/res/** diff --git a/binding/android/Falcon/falcon/build.gradle b/binding/android/Falcon/falcon/build.gradle new file mode 100644 index 0000000..f17389a --- /dev/null +++ b/binding/android/Falcon/falcon/build.gradle @@ -0,0 +1,45 @@ +apply plugin: 'com.android.library' + +ext { + PUBLISH_GROUP_ID = 'ai.picovoice' + PUBLISH_VERSION = '1.0.0' + PUBLISH_ARTIFACT_ID = 'falcon-android' +} + +android { + compileSdkVersion defaultTargetSdkVersion + + defaultConfig { + minSdkVersion 21 + targetSdkVersion defaultTargetSdkVersion + versionCode 1 + versionName "1.0" + + consumerProguardFiles "consumer-rules.pro" + } + + 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 { +} + +task copyLibs(type: Copy) { + from("${rootDir}/../../../lib/android") + into("${rootDir}/falcon/src/main/jniLibs") +} + +preBuild.dependsOn copyLibs diff --git a/binding/android/Falcon/falcon/consumer-rules.pro b/binding/android/Falcon/falcon/consumer-rules.pro new file mode 100644 index 0000000..b010a39 --- /dev/null +++ b/binding/android/Falcon/falcon/consumer-rules.pro @@ -0,0 +1,2 @@ +-keep class ai.picovoice.falcon.*Exception { (...); } +-keep class ai.picovoice.falcon.FalconSegments { (...); } diff --git a/binding/android/Falcon/falcon/proguard-rules.pro b/binding/android/Falcon/falcon/proguard-rules.pro new file mode 100644 index 0000000..b010a39 --- /dev/null +++ b/binding/android/Falcon/falcon/proguard-rules.pro @@ -0,0 +1,2 @@ +-keep class ai.picovoice.falcon.*Exception { (...); } +-keep class ai.picovoice.falcon.FalconSegments { (...); } diff --git a/binding/android/Falcon/falcon/src/main/AndroidManifest.xml b/binding/android/Falcon/falcon/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4bf2ae3 --- /dev/null +++ b/binding/android/Falcon/falcon/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/binding/android/Falcon/falcon/src/main/java/ai/picovoice/Falcon.java b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/Falcon.java new file mode 100644 index 0000000..bc1f0c9 --- /dev/null +++ b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/Falcon.java @@ -0,0 +1,236 @@ +/* + 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.falcon; + +import android.content.Context; +import android.text.TextUtils; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Android binding for Falcon Speaker Diarization engine. + */ +public class Falcon { + + private static final String[] VALID_EXTENSIONS = { + "3gp", + "flac", + "m4a", + "mp3", + "mp4", + "ogg", + "opus", + "vorbis", + "wav", + "webm" + }; + + static { + System.loadLibrary("pv_falcon"); + } + + private long handle; + private static String _sdk = "android"; + + public static void setSdk(String sdk) { + Falcon._sdk = sdk; + } + + /** + * Constructor. + * + * @param accessKey AccessKey obtained from Picovoice Console + * @param modelPath Absolute path to the file containing Falcon model parameters. + * @throws FalconException if there is an error while initializing Falcon. + */ + private Falcon( + String accessKey, + String modelPath) throws FalconException { + FalconNative.setSdk(Falcon._sdk); + + handle = FalconNative.init( + accessKey, + modelPath); + } + + private static String extractResource( + Context context, + InputStream srcFileStream, + String dstFilename) throws IOException { + InputStream is = new BufferedInputStream( + srcFileStream, + 256); + OutputStream os = new BufferedOutputStream( + context.openFileOutput(dstFilename, Context.MODE_PRIVATE), + 256); + int r; + while ((r = is.read()) != -1) { + os.write(r); + } + os.flush(); + + is.close(); + os.close(); + return new File(context.getFilesDir(), dstFilename).getAbsolutePath(); + } + + /** + * Releases resources acquired by Falcon. + */ + public void delete() { + if (handle != 0) { + FalconNative.delete(handle); + handle = 0; + } + } + + /** + * Processes given audio data and returns diarized speaker segments. + * + * @param pcm A frame of audio samples. The incoming audio needs to have a sample rate + * equal to {@link #getSampleRate()} and be 16-bit linearly-encoded. Furthermore, + * Falcon operates on single channel audio. If you wish to process data in a different + * sample rate or format consider using `.process_file`. + * @return FalconSegments object which contains the diarization results of the engine. + * @throws FalconException if there is an error while processing the audio frame. + */ + public FalconSegments process(short[] pcm) throws FalconException { + if (handle == 0) { + throw new FalconInvalidStateException("Attempted to call Falcon process after delete."); + } + + if (pcm == null) { + throw new FalconInvalidArgumentException("Passed null frame to Falcon process."); + } + + return FalconNative.process(handle, pcm, pcm.length); + } + + /** + * Processes given audio data and returns diarized speaker segments. + * + * @param path Absolute path to the audio file. The supported formats are: + * `3gp (AMR)`, `FLAC`, `MP3`, `MP4/m4a (AAC)`, `Ogg`, `WAV` and `WebM`. + * @return FalconSegments object which contains the diarization results of the engine. + * @throws FalconException if there is an error while processing the audio frame. + */ + public FalconSegments processFile(String path) throws FalconException { + if (handle == 0) { + throw new FalconInvalidStateException("Attempted to call Falcon processFile after delete."); + } + + if (path == null || path.equals("")) { + throw new FalconInvalidArgumentException("Passed null path to Falcon processFile."); + } + + try { + return FalconNative.processFile(handle, path); + } catch (FalconInvalidArgumentException e) { + boolean endsWithValidExt = false; + for (String ext : VALID_EXTENSIONS) { + if (path.endsWith(ext)) { + endsWithValidExt = true; + break; + } + } + if (!endsWithValidExt) { + throw new FalconInvalidArgumentException( + String.format( + "Specified file '%s' does not have an accepted file extension. " + + "Valid extensions are: %s", + path, + TextUtils.join(", ", VALID_EXTENSIONS))); + } + throw e; + } + } + + /** + * Getter for required audio sample rate for PCM data. + * + * @return Required audio sample rate for PCM data. + */ + public int getSampleRate() { + return FalconNative.getSampleRate(); + } + + /** + * Getter for Falcon version. + * + * @return Falcon version. + */ + public String getVersion() { + return FalconNative.getVersion(); + } + + /** + * Builder for creating an instance of Falcon with a mixture of default arguments. + */ + public static class Builder { + + private String accessKey = null; + private String modelPath = null; + + /** + * Setter the AccessKey. + * + * @param accessKey AccessKey obtained from Picovoice Console + */ + public Builder setAccessKey(String accessKey) { + this.accessKey = accessKey; + return this; + } + + /** + * Setter for the absolute path to the file containing Falcon model parameters. + * + * @param modelPath Absolute path to the file containing Falcon model parameters. + */ + public Builder setModelPath(String modelPath) { + this.modelPath = modelPath; + return this; + } + + /** + * Creates an instance of Falcon Speaker Diarization engine. + */ + public Falcon build(Context context) throws FalconException { + if (accessKey == null || this.accessKey.equals("")) { + throw new FalconInvalidArgumentException("No AccessKey was provided to Falcon"); + } + + if (modelPath == null) { + throw new FalconInvalidArgumentException("ModelPath must not be null"); + } else { + File modelFile = new File(modelPath); + String modelFilename = modelFile.getName(); + if (!modelFile.exists() && !modelFilename.equals("")) { + try { + modelPath = extractResource(context, + context.getAssets().open(modelPath), + modelFilename); + } catch (IOException ex) { + throw new FalconIOException(ex); + } + } + } + + return new Falcon( + accessKey, + modelPath); + } + } +} diff --git a/binding/android/Falcon/falcon/src/main/java/ai/picovoice/FalconNative.java b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/FalconNative.java new file mode 100644 index 0000000..b96d39e --- /dev/null +++ b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/FalconNative.java @@ -0,0 +1,37 @@ +/* + 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.falcon; + +class FalconNative { + + static native String getVersion(); + + static native int getSampleRate(); + + static native void setSdk(String sdk); + + static native long init( + String accessKey, + String modelPath) throws FalconException; + + static native void delete(long object); + + static native FalconSegments process( + long object, + short[] pcm, + int numSamples) throws FalconException; + + static native FalconSegments processFile( + long object, + String path) throws FalconException; +} diff --git a/binding/android/Falcon/falcon/src/main/java/ai/picovoice/FalconSegments.java b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/FalconSegments.java new file mode 100644 index 0000000..6cabec5 --- /dev/null +++ b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/FalconSegments.java @@ -0,0 +1,92 @@ +/* + 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.falcon; + +/** + * FalconSegments Class. + */ +public class FalconSegments { + + private final Segment[] segmentArray; + + /** + * Constructor. + * + * @param segmentArray Diarized segments and their associated metadata. + */ + public FalconSegments(Segment[] segmentArray) { + this.segmentArray = segmentArray; + } + + /** + * Getter for diarized segments and their associated metadata. + * + * @return Diarized segments and their associated metadata. + */ + public Segment[] getSegmentArray() { + return segmentArray; + } + + /** + * FalconSegments.Segment class + */ + public static class Segment { + private final float startSec; + private final float endSec; + private final int speakerTag; + + /** + * Constructor. + * + * @param startSec Start of segment in seconds. + * @param endSec End of segment in seconds. + * @param speakerTag A non-negative integer that identifies unique speakers. + */ + public Segment( + float startSec, + float endSec, + int speakerTag + ) { + this.startSec = startSec; + this.endSec = endSec; + this.speakerTag = speakerTag; + } + + /** + * Getter for the start of segment in seconds. + * + * @return Start of segment in seconds. + */ + public float getStartSec() { + return startSec; + } + + /** + * Getter for the end of segment in seconds. + * + * @return End of segment in seconds. + */ + public float getEndSec() { + return endSec; + } + + /** + * Getter for the speaker tag. + * + * @return Speaker tag. + */ + public int getSpeakerTag() { + return speakerTag; + } + } +} diff --git a/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconActivationException.java b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconActivationException.java new file mode 100644 index 0000000..eb5fbe0 --- /dev/null +++ b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconActivationException.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.falcon; + +public class FalconActivationException extends FalconException { + public FalconActivationException(Throwable cause) { + super(cause); + } + + public FalconActivationException(String message) { + super(message); + } + + public FalconActivationException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconActivationLimitException.java b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconActivationLimitException.java new file mode 100644 index 0000000..9fc66f8 --- /dev/null +++ b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconActivationLimitException.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.falcon; + +public class FalconActivationLimitException extends FalconException { + public FalconActivationLimitException(Throwable cause) { + super(cause); + } + + public FalconActivationLimitException(String message) { + super(message); + } + + public FalconActivationLimitException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconActivationRefusedException.java b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconActivationRefusedException.java new file mode 100644 index 0000000..78ff234 --- /dev/null +++ b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconActivationRefusedException.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.falcon; + +public class FalconActivationRefusedException extends FalconException { + public FalconActivationRefusedException(Throwable cause) { + super(cause); + } + + public FalconActivationRefusedException(String message) { + super(message); + } + + public FalconActivationRefusedException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconActivationThrottledException.java b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconActivationThrottledException.java new file mode 100644 index 0000000..4b5e61b --- /dev/null +++ b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconActivationThrottledException.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.falcon; + +public class FalconActivationThrottledException extends FalconException { + public FalconActivationThrottledException(Throwable cause) { + super(cause); + } + + public FalconActivationThrottledException(String message) { + super(message); + } + + public FalconActivationThrottledException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconException.java b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconException.java new file mode 100644 index 0000000..d0b52f3 --- /dev/null +++ b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconException.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.falcon; + +public class FalconException extends Exception { + private final String message; + private final String[] messageStack; + + public FalconException(Throwable cause) { + super(cause); + this.message = cause.getMessage(); + this.messageStack = null; + } + + public FalconException(String message) { + super(message); + this.message = message; + this.messageStack = null; + } + + public FalconException(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/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconIOException.java b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconIOException.java new file mode 100644 index 0000000..79ab55f --- /dev/null +++ b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconIOException.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.falcon; + +public class FalconIOException extends FalconException { + public FalconIOException(Throwable cause) { + super(cause); + } + + public FalconIOException(String message) { + super(message); + } + + public FalconIOException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconInvalidArgumentException.java b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconInvalidArgumentException.java new file mode 100644 index 0000000..c1bbe4b --- /dev/null +++ b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconInvalidArgumentException.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.falcon; + +public class FalconInvalidArgumentException extends FalconException { + public FalconInvalidArgumentException(Throwable cause) { + super(cause); + } + + public FalconInvalidArgumentException(String message) { + super(message); + } + + public FalconInvalidArgumentException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconInvalidStateException.java b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconInvalidStateException.java new file mode 100644 index 0000000..29d5998 --- /dev/null +++ b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconInvalidStateException.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.falcon; + +public class FalconInvalidStateException extends FalconException { + public FalconInvalidStateException(Throwable cause) { + super(cause); + } + + public FalconInvalidStateException(String message) { + super(message); + } + + public FalconInvalidStateException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconKeyException.java b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconKeyException.java new file mode 100644 index 0000000..2612f66 --- /dev/null +++ b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconKeyException.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.falcon; + +public class FalconKeyException extends FalconException { + public FalconKeyException(Throwable cause) { + super(cause); + } + + public FalconKeyException(String message) { + super(message); + } + + public FalconKeyException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconMemoryException.java b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconMemoryException.java new file mode 100644 index 0000000..dbc2584 --- /dev/null +++ b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconMemoryException.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.falcon; + +public class FalconMemoryException extends FalconException { + public FalconMemoryException(Throwable cause) { + super(cause); + } + + public FalconMemoryException(String message) { + super(message); + } + + public FalconMemoryException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconRuntimeException.java b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconRuntimeException.java new file mode 100644 index 0000000..e331720 --- /dev/null +++ b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconRuntimeException.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.falcon; + +public class FalconRuntimeException extends FalconException { + public FalconRuntimeException(Throwable cause) { + super(cause); + } + + public FalconRuntimeException(String message) { + super(message); + } + + public FalconRuntimeException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconStopIterationException.java b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconStopIterationException.java new file mode 100644 index 0000000..18f9f57 --- /dev/null +++ b/binding/android/Falcon/falcon/src/main/java/ai/picovoice/exception/FalconStopIterationException.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.falcon; + +public class FalconStopIterationException extends FalconException { + public FalconStopIterationException(Throwable cause) { + super(cause); + } + + public FalconStopIterationException(String message) { + super(message); + } + + public FalconStopIterationException(String message, String[] messageStack) { + super(message, messageStack); + } +} diff --git a/binding/android/Falcon/gradle.properties b/binding/android/Falcon/gradle.properties new file mode 100644 index 0000000..c09e1e3 --- /dev/null +++ b/binding/android/Falcon/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/Falcon/gradle/wrapper/gradle-wrapper.jar b/binding/android/Falcon/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f6b961fd5a86aa5fbfe90f707c3138408be7c718 GIT binary patch literal 54329 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

<5G2M1WZP5 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 F{|7XC?P&l2 literal 0 HcmV?d00001 diff --git a/binding/android/Falcon/gradle/wrapper/gradle-wrapper.properties b/binding/android/Falcon/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8d85db5 --- /dev/null +++ b/binding/android/Falcon/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Aug 03 15:31:32 PDT 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/binding/android/Falcon/gradlew b/binding/android/Falcon/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/binding/android/Falcon/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/Falcon/gradlew.bat b/binding/android/Falcon/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/binding/android/Falcon/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/Falcon/settings.gradle b/binding/android/Falcon/settings.gradle new file mode 100644 index 0000000..3d97666 --- /dev/null +++ b/binding/android/Falcon/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = "Falcon" +include ':falcon' diff --git a/binding/android/FalconTestApp/.gitignore b/binding/android/FalconTestApp/.gitignore new file mode 100644 index 0000000..76b9157 --- /dev/null +++ b/binding/android/FalconTestApp/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/ +.DS_Store +/build +/captures +.externalNativeBuild +release +test_resources +*.pv +*.wav + +*.jks diff --git a/binding/android/FalconTestApp/build.gradle b/binding/android/FalconTestApp/build.gradle new file mode 100644 index 0000000..20f9f43 --- /dev/null +++ b/binding/android/FalconTestApp/build.gradle @@ -0,0 +1,24 @@ +ext { + defaultTargetSdkVersion = 33 +} + +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:7.4.2' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/binding/android/FalconTestApp/copy_test_resources.sh b/binding/android/FalconTestApp/copy_test_resources.sh new file mode 100644 index 0000000..97ac0c5 --- /dev/null +++ b/binding/android/FalconTestApp/copy_test_resources.sh @@ -0,0 +1,20 @@ +if [ ! -d "./falcon-test-app/src/androidTest/assets/test_resources/audio_samples" ] +then + echo "Creating test audio samples directory..." + mkdir -p ./falcon-test-app/src/androidTest/assets/test_resources/audio_samples +fi + +echo "Copying test audio samples..." +cp ../../../resources/audio_samples/*.wav ./falcon-test-app/src/androidTest/assets/test_resources/audio_samples + +if [ ! -d "./falcon-test-app/src/androidTest/assets/test_resources/model_files" ] +then + echo "Creating test model files directory..." + mkdir -p ./falcon-test-app/src/androidTest/assets/test_resources/model_files +fi + +echo "Copying falcon model files ..." +cp ../../../lib/common/*.pv ./falcon-test-app/src/androidTest/assets/test_resources/model_files + +echo "Copying test data file..." +cp ../../../resources/.test/test_data.json ./falcon-test-app/src/androidTest/assets/test_resources diff --git a/binding/android/FalconTestApp/falcon-test-app/.gitignore b/binding/android/FalconTestApp/falcon-test-app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/binding/android/FalconTestApp/falcon-test-app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/binding/android/FalconTestApp/falcon-test-app/build.gradle b/binding/android/FalconTestApp/falcon-test-app/build.gradle new file mode 100644 index 0000000..097b23a --- /dev/null +++ b/binding/android/FalconTestApp/falcon-test-app/build.gradle @@ -0,0 +1,162 @@ +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("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.falcon.testapp" + minSdkVersion 21 + targetSdkVersion defaultTargetSdkVersion + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + resValue 'string', 'pvTestingAccessKey', properties.getProperty("pvTestingAccessKey", "") + 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("falcon_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 "**/FalconTest.java" + exclude "**/IntegrationTest.java" + } else if (System.getProperty("testBuildType", "debug") == "integ") { + exclude "**/FalconTest.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 files('../../Falcon/falcon/build/outputs/aar/falcon-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/FalconTestApp/falcon-test-app/proguard-rules.pro b/binding/android/FalconTestApp/falcon-test-app/proguard-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/binding/android/FalconTestApp/falcon-test-app/src/androidTest/assets/.gitkeep b/binding/android/FalconTestApp/falcon-test-app/src/androidTest/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/binding/android/FalconTestApp/falcon-test-app/src/androidTest/java/ai/picovoice/falcon/testapp/BaseTest.java b/binding/android/FalconTestApp/falcon-test-app/src/androidTest/java/ai/picovoice/falcon/testapp/BaseTest.java new file mode 100644 index 0000000..25bf64f --- /dev/null +++ b/binding/android/FalconTestApp/falcon-test-app/src/androidTest/java/ai/picovoice/falcon/testapp/BaseTest.java @@ -0,0 +1,182 @@ +/* + 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.falcon.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.falcon.FalconSegments; + +public class BaseTest { + + @Rule + public ReportHelper reportHelper = Factory.getReportHelper(); + + Context testContext; + Context appContext; + AssetManager assetManager; + 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(); + extractAssetsRecursively("test_resources"); + testResourcesPath = new File(appContext.getFilesDir(), "test_resources").getAbsolutePath(); + defaultModelPath = new File(testResourcesPath, "model_files/falcon_params.pv").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"); + } + + protected static short[] readAudioFile(String audioFile) throws Exception { + FileInputStream audioInputStream = new FileInputStream(audioFile); + 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, 22, pcm.length); + + return pcm; + } + + protected void validateMetadata( + FalconSegments.Segment[] segments, + FalconSegments.Segment[] expectedSegments + ) { + assertEquals(segments.length, expectedSegments.length); + for (int i = 0; i < segments.length; i++) { + assertEquals(segments[i].getStartSec(), expectedSegments[i].getStartSec(), 0.01); + assertEquals(segments[i].getEndSec(), expectedSegments[i].getEndSec(), 0.01); + assertEquals(segments[i].getSpeakerTag(), expectedSegments[i].getSpeakerTag()); + } + } + + protected static float getSegmentErrorRate( + String transcript, + String expectedTranscript, + boolean useCER) { + String splitter = (useCER) ? "" : " "; + return (float) levenshteinDistance( + transcript.split(splitter), + expectedTranscript.split(splitter)) / transcript.length(); + } + + private static int levenshteinDistance(String[] segments1, String[] segments2) { + int[][] res = new int[segments1.length + 1][segments2.length + 1]; + for (int i = 0; i <= segments1.length; i++) { + res[i][0] = i; + } + for (int j = 0; j <= segments2.length; j++) { + res[0][j] = j; + } + for (int i = 1; i <= segments1.length; i++) { + for (int j = 1; j <= segments2.length; j++) { + res[i][j] = Math.min( + Math.min( + res[i - 1][j] + 1, + res[i][j - 1] + 1), + res[i - 1][j - 1] + (segments1[i - 1].equalsIgnoreCase(segments2[j - 1]) ? 0 : 1) + ); + } + } + return res[segments1.length][segments2.length]; + } + + private void extractAssetsRecursively(String path) throws IOException { + String[] list = assetManager.list(path); + if (list.length > 0) { + File outputFile = new File(appContext.getFilesDir(), 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(appContext.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/FalconTestApp/falcon-test-app/src/androidTest/java/ai/picovoice/falcon/testapp/FalconTest.java b/binding/android/FalconTestApp/falcon-test-app/src/androidTest/java/ai/picovoice/falcon/testapp/FalconTest.java new file mode 100644 index 0000000..6579bbd --- /dev/null +++ b/binding/android/FalconTestApp/falcon-test-app/src/androidTest/java/ai/picovoice/falcon/testapp/FalconTest.java @@ -0,0 +1,426 @@ +/* + 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.falcon.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.falcon.Falcon; +import ai.picovoice.falcon.FalconException; +//import ai.picovoice.falcon.FalconSegments; + + +@RunWith(Enclosed.class) +public class FalconTest { + + public static class StandardTests extends BaseTest { + + @Test + public void testInitFailWithInvalidAccessKey() { + boolean didFail = false; + try { + new Falcon.Builder() + .setAccessKey("") + .setModelPath(defaultModelPath) + .build(appContext); + } catch (FalconException e) { + didFail = true; + } + + assertTrue(didFail); + } + + @Test + public void testInitFailWithMissingAccessKey() { + boolean didFail = false; + try { + new Falcon.Builder() + .setModelPath(defaultModelPath) + .build(appContext); + } catch (FalconException e) { + didFail = true; + } + + assertTrue(didFail); + } + + @Test + public void testInitFailWithInvalidModelPath() { + boolean didFail = false; + File modelPath = new File(testResourcesPath, "bad_path/bad_path.pv"); + try { + new Falcon.Builder() + .setAccessKey(accessKey) + .setModelPath(modelPath.getAbsolutePath()) + .build(appContext); + } catch (FalconException e) { + didFail = true; + } + + assertTrue(didFail); + } + + @Test + public void testInitFailWithMissingModelPath() { + boolean didFail = false; + try { + new Falcon.Builder() + .setAccessKey(accessKey) + .build(appContext); + } catch (FalconException e) { + didFail = true; + } + + assertTrue(didFail); + } + + @Test + public void getVersion() throws FalconException { + Falcon falcon = new Falcon.Builder().setAccessKey(accessKey) + .setModelPath(defaultModelPath) + .build(appContext); + + assertTrue(falcon.getVersion() != null && !falcon.getVersion().equals("")); + + falcon.delete(); + } + + @Test + public void getSampleRate() throws FalconException { + Falcon falcon = new Falcon.Builder().setAccessKey(accessKey) + .setModelPath(defaultModelPath) + .build(appContext); + + assertTrue(falcon.getSampleRate() > 0); + + falcon.delete(); + } + + @Test + public void testErrorStack() { + String[] error = {}; + try { + new Falcon.Builder() + .setAccessKey("invalid") + .setModelPath(defaultModelPath) + .build(appContext); + } catch (FalconException e) { + error = e.getMessageStack(); + } + + assertTrue(0 < error.length); + assertTrue(error.length <= 8); + + try { + new Falcon.Builder() + .setAccessKey("invalid") + .setModelPath(defaultModelPath) + .build(appContext); + } catch (FalconException e) { + for (int i = 0; i < error.length; i++) { + assertEquals(e.getMessageStack()[i], error[i]); + } + } + } + } + +// @RunWith(Parameterized.class) +// public static class LanguageTests extends BaseTest { +// @Parameterized.Parameter(value = 0) +// public String language; +// +// @Parameterized.Parameter(value = 1) +// public String modelFile; +// +// @Parameterized.Parameter(value = 2) +// public String testAudioFile; +// +// @Parameterized.Parameter(value = 3) +// public String expectedTranscript; +// +// @Parameterized.Parameter(value = 4) +// public String expectedTranscriptWithPunctuation; +// +// @Parameterized.Parameter(value = 5) +// public float errorRate; +// +// @Parameterized.Parameter(value = 6) +// public FalconSegments.Word[] expectedWords; +// +// @Parameterized.Parameters(name = "{0}") +// public static Collection initParameters() throws IOException { +// String testDataJsonString = getTestDataString(); +// +// JsonParser parser = new JsonParser(); +// JsonObject testDataJson = parser.parse(testDataJsonString).getAsJsonObject(); +// JsonArray languageTests = testDataJson +// .getAsJsonObject("tests") +// .getAsJsonArray("language_tests"); +// +// List parameters = new ArrayList<>(); +// for (int i = 0; i < languageTests.size(); i++) { +// JsonObject testData = languageTests.get(i).getAsJsonObject(); +// +// String language = testData.get("language").getAsString(); +// String audioFile = testData.get("audio_file").getAsString(); +// String transcript = testData.get("transcript").getAsString(); +// String transcriptWithPunctuation = testData.get("transcript_with_punctuation").getAsString(); +// float errorRate = testData.get("error_rate").getAsFloat(); +// JsonArray words = testData.get("words").getAsJsonArray(); +// +// String modelFile; +// if (language.equals("en")) { +// modelFile = "model_files/falcon_params.pv"; +// } else { +// modelFile = String.format("model_files/falcon_params_%s.pv", language); +// } +// +// String testAudioFile = String.format("audio_samples/%s", audioFile); +// +// FalconSegments.Word[] paramWords = new FalconSegments.Word[words.size()]; +// for (int j = 0; j < words.size(); j++) { +// JsonObject wordObject = words.get(j).getAsJsonObject(); +// +// String word = wordObject.get("word").getAsString(); +// float confidence = wordObject.get("confidence").getAsFloat(); +// float startSec = wordObject.get("start_sec").getAsFloat(); +// float endSec = wordObject.get("end_sec").getAsFloat(); +// int speakerTag = wordObject.get("speaker_tag").getAsInt(); +// +// paramWords[j] = new FalconSegments( +// word, +// confidence, +// startSec, +// endSec, +// speakerTag +// ); +// } +// +// parameters.add(new Object[]{ +// language, +// modelFile, +// testAudioFile, +// transcript, +// transcriptWithPunctuation, +// errorRate, +// paramWords +// }); +// } +// +// return parameters; +// } +// +// +// @Test +// public void testTranscribeAudioFile() throws Exception { +// String modelPath = new File(testResourcesPath, modelFile).getAbsolutePath(); +// Falcon falcon = new Falcon.Builder() +// .setAccessKey(accessKey) +// .setModelPath(modelPath) +// .build(appContext); +// +// File audioFile = new File(testResourcesPath, testAudioFile); +// boolean useCER = language.equals("ja"); +// +// FalconSegments result = falcon.processFile(audioFile.getAbsolutePath()); +// +// assertTrue(getWordErrorRate(result.getTranscriptString(), expectedTranscript, useCER) < errorRate); +// validateMetadata( +// result.getWordArray(), +// expectedWords, +// false +// ); +// +// falcon.delete(); +// } +// +// @Test +// public void testTranscribeAudioFileWithPunctuation() throws Exception { +// String modelPath = new File(testResourcesPath, modelFile).getAbsolutePath(); +// Falcon falcon = new Falcon.Builder() +// .setAccessKey(accessKey) +// .setModelPath(modelPath) +// .setEnableAutomaticPunctuation(true) +// .build(appContext); +// +// File audioFile = new File(testResourcesPath, testAudioFile); +// boolean useCER = language.equals("ja"); +// +// FalconSegments result = falcon.processFile(audioFile.getAbsolutePath()); +// assertTrue(getWordErrorRate( +// result.getTranscriptString(), expectedTranscriptWithPunctuation, useCER) < errorRate); +// +// validateMetadata( +// result.getWordArray(), +// expectedWords, +// false +// ); +// +// falcon.delete(); +// } +// +// @Test +// public void testTranscribeAudioData() throws Exception { +// String modelPath = new File(testResourcesPath, modelFile).getAbsolutePath(); +// Falcon falcon = new Falcon.Builder() +// .setAccessKey(accessKey) +// .setModelPath(modelPath) +// .build(appContext); +// +// File audioFile = new File(testResourcesPath, testAudioFile); +// short[] pcm = readAudioFile(audioFile.getAbsolutePath()); +// +// FalconSegments result = falcon.process(pcm); +// boolean useCER = language.equals("ja"); +// +// assertTrue(getWordErrorRate(result.getTranscriptString(), expectedTranscript, useCER) < errorRate); +// validateMetadata( +// result.getWordArray(), +// expectedWords, +// false +// ); +// +// falcon.delete(); +// } +// +// @Test +// public void testTranscribeAudioDataWithDiarization() throws Exception { +// String modelPath = new File(testResourcesPath, modelFile).getAbsolutePath(); +// Falcon falcon = new Falcon.Builder() +// .setAccessKey(accessKey) +// .setModelPath(modelPath) +// .setEnableDiarization(true) +// .build(appContext); +// +// File audioFile = new File(testResourcesPath, testAudioFile); +// short[] pcm = readAudioFile(audioFile.getAbsolutePath()); +// +// FalconSegments result = falcon.process(pcm); +// boolean useCER = language.equals("ja"); +// +// assertTrue(getWordErrorRate(result.getTranscriptString(), expectedTranscript, useCER) < errorRate); +// validateMetadata( +// result.getWordArray(), +// expectedWords, +// true +// ); +// +// falcon.delete(); +// } +// } +// +// @RunWith(Parameterized.class) +// public static class DiarizationTests extends BaseTest { +// @Parameterized.Parameter(value = 0) +// public String language; +// +// @Parameterized.Parameter(value = 1) +// public String modelFile; +// +// @Parameterized.Parameter(value = 2) +// public String testAudioFile; +// +// @Parameterized.Parameter(value = 3) +// public FalconSegments.Word[] expectedWords; +// +// @Parameterized.Parameters(name = "{0}") +// public static Collection initParameters() throws IOException { +// String testDataJsonString = getTestDataString(); +// +// JsonParser parser = new JsonParser(); +// JsonObject testDataJson = parser.parse(testDataJsonString).getAsJsonObject(); +// JsonArray languageTests = testDataJson +// .getAsJsonObject("tests") +// .getAsJsonArray("diarization_tests"); +// +// List parameters = new ArrayList<>(); +// for (int i = 0; i < languageTests.size(); i++) { +// JsonObject testData = languageTests.get(i).getAsJsonObject(); +// +// String language = testData.get("language").getAsString(); +// String audioFile = testData.get("audio_file").getAsString(); +// JsonArray words = testData.get("words").getAsJsonArray(); +// +// String modelFile; +// if (language.equals("en")) { +// modelFile = "model_files/falcon_params.pv"; +// } else { +// modelFile = String.format("model_files/falcon_params_%s.pv", language); +// } +// +// String testAudioFile = String.format("audio_samples/%s", audioFile); +// +// FalconSegments.Word[] paramWords = new FalconSegments.Word[words.size()]; +// for (int j = 0; j < words.size(); j++) { +// JsonObject wordObject = words.get(j).getAsJsonObject(); +// +// String word = wordObject.get("word").getAsString(); +// int speakerTag = wordObject.get("speaker_tag").getAsInt(); +// +// paramWords[j] = new FalconSegments( +// word, +// 0.f, +// 0.f, +// 0.f, +// speakerTag +// ); +// } +// +// parameters.add(new Object[]{ +// language, +// modelFile, +// testAudioFile, +// paramWords +// }); +// } +// +// return parameters; +// } +// +// @Test +// public void testDiarizationMultipleSpeakers() throws Exception { +// String modelPath = new File(testResourcesPath, modelFile).getAbsolutePath(); +// Falcon falcon = new Falcon.Builder() +// .setAccessKey(accessKey) +// .setModelPath(modelPath) +// .setEnableDiarization(true) +// .build(appContext); +// +// File audioFile = new File(testResourcesPath, testAudioFile); +// short[] pcm = readAudioFile(audioFile.getAbsolutePath()); +// +// FalconSegments result = falcon.process(pcm); +// +// assertEquals(result.getWordArray().length, expectedWords.length); +// for (int i = 0; i < result.getWordArray().length; i++) { +// assertEquals(result.getWordArray()[i].getWord(), expectedWords[i].getWord()); +// assertEquals(result.getWordArray()[i].getSpeakerTag(), expectedWords[i].getSpeakerTag()); +// } +// falcon.delete(); +// } +// } +} diff --git a/binding/android/FalconTestApp/falcon-test-app/src/androidTest/java/ai/picovoice/falcon/testapp/IntegrationTest.java b/binding/android/FalconTestApp/falcon-test-app/src/androidTest/java/ai/picovoice/falcon/testapp/IntegrationTest.java new file mode 100644 index 0000000..332f346 --- /dev/null +++ b/binding/android/FalconTestApp/falcon-test-app/src/androidTest/java/ai/picovoice/falcon/testapp/IntegrationTest.java @@ -0,0 +1,108 @@ +package ai.picovoice.falcon.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/FalconTestApp/falcon-test-app/src/androidTest/java/ai/picovoice/falcon/testapp/PerformanceTest.java b/binding/android/FalconTestApp/falcon-test-app/src/androidTest/java/ai/picovoice/falcon/testapp/PerformanceTest.java new file mode 100644 index 0000000..d93a8f1 --- /dev/null +++ b/binding/android/FalconTestApp/falcon-test-app/src/androidTest/java/ai/picovoice/falcon/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.falcon.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.falcon.Falcon; + +@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(); + Falcon falcon = new Falcon.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); + } + + falcon.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); + + Falcon falcon = new Falcon.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(); + falcon.processFile(audioFile.getAbsolutePath()); + long after = System.nanoTime(); + + // throw away first run to account for cold start + if (i > 0) { + totalNSec += (after - before); + } + } + falcon.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/FalconTestApp/falcon-test-app/src/main/AndroidManifest.xml b/binding/android/FalconTestApp/falcon-test-app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e684f65 --- /dev/null +++ b/binding/android/FalconTestApp/falcon-test-app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/binding/android/FalconTestApp/falcon-test-app/src/main/assets/.gitkeep b/binding/android/FalconTestApp/falcon-test-app/src/main/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/binding/android/FalconTestApp/falcon-test-app/src/main/ic_launcher-playstore.png b/binding/android/FalconTestApp/falcon-test-app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..0e7d9f62a211b22188ef790a8ca3b60923dca1fc GIT binary patch literal 27403 zcmce;bzD?m_dj}O=x(GtL5|vmrx1ljoI3Vb7=+vwcp& z2c?_kJ_kApat*>dZeZ|E`x>o2?B#Wk^)i$x4?f%u>$M6C_26C8SkAiQ;vr906jyiV zZbk>$Du90)YcnNOY(IN1{gbV{qaM?fT@cc1)tfCRrGGczZftge6(;L7Y^y_BYq+hW z`hrASoyk6{M@>;m-M+7<*e8j!dn#cly)ifviuhkc={^Kqbd2p?4c^j`v5`sB5O;Xp zSxcnVU=YrJ*StDgYl+3VWu}={k+cf#loT#!8)=*Os8f105aSzv>|Pn}l(ZDnhrSSd z^na&D<=@ntKlaSI67o)>_M5qV@4Q2r+EVylj*kOJn@wn2{{+olQTE({j)W3wN^h>$ zPAyYead6joL)y`ySB=4&DzxCXYsY?^YRLx4NvSW8RrDN`EP)#v%1xYTtE8hCFgzWm zZwb1U=Hp9|L6VV5u%@}Hx0>QZTLw4YcbAt^w{=#tYEvCFEcEz)VZ{d^|Nj`MKL)`6 z|HCusCh-4#yrBo=r*pTG?rS)*w6MmZ>v+3nGKqIT^P=wwJwMT)Z+Y3bD4&h9+mVKz zTyw67TtleMxdd+QFaEZ*`&3Pt+p3Blr(a0E%($!x!OCJ5My{;SV7+i{EbJ%T@{0Vd z`Oof~cFS=5vqyg%H#LUgY8c1uxc`$;?`#04i!yB=@63b4x?%?MBPYHVvdmQZHzd_k zs4!LZ&Lax_9_1S9@#m*L-u$KQfA}?hB?2cmf&YElU?uXN397n_N z>G#ktJJN}ADmGt|b20o07!9vasiVaNOu!w9f&UqnB!~k`s>ya%#p4|$T;B|JQKi)q zT!_kzvL!poeVKW1JJ$7ykd^97&lQ$fvDO>pUP}r5sE58Mk7M2a>2>WRUc{;B$~hcv zwx^btLUvwTU;>W*{`v|=?4al1;gIA|iHdFXW3+XQV z3<6#^xip4(d20NKAiQ};+{RiISASG~%|qh*jw@I7md~Ecaf*T@TzGeQO>6Zp=E>HP z^qWS~k=5})n-|6ZOJE7P_P=4o97W7$&$BM7ggn#6k+qu)R?|PGXhQJx1anbQiTxS< zIsNZlk3<`4rPCTG^636ZFRp#a=Z}XX?#2Dv3LtGuE*PR&lCgPRCoF?8Aydy9CRq_; zTRbeH%f)`hBs+WU*pT*zR+ z;Ykf!!T;emG2+Z>EmmV314`##k?0Uty6hnL^XB6>5x!S;i`W`F{K24zyyxBAs^2;< zdQ_1r(G{vocn$j&i}PmA4x$>!_jUgMLHGVgNzrR*;Zv<9&2b4&m4X%avs@7vdT#M=^ z1&<^oH|bO?l_~$%WQ0MI{^>iBV$CE;FNl`M*SC!2MV;f9tXt9c_w6qr@olNC8-dYZi`kzvk?7#*#Whkr&dYsvK}toeq|r++hh1& zoAl5OI8@o-b(xc`@5q>WY_%0TKSDCze@qPT=#1sw*m$AaZ>5^?m+UvUq4}c4?<#Yf zH=Bvi0=fQ&fq^<7b5ES;XJ`C7j*QVZr$Pgz`sb_GnGf%xJ@s4#Le!3vy2)>EjU1ai zZ>x_?v)A|+#n)7$hbO{Y*pO?}4dx=!LP4ABAux-i>lPr_fr#YkP3YV*i@du~k5{W4SE^=~p-w<`?IbP|9ty>m88Vh{~ zVgKdnr&{1tzQs}@wD|DR(R+GLAn)NtoShMjb-Q1R#N06In5;s~g2DLT((=n12+SH=6b8lQpoEm^ zY(lKGOaHt6-yUo39D@sdE5R9*u!!XUdS45at*)6*%CLb=ixGFLgM+X741#E^DFKaI zEv$fKN2qzx-O9sPpzz;3`&0p}$BWLNNSbcJ@&+G?FFrnJGyF1a(BClsdb+idY5svU zR#0B&-vVSu1{uzQ?nanqE?1ARwu_s;7vpfaH%C;>q^tzR-#R#ifhOr+1NjxoacISO zECgg(7CU+l3LZb+kxFB#?{># zBX&-oc$yTLPFt;_%fhUr?gjeyM(uyj4N@pZJ|sztl6?9y&{TFWgnp>vo}%_4_5Ql! zrwn39G9RwZ09z;QC-YaBs4bRH;NL#|GFIh?{Sp39(l+{!ZwISHhh(^Nq^XI=Hj^Nh_k!oV0`@u{O>FT;KfLLxbWvyRx>XC4x4c-T`A6)* zNQZh3?nWd>XSt}uv?Qy@=|99o*ox!0AH?l$FikOY)AApQu`BlF>GZinQI$ANjeSS^)EA`-bIMZF_-fXYpyfHV; zB;HZHYYBUE1DHSNX{a~rpbC1qi-}GN`i3_<;U4#fhTK{IpQ!E5ZkZxM)l>P31%1N3 zvsA<{da(ukYsYm{05~*;2QjkjJqa!nX*;4(4o&(+Kk)5E|co)Tz6r zJi0|_biEKKa43lVa<|~ag7_|`Wrsidbm^X52lo=R8$V9yZdd9x9}wGkh!c&s^P^)_ zA)3{*s@$rCM9+?tKC2qt9}W>u_=nxLE1~ii#4mTkf@$)c^)Pa0G|JH5+DgN2uYd7< zHte{?eQj>+^dTI&p||QfGv-Our4}xTIQdj+bnOee&Ef|BVadDQH0)>IU`;*8Q8c&l zB_U@wX<4Lw`5}E*=Jp5RP9ty{?#bH5{4hZ=$2zG#WGwC*n5w;C7Vdie>gPpDV{nNi zx1Hkm@|Qh<-UEhG_99|E$&~gvn-Q4<)HA&IZW|mMxz+qyC)lq0<^6f%&>eqit7omx zO8(WoFC=9*Zhcz6J=wY^&hE4HV zLmKg$(Ug5X1TVIOy^aJz0|VvwnNKS8(!5%q*ezy?Z#Oxr;z+?~yeQXsIF{MYYeVl`9f{eKPln z@y+6^8_26O{(sV)uv-G$c?A&4o5$$8WTKZw4I8@7=Jg3bE7v}B&e3;vwV&MJ?x73q?}e`S{y-cY0<_{v7>vlG!$(6xw;IAoumIk4v;{~xPf5twf1@j;f+5xzG=XfUqot%y{||` z?^qYis9f8H3lhNVstIu&$wP@@&*`BO~*`T!!K3CPopXKrIx-Y zW&s2K*UI+L9Ps_tVQERK{+Bm=Jeu;Z@sjXn$Ef{-YjaV-4}5*ORrZL)vHxj!Klsqn zUS#W@_&??hsSHI0%E^UHc2M~}XBXv+?oV46M4MFSy&}90APL)mlvKDxjv{CDsSfVA zoDJ@^5y1tQ>tR$458T!#d$=?3IV>H4XjT z0M8KIcCl3sPJhY?TC548ci*0E`azFx=se=THai?>sFKUYRAmhJ9#qjvnQ%`mxJPMV zRpFQHzn*@ADg|7ffjQ)U7f*DGQXBw2x(U6uw$NKZ%hKi8d2!`6HQoBHIB&DXRQ^Rz zJ|4n+Ca|ig@`b5`2qSI!S8ti zb&F+EGH_)=Q9=Fhw%-Zp+?xij6TWx#03q|Qq~sIv<==ny%EmAcSHMm58?r0ajM$ee zR(U9&DhYrqA!qTFy3gECec{|Ty$ytpH+?WfoZW|9N;MNA4vC9nP55&$*YRP$CJ_4D zAlclEw2SVW6}aAK;wrS}oPPpY$@OYrL~ChGH~(;sLLYYw12WR(#df%H&0^j?^J@v~ z&z4N4Tdb9kN2`g>fpk=8X^iV$er#ZF_Yf$A6#(xb?gD|&x*i8UVb}wY!R$M;5805L z93jl|E`}Ko$-N#Q(fk!Ucq$q$6-{ZMjzpdsHq`G9yM^Aq-okj`aYd|{AA&5LkdtE1 z1Q%@qMQ_p8qN9J?VG6wrEZRMmuUW>{8FKU5t9eH6&E-+<-+>R|5TD@8WQ}ve+0n`s z)<5wUB3*;(J+`i2zFIh|FYGY&jD8wU#A#fTCIp>i1B7j3jh+NBqrF>Nf8sS0GfQOU zX@OdrKf*|G@^;SiTjJO-m2&Z)mpu7{yKZt_oryIO0F>M^vy zEfPfT%|L%xwAf>Z&AaWOHWn-r4keZPoh!nLPTe!TE*_U3jk~pq?L~TA^bmmF5EN2^ z1C)*;f%-ZtleBX0INnp`2zPZvx#T!CurG_fJ6*i>@pmYg5)BaiF#<|&0xjQ17k|oE z5yTAlf+Q_hn&h`q7_1+&2k=F}5)dh#%+>Cvx(#M^&TSkop-KV}j@>H|b@?0^iJwHd zuR*Q#a?3OATO$=JEfDE^a`du=JV297xoPPpFdlf499R7q0*^8 zYorZSl;Pw)s_2#Zp4eq)Ut}| zBEMV4wGG)=sDD7VK?@wWohQeZqX}6a8~dJGYCKUVY|QJ|vOA2ofTG^;1?I+m-OUt! zw>cz{hq~orz$Iaz3&}6JOtXVTT@iAMp?(mehKW#^;i>G81jUSG~*l^V3Z-WB^>bS9}D^ib4JTsq&xUg} zi$1u5TW!00TFW^FwZ1BlSBY!X7jbl?WLiF=TX|pXZnFhqNruBq7Oh#wN8BlG+bySY zBI|tVWBz`+y`Dd!Cu_p`0B`KFvH5GKzbSNE1PJ~BqJZ*#ZHjv&qvC%q?DH##)?4p{ z;Ed0bIG8fceV2Qjtxqj?)lA+Hs9U*(6{JCma5WwqACaK%-L9W7zK+J~g%}4{=4M+U zs3IwjD%y2M@p(#KCl_IU{pP9sp z!F=mb@qL)0_^yJ{+&O*yv%rl_^Y8-pG-VCPJh`5R$JO$h+a=0T<@uneM&YISz83+wA7Np43%K zQRuoN1K{23s)vCtQ=Oj!?Y~jwI`|u6Ml4^}UuD%t`4};_lbh7zz=xiErd|qBv9f9T zxGnL#3R^V#-eJoV5y3`9y*i;7d3UfR))#=dq`B z%8PpME3${KVST!IO{XW>apxa(N^rH^kJK~0g%SKr9#m^};tYcn6BClQ7fq$_)$tnw z@A%f*h1mxK@vqL72NdmThNbNW14gu`5}RgK_pP&xq*7PX%L7sGyQSNA=4UGndfXhv(6KsI%o&zL@nc;s zpmxmEBobF)vh$!HsydAuf4r`RxN!r8v(>J@i9C$h59}-U+QNS*uNd%V25IA zGkW~0RxcGTswOvV)fR3pJFePtej=HVp`!)vdtGg7g4rsbdbh5J8hiVN_EGDV=vK}P zwQ2_4n1|9S{manzS^^blJc-u-=D5^W?B0J{KNlLkO*%RauuQ49B z^{tb5r=c^c6>j(}^5{h1eE`bFl4AF>FEP+Q%8H{u!6!wn*02%U+vlbB5<`@Aq?Ep- zx*dj8ub@(5tMFtbzjUm$7_`01T3GeHob>h41Fw7%^k3nTus21x$SEAv(~s?xyNAL( zt|h6u10(~2ivx5;Or?0!Wj~mYOTwavEQob)D%7(a1bSyy3xw{Yn&c(Gm98>d( zJrO%xg`Xas7(P2GxK5%HcHvx$%F@(e#R-Fw!B7nRTx;zKEZGEFyv!SZhxS&dzGPZA zOFYkextOXZq7%ZUNOyJSo>{wTb!wm82wWDVZ0*)t7M*&v)HIXlA+%6yzC3WL;33Y=-1%2NChF_KsQ?@5z|_je-nTtr`or6U^tDe>@<@`u z`5Oy3ruW&eCJf#GGh4VeA>z~3_G#%CJpFHao~dV)Ww`z>4_}Jk6h*u&LDY{m>{R#? z{oMuKY@)S<(UZojaQgJj*J%l3Ah5S zyG4%OradxPKMH7G<)3qc!d$JkMi}HaFVIP#<)?aNGVA9d%)W0Ub0<)<691;6s!oi;OA3@1Y5Uu+Db;sf?uR zq}^_2yxV76TgJmNQEIxLbg{G#UX3gLz*`G9NSLCD;4T@T-Zi34$iYGrPH*p7KRYb@ z_W>z?fVQs-B4=6|16O=sH%ev1JN5;1u?7UG`DOg*Pa$-2&MuhVz^*NqAa3%XMBNP( zU%b#v^5Qak+RH!hO@HbsJ+%OnhZZ~C_?y(8=onzKm}V?cd{F++^pbv@957=(5WZ~q z5z2VR?(+5A^ar7yqj1d^YT8xteLgT4MT1;X4mjm0B z;n4KcJ9}V`TO*HQ*PfyqccZ-#6grQc4(6sz>SCBlh7KJFD8)T9U!X0WI@?8SqAmyk8$bj0bj=rrZMn*`1WXtVKIM_aKXO`AE@*ss|5dEr zk~o{q%+)rJ1xoM!XPV%I9wlrgXz7=y2D$TKR<^Dyqc;hzVEjHIk!HA-+4p$tfz4M zL(XJ!E|=9znbGAcJB-boogSxOEJ7ZAVQqW5rg-q{@jyxAiCaDp_-CV?pSUWL8%(d3 zHWp%YhD-#*WbzQm=UZU3?X&Q!><~74?`YH}Kpbc2K(kNI4ZS2LU@v1Sltse7uxHdzQJ1&46|!?wCQFLr#;*!YVWW^-tMmW z!s=*fcD)41*^lFj>YWUdaU#Zf@sAGRRcc=w2G?1>w>nr~r4X^Ko4M|iJ1t4Hy;+(V ze;(og#t4`VINwc-5-h<<`ZNQ&OseW{I2SS-{cAG9i8!`v%&dPJ@+x8reZtLg69wO5 zxFXlM{jbDeO8KkHV6sZM5nlx6u4r3b(d-`&X74C;GUJEVQ8#@1((c3N?cTCYh&H=* zlFEzc2~9CCpFF~D%PNh8Kl}Xdik{;P!~qg+0*Ou7SDtkDz<4Jzt1A|U-P+pdUX$es zbQ{56mVSQm)8g_e-mtG)B_?7gYt#wV@O68QoV%%4Bs z6{-S*$hBP6KYOY)K+t4XTgnTx=xQ(Ov><2U?Dz_r^9Q(5*h__^6PyM5J$j@szragu z70$WZ#B_439snO$JhyD(L#&dt4lr&K0w>2go+{SMOBaN3S83#SED?=(H$OhPjbPle z{bR((o)b1lj4-Hf(KH^#a7dM7^=>PL~f>ZO-)fb3;Uir3r4++aY@! z{lOK*L%%|QRX;o^U+hCBZ#7Z~C9jg?^o95?IE6q^&JW+4-(Tvy*!h!c;gO9m1r_s{ z!=IdBX1&o!pvw{YAfk6*^C@!N7l8joBLW1$ zzGMul>4E3O3L-s_9zRgp`YLd9lUL7YxEMV5$QmR~XRNNX#*A?Kq+v~BfL%iVl_0Wj zY9l4ei^RbDMLqCL05cP)Rf(BW#1FM}!d3L9_@xABIHL~d5pp=VxhYaeuzb+^SylR}d(?c*VWMcVk*z zW2N0}^@?DZ9!37C_7x+fL&fE8>01c*g)o>OlK7W?of1Nu4{C zfRiK;CR+eCBl*-@Vt7QIz%BD3;;03WP#jhSr4w@5mJm>(Yl09Gt)0)*h@*u9{XJ^( z0MBOuch47Mr?5mtYCJNr((?}W?0C5eOY@{WM(6b*29>Ro^NzIg;JHaK+ffhBl4lVi zmc1xeu2LHh{Fy#&6S7wS_JDMAdcpB`&f!NkN9VR0Vjrh|oej-@;MiwUZnEFz20-t0 z=XB_Mguc;>m@cb&U`1E$3Ez$(_8=Vp(@8_{6n1}Gdz1SF(HaGjpV$J6uG!_u^5bX8 zQ&^t|^H-yS<1WGWX`E9d3pad{@u32k5Vg-&H^@p53nDAiGgpjC2WeH8d>4cOJ$8?E zx7d|`;F*l2SQ7IzQRN}wi4#W^R+2^h=i*24FCP&%z@V@01`r&JEWbwy#s_He+j+l5 zbe?rnxJINt@U45sx-WjaY0cRq@H51p4B*e2LxM1*Z{6s!gTI3SAtVX@4z62leHn4%DW zHB@OPg8mWz;Cxg&?VF;5_riJRiYcP-P=sYvW9jKDAaFyR*y#(=O#K)LZ}=tD1A9ZLb)`Jd5xGs$^0Xi;!EmmFD2U$U=(~$n zW5c>(V+bu9tfhxW;rVA96fYAxh#Q7Fn)@~ouT`&MyHazIw>bplhN&tH;G^5YPLEjE zFxKnu+9y99RhxeArQ>)kdx8#q<=;k%lL>_GM5Q1)9gWaaEl2H~>F5E{veX|d6L%b#pM6nDsx>m&v{ zHNHw%V#F~|Yq}^5SWODl%k$>iJOAzDnOj0qZ26i`u5k?(1IbL$%L8eC_gmub3@&jN7ja2VPuCT(89 zuB7p?ukr|>!W$=#SU`kcNuRbwUF;FitFGdKKWLSI@O4Z`DTwCeNGB**)F8yF75Z?* zGJ-$jLO*>1Uy9ObHVY_&|-_tRN{JIS9tL%mx#U6K;#5FDuJpr z*}ibxuvNf0E#pW5S5fDhwLJEOgdxj`zGaElVE+b?7<@{;2;pKnAA-?Tc(UDD>NmU9JI5{mibt?*P#O}+1hFTl@ z+W8i=m~>NNG8?J{1(?&jzybw{lzyRIq6xOSX9r%_ikC8ekH~vDJTFti4|y7GabZ70 zm3#H0Kn7?2x*n^EhwuevExVyg3QCdPf6i08=DAP_rliN2#AWb>c}HJk6X(#z%!>%Q zrhudpVfZ{91Oqm!@UKtAH+U+nht@%)q*eHxPd=YW;j|SmHFGV?STTy0iryVG8ganS z09KwhRZHZ6(q4qDt;>}J4unqgbAbI5#tkKwd&zhWI3W5|(sps&{%umNfGPLZ0}q)= zHq>+~BQCT`HveqeVSxxSbZ)i`3oA!|+c3G@H<`^2s^cZu`6~>1W);Q0A1QO#aRy5! zMiR77ZZafTgh8L@V`7{olK;0=$lWLt~hWfooPg28`10NJs_LXXGYVf;x zLiLS2kv44H-_J}A$&(!64WtRNb%PnZ)2^tvhJoi<{IA)HEzVOnxvOL9f%Bu{RowDv z1T*o|c8$lS*)CP`8IJ?{do53^3WEPjl>!y87*%xo<#X;CR^bj}6tm0JhrK0H_y)M}nonEsUI?0&pFGFK*5($?ao2?4p9bpJtornUB|7@t`M49wqyC%8uXK|CKu)qt z#7w+c*Z~`D1KEp}VNGr^>Zw+*AIU22X3l5y2g&D!rjbDOwQ$lg^6#KcvdXDeDb^s&9>lPk% zd+WmhcMaGOaR`}P8=SEH=NK0FpCejgYZen*yYaFJV0?@#`qJZU?2b4)21m!(vd6V9 zwpLB#(XbD5+&&yTTrFh3SIcnM1S&~W~=@#eAvvzfh|-|W+}6Sus23{}I z-xq5lj3xGID)a4=!>g}?#POzU@i=q5_$YzxAD*z^x{x~*;aT{Hq!yCz%2sZF3ap{} zfvH$smw!Blts2GbtORm|B)`J%*+WasSAP>i(6&*3E*+g|zpR>zsyI_x{Y_(M$=kB6 z*#YwUt$4#En*$jlXi|~A!Abs!zn1JCe>zUPW=8A%)R_o4Et4OG$<~EJ>dc!Ly59!k zD*G~HnDkX{z1$=XsXAfn|1idt!>4Iq<49 zRQxf{jp~sztazd5c-D{O*%N9Kqxp)-V6A6~-EG4=ug>NHDI@%&b6JpEM?qjdw4yvf z6&IiG;rWc+r~u5`0_A_eTl-epg}Chg-pXlDe6NjMEB|^%UTZrCrwT2IuRPdzjOu#& zHK?0FC3&k&@)Vjc%nl|VHrSz8en-eOA|Ga9&0XzT@)i~ZRKj~nsH%YQRKQ-YemZ<5 zV&&0;=xVL9d@#!_YRikyAK+HFiBId#%rZ+_kIXWaZ2f;iBKk_n!kJ;QqMr<>J^ z^x%8g@RZxFi5_Kk=}0u;C+;Ktr%$M*EJCb4&t&&0ZIVyREj2p=VAIb4fjXWc*a6UK zTpI_qOS+WlnO%Vsh%l)-M|wdqeZ(jfQLR?Dp2VLMec>AY-w&x;U!T3VKsv4=_Cm&X zsPc#qbIgA1EOHP}IY=(+i~z9O3!luCB-6@F>peQCjvTKt`Ow{NiQ{`%Mkk;!@oT|( zysy41H=QsrNYBVwV`G7J03n%pFZuKn6Kc??l^O>29fVMW?X8ADKUG|*12&E!~pN=^bi6?@NN=HRk*qqEzT>KzF?)uro>=?aPC& z4+L5oqlC+ffOp*Xgs_ai7~4&%Ugs5xj8!LX!66D)2HH|dli-=9=MjI*)N}n!61G!} ze#44J=c(995V{kfOT#q;SS+Da)4{^SEGfaKMs)pd^)sU#yD;h1rm9Pyw^ZW(TS-k& z)a*^P^((B=9UQvL`UtXJtLD?xf0z`D)47X{pO8xQpi zro=;z=rovpp+H*omff2&Exo*Lw7%B!&}T6H>-T}*IEhw<=C~|dcnX%z0~ldRE!&E_ z`-`TG``KnwbS+92_CAo5sSg+6nIgWecvi0G8lG@@D-tvf_!YI5_ej0Z2pSBpA3v2g z4I@s8(G-wHINtunYY_eP%BFTw`=>?qytbwKzX+Y)$}vCv zij~mj6&EnP752L_rTD9-yysF&Q$5f;Q;|-6GlOSWSkywXT>Kh%pdAJod?yX)mBWK| z#d8oNq8dGm$Q8~GyIG=VG@7<= z24ilKHD2BLd~C!5vL+eu1d*o`13gMSC5)C-YD^%`9qq!VT&p$wkX|yCrzB{X$l6u? z`3{Oalk&HhL+U>`4-}^^v!}jdOuZjgO1y^J_LVzry};%I*#c{|4^DsD5hLWJXhgt1 ziPxL{J}po3z`BD#U$B7IsDbLw<&R91zvKxo4OnKmbZZ1Kg3j3=>3m#v@Dv7$D!Id3 zAR26wxJowQK}DE)@+E@Si(Z3YTpp{~TQU`Asr2_=!E`R&+8&0qAg{%$_T#<6pmDGY zl}^7-!68sY#p_a;{NiG?U^2*zo4ysI!H{9BZO2ZGnDG2&?ZAX`V=R6pi zTC@(r_uXM~xN?ko;0)O|%szPYO_v~Rp;BDcpl%|&UcP>Gfuph0e&}i7tfBkzh3;2B9-tmDobtUkHQ+6nq#hbtKukp|a-P&) zz~rpC2tOi?`KL~gA)XpF zrh6lkuMo3DVm1?Ry(C#y3N4e!6r;KzM4(C_dZvUgNRBAmU*J{| zG+?S7n7^Jd!s}gzgs3ScLm$ag4f~Uy$|#=xW{{6^8Mx?RTV9O=kJJ>9J9T{3A&e`o zDqy#dKEm=#tG!B5N=h(~z*S{_t$_{t5o{pJ@W~U7UbU9d=1a>cnDFC>?QcwDcBM+7 z4#UgMe+zvkmj9jW?DvLjl+U^Ok?trB{-rzxL-WVC+^>~2UWZ`AQ*MoD;>l?~H+DXJ z33<$WTwbO5i_~dn8}Wz?4e!lz^HaBbIl4~AQDn6(si~+?ghZ-xG|EZ&RM|h_ zJFCOKQI%DPhGIl;n^`}ZU-ab=6(sAM8@zKYfL^c(m0;i2s6A$xfZ6caRV5PrUHQ%(7AkVpUnu2kOYzgvDgRC z?E0`jH{ct>YCI$`9!SXaL^GHp4((3j`d48qaw5y=vbKyLAWwQOCpA)Vl}0^3fynrF zEW-!j9sUN9aO3ZYP@J80oO!L4*{uWi#|-N;@pwhnjgJeL;sCO7E0Jj+yS;EyE?ok$ z`{jV0=q;cjO#$9kyH5)c_$=zMzWW{W{KE!(CJA?t!@B923J<|3I;-_g%J?&wT-n_@e>Fb6U_=f8X)3LEA#Toha#>G1X5l0p?kUaq<0SYMTl?@k z`GxI0Zg^@{r4sGTzK`2yOX;=b1iW733eA*mc?~TTJI16`Ez_!K z2N1R~cD3O}?M>i@8}l?kk*zyGi;y#TT9pD|&$@R+v6R=(3phvoutpzv*K(h^OP@Yaj&71uI z&Bae_{42sY_g|+9CA{i)v(`!m%MalnNzXFb^(sIRxWKmZtYY_GJlkFf*Zmtn5nU#Q z9_a@_Di_cXV#Gt@X#DZ(xI!`%PS&VhScdb6HRGLX-@C@h^t8_>zMr1PX=n+FEV$!# zmmLC^IH<;|>6|10iQ4V*)hwDoJ==F2kWW`Z|HDE{QM*Wfac zAuSKe9&@xrvwwG67US`@uFXAqkRG9Le(u5cK$58zZOSz2up$5UiuV~6P}@Ve5l8$1 zNKry!&OD>_=CNG@w}F<$n5exF^n!K%u~Zax^_-T2k?9s*wWb#8+k>{g(woz;H(jOz zZ@EC|pT*ccjwlzPkDiYJffzl7nR;lx0!313c_O6YyGX%HkI*YNQ2IVvgfsZ?hNfE7 z16=-tR)EcZ$3<-+-OjEN*nQ~G<*U@84SK*g_wq8(?7^vu~CPy zuD`AJS7wqpN-pyw@jR2EY<9o$;fFl`NM}0j|E35Y7gU8k(nG}7=yCu|>0t38-V0D9 zN!ky<;rM~6mRijl7f!qR>qdhO*)s<3Wv}&Ust(0G6D?xp4Vn5mVtENmNy@#hb;K@`6uzv~DtSaS zD<~~q-)XqNd;2#LXq(;w4?__3?%fVhJ9kX>PvF7|i2lmo1>R*%fMA?(A8P(Kg{xy> z9(N+i&b<2B%YI2!JrvCX3sNLLg!pgp%}wK1t`ej!9TzWibqV)s0Z&-vsNf#HEx?i- z8?pu>eNLCRM%LR_heYpsh}$j3E;jEYjr8vq`PX@qq?@)*H zgM{SU3o6SjgDh=&f+~9|nxB_h@YngE`u^+5a{Cu&k57~6LdI`V+_oKRcb^D{=OuzU+fXDx9FEk#ppBo3kX|z6TQ$WJZdQm*+?x8L_PmDC zuC*oCj3YPC$`89;ucIm~Z)w0ifDA z9aCYdfz^^E)p?pZG}Yp3U0MD%XG%&xXvpVKicvi7*6ji)0u+UV4X>15Eho7LA0uRo z;^UO6Xi+vOVrNy@){Us9p`7w|dRq|%+fXcxpj!E7D{hZK+0NLy{y6rgfW@O0$TR4r zlQ;?E-k5Be6}CMWq)|kIMVvpdVhSt4(toco`*69c;pq^oLfK@CM#;c}P>y9259bvv z0IS&fP9fjC`nS3VyfS}d0jqNSknD@PE2W}UOwR+$weC)1xYnoHXmgfTp#oc0P_p8QO`>P27mt-n}XVn z{PqqtnNWtH3%oMVot_(d4NR55{jUz-lqKzbQd#aA)`0W&7ExBXjl=GbxQ9iY_E{hc zKznEF&))UHXJr*c262i`mptY3fE`ZN;J3TSIij1aqKV=KuP|6r4k4dvozj)Re}dsM zj{N!-)FB^?#&|USXg?3k+_P^Y1BQ7ZyLv`S859Fx!aw=xlVN z2qF}@X;^6Fn}_rrMWYQ~%z=ts>reMkp*$Xv7GN>!XXe*kp_m0Ic#r?Z%xP6C2wI*i zKqw`w;!EGVp9bd>!ngaf#NZwx9jQNVuwT8W+a|tQauo2^sid6dKsN4nIn+iM<;;_k z2H}<0D-w>lyYIr@hL_?PG3OqmWZ)cX**)6cjjOXLo2SJ%5w;x~T6-G2ceZqqcUl3H zxSFgdhGpyZi0JnWWH?%WLD`H9sD>!v_R?)OMb$_ukCrRc&U#)6%U}_&O^R(EbDV`O z=rq|ng?}PlXEG?a{D3>7b(;`RQnX0_$Ij^gnmx>jP(uWN{Ow&obETdeI;3$7Z$44m z!Kaq>-;U<`htQytl~K*c5SsC_oc-tscl|MvyGK40JUGgGIG)obK#h ztgw4je05H8^>GldQce;s*z9h{hvbgO{o#%bP*w7fFz_OEk|#MNh4K$F>*INo3Fdt7 zhRFXA>_xTKJcnj6f#JnpmQ(DwWDv1*>BDjd#=KRgt)X4SJ5d~@wM|mWG7%qm!*-Om z^qnLi7D!LYEw~3^7bZ~nG1&{bdgp9)qIion0XGR-fj4;b?S(J~@~2pPrb@&TU}Mj=&g>a9N!M%b%b+=7zl)vk};h4l`w zL}6bz$2AbamhTQMsAU)c8Z2OFk{!owb<>`tknZmb`)ybeD!B!ty_`2g9LIiUjZOK1 zu(Y2)frA%vSD%cnZI@v90Sn{Bes{-P9G~&v1BN0D=n_)eiBXQ3L5Tk6eRk2kqjs@s z(}T|feDnZooTq!$DwNt1>|L`{7xP@EmvU1W_5U==CmIBx-p&PoYIVj_(lEa*SL>QV zq~!J>RA5+Ai@2+o&r(vr^z;@rBs|31Y`7aP#0R`?)X-c;Vjr(WsgaG55V& zs9=fvL{c;WKv~a~#KR=ofrc9d|5t6_70}cYwYw8~Z&IWqO{5D51cazaFA7Q*Fd)4b zr4tY(bQA&UC`geSl_o7fK$Idyiu58Nf`r~7cZYM%e_#Ilc=N(T*6dkXGka#utnd4n zMKyMI2<(+mBvElH>J>Q^&9*d4&(>dFcSCr~TK_5C$0NysfKty#IrD7{7Q@72{+dlt z#TvcSp%JfDhIJQ2IDjcjRtdt>#Su!BLG3D}V1OB#m=%<6E;~he9l|#BAp5dtIAEhy z{(Tw`R(3zz?z|H0V-LC6UdiXT(S>Gn1`T|c80(xF92&CTt!{HlIJ6LoMh9RpaK>W*3CfNp7lzJnF|ku zHzf{)!PHe@jg_0&OpPCXN?nw~vE@p^6t!u{)-4c$EY|xvN7fs}AcW-?=jf|mfv1q@ zkQk`b$sOvfX1K7+(tr~5#D!*G6pcAoT_QB^&f&jV3FeQpFi2q%DM#=Rq2?DhR% z&*iP;^XtNf<%Rt#{}@j(Tl^IU8r?(!9rz_+==<;W6_V<>>H;$RM&fMOu2j8G9mQiW zIjRg!q`Im?cz5HnyY;2mlExA$jft{}K(qws^{3#id7VZyNetex`7Z8klTB7Tpk5b;`$4Gr=AJd zToT|!O+$4j3x?!|*gjK>O?8}ViQW8hAHtvKwPB0;jxnvq1#p;}{1s`t zrf)5Cta<$H8w@y3-;=b7Dyu zYdFds6`}G12UkmD!0_I#z%yygmWbXl0sNH~W_tqbZt?P^o>j?_?RZ`DJY&rE=GE|# z+*ov8S4BKq4O=;Ra&tfY8;Ix=!YoaBs#(uH2S>QqF0U+FU-8buob5stE?2nOAOJ!?$V36| z7hr6~U7%py(T5hRFh&&oviG`%7;xxXA_%pcYFa|T?G zZ1lLm?QvZZ5zfZfI~uT8L6Ok{*4C5YeTX%^E5I>LWJY|RR$t1k@8*c#RyBx{|f4Ut+D$8KtqJaSuu)ad`IajV60I8O(AjyK`*;tIVAhg_JrR-Fi0D+U3peJQ#}k9cE)IpT)7S<{xFH`E#TS){$jXPRbJO+X;V1jwMXPh~VE z)tD!jzksGOkR(Oa4muq>TQ)rGDM}8CeSNx9d3bfd&v59#WQYpH(@|y}iDRf|Ovt4h z7)*&9E3%C%X9~9WIvkZrf-|~9ZtI@y;q$Z3{=siAoPo&Aw6ykgDNyRdm6^J(A-->d zl|PI}3D2-i1vs=7hvthD0NdAUfiHtaO(eY77)^A>v(=TxWhM1W=o^msS?zUE#2>&$ z@zkZcN4I&H5z1R2%)b7C%d^wkPKp=(MFx}DS>m@tnSBu4*{lyjY9*>?ua{~iQ?+RAltm)|(?yhxyN za1oA11Qtezo~JFt#3Zz=?2SzB-#Qrz0e7;T=wIBIgendho^XYgzzUBgWRC7_?#^#_ z>}Og5U4Tpx(s_4pE% zBJO{6iBz5qO_C#(_l~tLC@mqR(AI~Duqe_>OM*m@+5Gn`Qw|{fZi^+ai4;6!E(`J3 z@Hr`YX4V2S?D8H&vGNzqE=($6dy4S5@lw6NIyfePg7w>rywleGr>i@~@Q+&o9@5h# zFu-Gy*V!0}^?@9D}zb^$w_j&CbMC746`Z^pKMO4oZP}n)vo7fob!g zlv?4^v%-{(rCOW!`r(nL()@XaPr=?V-aZbu7WDp3|IwRuI>NrOKg%Uh^&e_Xs1YkP z5Vg+ce?`9a1x7y{%Y70gF69+Vdv56!HqOT6K?Ng3=b0V`zDd$sMm1BLtfsU^e; zkNhx8zI8ElZ49uldKzNisiZOKR2YF7NFkS=0}=!RR?;Uk29m8;?-rIGdgwrSAEYP~ zxb<1fo%}U6?KfsgLCPAJ=wl*D^fr(5s*%;7yrtxk*Y6i{Lr9R5&`*VJ4-Ba^4#eHz zh=791(|0buJ4Q6=X_Z^?pS0GQ5wJG*)}|8mZ7q@1&D>O0;4`^G6?@;#M>@%_o9@~O z^yHjp-mchFND;f=1B}N|od!zF0G~m(#cBFTWX6dO(w{^8#EAq0yOzb||^<2HE&F9i#(`k2QVrn?rV!*1Ns?cBX?4 zOh{0vsP^|ywcko1gq!Ipv?L?!;e>c}3V%58h!o2~ci2i=va4okp z=S3@%93wYZF9555K=fNg@UH58_4FlM<%!2mD#BksoDGIT;W^L*`?mBBBpl%;@Vk6J zVw@%dOry@s91AqO4d{)s&pzt^Y3}^iJP2~u3Pedkl{t{=o}2!XA6xK)o6P+(C$+yb zsx=H786KA5!N9}c{2Ypjs|DOd)N=t+Vn!vxAKtDV9{P@>7-N`*eOEVff;@Mo9IV}HW&M^7v}() zl>oRP95HRrep^vPEg7xN^SDU@zeF$-Rh$W)dlbAQ@7|KUzEh^R(Xb+Rs-qjZbPJni z`fgZ~1cUzoMa+pzDek_#DvC5kK3fF~fA^o;sVcK}_zB;zVbA4NQ45aF4$fzX!g0(= z!1CbGm;{71T}T?7DkA7#?_)S8aQf&CQ03xSxVVG zY*|$W(#Ym=(q|dO=qsB$=hjy>Jlqlph5t@?Ui98>i6J-gHA>YUD&Dlm3MeH&3~qZi zMN@n`{WjdPBm>fu2ZY}X0_z<_b>0(mroL&JIi{2bY*i~#o`0V=kzn>+KFy@m;{|?` zgsrz#f9QRTJ9DN2v}j!Wko@b6w*fTg*0s?2#*plmPw>!$6g&#NDFsEjymmcot&0nQ zq*~4=_bFJ^q~wxJW5GOc%5Be$sd1Cu0FNd94$1KFG^wmzY)~aZS_w)KM649r+`k`) zqmuH-h$NsdyTi|jR!ZJ$nMzGoO@ zE{em0=~fxicExk?@wvDI1Wo*19l}9JB=0`4up@~O5{O&Xj!?tu5mf8kw2Z#(-vv$t zptMV1HbVW zf|5^l`3baA9QiQIwM}HaFKYp18?=czzlHk*$UlWfuLMX?nl_DQoI-?f&?Z)7+Cp7| z0LHOqs{Sq2bo)>3V;CiJC5HH~StVDaJgzQ15Ho>H;QEXCUB~@iO)~-q{UQ&smuOlQwKnQ~JTD zdPa|m^(f)_YpMc>X$xdErsSauITEb;MjBRJq`32~%)Vi!2iIQo<%%CDvjZ~fY#|%C zR+@tkZ@4SV*q)1K-3Z5KHRjl1p_7enXTQS&DrR?8DTphov>HDnG(XOf`C90MiV{*T z5PGjr4wZ5j4An(j&*vWud8LK%V#J@xdc}c?Y2cc?XSpT-J-aFFmwB3Q-@uwXqfKZg z52*r$VnJT#rk5X6jvjZEPmFgf)j{hE-fd^)Bomi{v2?iu+>X>ntz`xCCKnZp{@jTi z$EoEX1(gz~LATm5Pf)W4sYnGp9H{pu$nq2|n2994PX2BT?RAKQ9ouslRu%7D9z`AS zpa*!?oo_o@|HQA!6Gots7+D2s`7}0^XD>!Xiv-w9)7R3}1@{JffWYESt!rSL!#}Ej z*r!F^+7lffPdhXaWHy$>fk?UuomsOXKto}ODEiRffdXV^csdL!Lv2oARTVidw@!4i zdj2EaZ5m(XRqSfd#ZN8)b`?3wSwtpQqMQ$P=>(P*V(-Ior~rL}6a!gzodh<~v1Y9L zJ&D0JHvM=|9|q$AVr`l35{?YjZ#f7r*#W8qyeHATBBKG%dv*H2=!+cRS=nY7S;gYi zXFZk)T`-)pPat{nw6H45zMdghPBq;xAXk_&yfv6IsS7{y5yNw}o*Qf6STmX)VEE(= zkxcLmDg^8_LoPwD23E&+u|u$ysDhS_RQ<MSO#2VeYdc_hI~+G#EEH z9K3@xD5!y$w1Xr_x|gphpIqM9SDtP2_asK55^Ig>X|V$A-+qh_g-Vgapp#L`f z6(98F^TUUXK5{oH$eO>7s$mu}u(0|ghXI~>`Q=>Yf|=%+zo+%$1SuHuMt~kH0{yfG z9Oj>uh#I32ycCSnQtcOWY3|q~e;m_I0d-@Wi9OYAGx=xr_O{oLx8*W9PLsIK8z+E` zWf`h!=@aT=Pf*b-YRH0lKiY%Q!`LpAC<&+u;WHnV9CpIicueVMp_D>%-Y5s42X8J* z#Gmu;T~#$jTv2|5nQ&6qnr;Y1QLhc?pUnwkk{PmG=E;_Me9S(UyT~r{D3zbSBRE?@ z_<738!yL1y&#+y4tFC2$k@J`N*`flkw~Xh2hTv1d;i_h`gDX7#F@3oQbAM(ohTQ*H zvbsYwlJ6=>i0B3h(o@-Lrs}Qv;X@{)?1BOhxWGoM_bhz>Bnw}E*XGbYpBPQd=^Ywv zt$#LuBA0oj&;ZKOL2PIto;XE}MFS})6mpUjEiyzrqX|808li`N6k!)g2phJVU2vT} zWFtLcP<``LAN?j#>5!m}^BTMs3=c@FD)x_5_oN-AL*THprOb5VLd!TcNA z${SXny*}V&O>7tO+XIxd5B}yhvFUsW(o%U<`o5W5lnE~3h@qF7=l?2TrAy$Ai5Qkh829MIciMjM|B{FC( zSo|)xmh)&JEI&5ju{joSYi|JeHS!iMemMDEy57BLnwfv)Zn#X&)}y9ZY5*(=mENMH}oO`T)fXk ztig(0q>5I)9EwLGNK>)o+u(~UWevV;7M+RH8hxhXm!fJ8PjD9gfx;@qjvXIG7CfID z64T#w{e2=gA#`oM{Xw~@zkSj7zs3LClMNs?PzY~ij%fRrVLg97TWWAQXQ#f2aw902 z>A7Z&!NSJw5X8W`DFMFVybHM_aorT_Okq( z_0v_11m_)AcK0Og$|{FyOp!b{w$j{~tjFqqQCCYB9f6P|xR4iz zDIImrg`GY@2r7&l^fz@aCzn(0OONC-tBqOG zhMPUn_L*53@mp(Vs;kD$gvkkJ3=K32uc%B~859tmMXsWaNOctbe5EM<*^sXov&99U zi(nGjtG-Cwt3r^Z8Z!reU&<4tSr-RUlL=M%ynF(6j@P=<{UKmo0KBat9lmyMW(E8P z#nA}K){4U%s!p-VyeRxqWRHBm$DAzQ^5gZsIIAg^Xg$iB2ZWKka|@Kcy9)}}g1Gd* z_;ID}BC$%*g#F6E83%s7BAKtg&dKN@eWmO^-|_T5S(zFq;jgkojLh*}>y#CjBdFFn zV&`KxT1l!hx1$B9ry0k*;cx3_w0+q%NEmKLTW;ZMgZe6X)*P>O_cs}624Ea1!7^%xh^kJ+E%JCX_W+H9A2TT@0d#~fkz)%H4YgXn zOYH}^ZFTZT>!J8E9CoJR=k(2tD~Gh_CZ>K%+Wm$?`cC^}>_NPdSGD`z#=aajclOb^ zvIrws09I(hB)^I~ja)R0y#cx}Z;>|@y!dWA;jim+`-<2h5y6ChITh|1T(9tGs=b!A z&;CB}##dy6W7D|b>9l6R)Dw|z_n^z*=LtdSAHbkWA@wzSMJ+*5YE!)PjOzpf~uCpyO~GD$u8NHLEAb0LcUO^+=$!~5z}!=-ROBZF#dmT<`5Ts!_KJm z+xthlg_H9l?|hq@X94_{l^4>8zf=FaHy9X`F`t74ert z(!On_4(sUJv)2bvBjcvMSWXgxG5oD01FVVHmCvJ4!s^48)Exm zDj-Ga$9$U@=v}ByxYGL*wK6Zyda@p;al%2Xyh<#}hoY%c^OvuHUXV$`>mTI;eGV=>J)a1u1hhwOJLi&P|e((%AceYyf1RUKPb z&z|$%06>5QC|0RA7RUKA>v%f9@R!@2FLO@N;JEm07%mwntg4x=#t z_A5KK<5~@3f##n+dFz3^2Ai%DNM(hwWpa;#3aX#{%b{+ZW10VM^|i-2Q`1FY9(w*s zf&o&+t9g&Pw;%tN{rWdfK~P>0c1p&ma`Ork6+6@8`I+j4cMp#KF$N5o08;DAzBOIb z{CPA{S%)Tqu+;_z6Ov*k_{zbj%H~SxS>u}V*W`W8FtYQv4FC$10qW*^nhnYI90@T- zkImi&So`!!eM8nX8kAL*eh=#0mm^glDmWiZ0N@P)Y&PD*yDx3l$fG_M7-h9h5XDES zTQQ@mU`WH82IU4yw`)7h#zm$RKsD%1Ecriy87d#fmWS5(L334;e*Xw{ zXHygT+%w}5GM4VcKRGk=vE$%ijD6?dA5$O)YBq)UDQ_m;i*Bef%OWx982B5W^(9~F z20)!hEc+`O`?1y|_-6c>0O+gV*BXgZP6j5Azn2~kJGlAec*Xo13~&WT@@117h}Rq!_07jJ_TtUo%J69W zn&XRe=VjB0ppJFPLNEM-)j|wHr0_YrS`DBBvJ6~@|GIn?%*Iy1g~7Uv-eZBmYia*^ zDb!p+p;PBMGJ@KAI&qohTgk&4xkp(?IWEEd__vcsc9%l9|7{GQhU;%`(p56lz86%! zk!N7{nUjNz8UWIFU-9e@5B<^kW7JvuMoP@1o`BCjRRTt~^1kCyJf$HO&eLVN5?0 znEc2^MQumd^aN1Q%%5)>)0#Oz9mU90jvn;!H4MM{y%_ReQ{KCSR>W>p*a@^n)}VW| z?%yyUgH%V${q-;1XD^)W2PIcoJeDh}0$W7u=Q;mt#Fkhdv3)qtQ^rzzL=~jf?P$R0 zxwb)*)55dtmX5mUe_|8q2$1?#?puZv3#Xw> ztohwLoV+O4g(|e}?%jcR6=!Wuepw8xdjo@}K7zwQ^ch zw#%<(b)ye@yE}#^$V(HWKmIoK$6?f1KRrrxpHaH{qI*knBKf=itkH?*pwFSbt8bid znQxEp+S=XgT-_Bv5-k3E+H5=!9)S39eg5NZd&6|wSZqXL%KYeXbi-1l0#%T!$Ns%B zov_eNy~jOF0grod0^2bW=<0DxH{9@Wj7Mi%Li9xrE`BBBd`C3Ze~$!!O`WtCehnDq zN8?|Ye_Wc_>8QBi>VBbU{!<-Q^M$aQfU8^!G|$q-s?*2y-5lMVT%B>Fqn)tMk-11~ zMv2uYONC7>{{;;AN=vd#<=Sl`e)U*u0&zIC5`Vs+E_P8I-U^922|Q&^|Flzh&_Pto zVt-_ZKO}OA)#`RPpD2!3k*YDwEcyH6xwFoM`hKvz?%sbM!eds8(zbM6#Z%JdYr1!) z*$>N#tjn~U5KimjjOrkU(1*_v2`n~G2)p2XDb2w zhdV>zaq-^n?#W-LeDGn=8?it7G7f@_t-(yCHCm7eX(RNt-9=!mrBxz4sCswaZ9;gu zvwgn8jivU({mCs_FV-jN(R(^xOO~uf2gYNpeE15rYRQoG+K2HG`dx~NR@3@sc!RLQ zmi(Z#Bl#tdUAdj=HJ3jwnfv|wA8;i&H*Q+%f)E zSs|8`>&ME+FQ?lV?`C*#e~P}OJaqa&Tzp*Ugwd5;tC@|6c$EgUS!lo`m1|c6@mris z7Nu5IchAbr4^LBWk2%7fsj9WgvVS~wzU6w)&Q{x2bVNPZ%G$%0frI(yWSJstL_MQh zuE;k34s3KatFq?k1>+#KZC*=8Sn-QH^(Wo*3vClYj}E6g`3x;i5C2fQ+U+DZ%NhEX zu9I-|HcZ)MQ*oqplQl659lD0Js&}t*ze+qFd6*CRXP6BqN@&n`>*Gb@%S}0$GuTdo zW}IE^3hK>#e9Pix(_HkwsK4>87*(`(`=2!sj0&6-iopZ@0gr7TH@lY+!lZRuU#;S% HO~n5JsWn^e literal 0 HcmV?d00001 diff --git a/binding/android/FalconTestApp/falcon-test-app/src/main/java/ai/picovoice/falcon/testapp/MainActivity.java b/binding/android/FalconTestApp/falcon-test-app/src/main/java/ai/picovoice/falcon/testapp/MainActivity.java new file mode 100644 index 0000000..1701e07 --- /dev/null +++ b/binding/android/FalconTestApp/falcon-test-app/src/main/java/ai/picovoice/falcon/testapp/MainActivity.java @@ -0,0 +1,225 @@ +/* + 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.falcon.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.falcon.Falcon; +import ai.picovoice.falcon.FalconException; +import ai.picovoice.falcon.FalconSegments; + +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"; + Falcon falcon = null; + try { + falcon = new Falcon.Builder() + .setAccessKey(accessKey) + .setModelPath(modelFile) + .build(getApplicationContext()); + result.success = true; + } catch (FalconException e) { + result.success = false; + result.errorMessage = String.format("Failed to init falcon 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"; + + FalconSegments processResult = processTestAudio(falcon, 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 Falcon.Builder() + .setAccessKey("") + .setModelPath(modelFile) + .build(getApplicationContext()); + result.success = false; + result.errorMessage = "Init should have throw an exception"; + } catch (FalconException 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/falcon_params%s.pv", suffix); + } + + private FalconSegments processTestAudio(@NonNull Falcon 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/FalconTestApp/falcon-test-app/src/main/java/ai/picovoice/falcon/testapp/TestResult.java b/binding/android/FalconTestApp/falcon-test-app/src/main/java/ai/picovoice/falcon/testapp/TestResult.java new file mode 100644 index 0000000..7dc9b07 --- /dev/null +++ b/binding/android/FalconTestApp/falcon-test-app/src/main/java/ai/picovoice/falcon/testapp/TestResult.java @@ -0,0 +1,8 @@ +package ai.picovoice.falcon.testapp; + +public class TestResult { + public String testName; + public boolean success; + public String errorMessage; +} + diff --git a/binding/android/FalconTestApp/falcon-test-app/src/main/res/drawable/button_background.xml b/binding/android/FalconTestApp/falcon-test-app/src/main/res/drawable/button_background.xml new file mode 100644 index 0000000..7e73f1a --- /dev/null +++ b/binding/android/FalconTestApp/falcon-test-app/src/main/res/drawable/button_background.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/binding/android/FalconTestApp/falcon-test-app/src/main/res/drawable/button_disabled.xml b/binding/android/FalconTestApp/falcon-test-app/src/main/res/drawable/button_disabled.xml new file mode 100644 index 0000000..ffe1c93 --- /dev/null +++ b/binding/android/FalconTestApp/falcon-test-app/src/main/res/drawable/button_disabled.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/binding/android/FalconTestApp/falcon-test-app/src/main/res/drawable/ic_launcher_background.xml b/binding/android/FalconTestApp/falcon-test-app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..196e181 --- /dev/null +++ b/binding/android/FalconTestApp/falcon-test-app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/binding/android/FalconTestApp/falcon-test-app/src/main/res/drawable/ic_launcher_foreground.xml b/binding/android/FalconTestApp/falcon-test-app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..d2923ae --- /dev/null +++ b/binding/android/FalconTestApp/falcon-test-app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/binding/android/FalconTestApp/falcon-test-app/src/main/res/layout/activity_main.xml b/binding/android/FalconTestApp/falcon-test-app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..237ac94 --- /dev/null +++ b/binding/android/FalconTestApp/falcon-test-app/src/main/res/layout/activity_main.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +