From 54a08bde2986633f089a3a3297b7b67f3c61022b Mon Sep 17 00:00:00 2001 From: Tong Li <31761981+litongjava@users.noreply.github.com> Date: Sun, 12 Nov 2023 06:31:58 -1000 Subject: [PATCH] examples : add whisper.android.java for compatibility with older Android versions using Java (#1382) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * save the recorded audio to a file * Alignment -help * Save the correct audio * chage to a consistent coding style * Correct typo * Update examples/stream/stream.cpp * Update examples/stream/stream.cpp * Correct variable misuse * Update examples/stream/stream.cpp * Update examples/stream/stream.cpp * Update examples/stream/stream.cpp * Update examples/stream/stream.cpp * add *.bin .cxx/ .gradle/ cmake-build-debug/ to gitignore * add whisper.android.java * Added support for older versions of Android of Java * add examples for android java * add README.md for android java * add fullTranscribeWithTime * 增加 toString()方法和测试 * change return type to void * update to v1.4.1 * add WhisperService * chage to whisper_full_get_segment_t1 * add method transcribeDataWithTime * modified toString ``` return "[" + start + " --> " + end + "]:" + sentence; ``` * Optimize code logic * update text view on handle * set max lines * change Chinese to English * Update bindings/java/build.gradle * Update .gitignore * add android.java to github action * chage android.java to android_java in build.yml * remove gradle * chage jdk to temurin in android_java of CI * chage jdk to temurin 11 in android_java of CI * add x to gradlew * set api-level for android_java of CI * Update examples/whisper.android.java/app/src/main/jni/whisper/CMakeLists.txt * add ndk version in build.gradle * remove local.properties * add testFullTranscribeWithTime --------- Co-authored-by: litongmacos Co-authored-by: bobqianic <129547291+bobqianic@users.noreply.github.com> --- .github/workflows/build.yml | 26 ++ .gitignore | 4 + bindings/java/build.gradle | 1 + .../ggerganov/whispercpp/WhisperCpp.java | 25 ++ .../whispercpp/bean/WhisperSegment.java | 47 ++++ .../ggerganov/whispercpp/WhisperCppTest.java | 45 ++- examples/whisper.android.java/.gitignore | 15 + examples/whisper.android.java/README.md | 20 ++ .../whisper.android.java/README_files/1.jpg | Bin 0 -> 68988 bytes examples/whisper.android.java/app/.gitignore | 1 + .../whisper.android.java/app/build.gradle | 58 ++++ .../app/proguard-rules.pro | 21 ++ .../android/java/ExampleInstrumentedTest.java | 26 ++ .../app/src/main/AndroidManifest.xml | 22 ++ .../app/src/main/assets/logback.xml | 40 +++ .../whisper/android/java/MainActivity.java | 107 ++++++++ .../whisper/android/java/app/App.java | 13 + .../android/java/bean/WhisperSegment.java | 47 ++++ .../android/java/services/WhisperService.java | 101 +++++++ .../android/java/single/LocalWhisper.java | 66 +++++ .../android/java/task/LoadModelTask.java | 44 +++ .../android/java/task/TranscriptionTask.java | 44 +++ .../android/java/utils/AssetUtils.java | 91 +++++++ .../android/java/utils/WaveEncoder.java | 105 +++++++ .../com/whispercpp/java/whisper/CpuInfo.java | 121 +++++++++ .../java/whisper/WhisperContext.java | 138 ++++++++++ .../java/whisper/WhisperCpuConfig.java | 12 + .../whispercpp/java/whisper/WhisperLib.java | 75 +++++ .../whispercpp/java/whisper/WhisperUtils.java | 34 +++ .../app/src/main/jni/whisper/CMakeLists.txt | 56 ++++ .../app/src/main/jni/whisper/jni.c | 257 ++++++++++++++++++ .../drawable-v24/ic_launcher_foreground.xml | 30 ++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++ .../app/src/main/res/layout/activity_main.xml | 57 ++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3593 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5339 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2636 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3388 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4926 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7472 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7909 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11873 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10652 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16570 bytes .../app/src/main/res/values-night/themes.xml | 16 ++ .../app/src/main/res/values/colors.xml | 10 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/themes.xml | 16 ++ .../whisper/android/java/ExampleUnitTest.java | 17 ++ examples/whisper.android.java/build.gradle | 24 ++ .../whisper.android.java/gradle.properties | 19 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + examples/whisper.android.java/gradlew | 172 ++++++++++++ examples/whisper.android.java/gradlew.bat | 84 ++++++ examples/whisper.android.java/settings.gradle | 2 + examples/whisper.android/.idea/gradle.xml | 2 + 59 files changed, 2299 insertions(+), 1 deletion(-) create mode 100644 bindings/java/src/main/java/io/github/ggerganov/whispercpp/bean/WhisperSegment.java create mode 100644 examples/whisper.android.java/.gitignore create mode 100644 examples/whisper.android.java/README.md create mode 100644 examples/whisper.android.java/README_files/1.jpg create mode 100644 examples/whisper.android.java/app/.gitignore create mode 100644 examples/whisper.android.java/app/build.gradle create mode 100644 examples/whisper.android.java/app/proguard-rules.pro create mode 100644 examples/whisper.android.java/app/src/androidTest/java/com/litongjava/whisper/android/java/ExampleInstrumentedTest.java create mode 100644 examples/whisper.android.java/app/src/main/AndroidManifest.xml create mode 100644 examples/whisper.android.java/app/src/main/assets/logback.xml create mode 100644 examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/MainActivity.java create mode 100644 examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/app/App.java create mode 100644 examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/bean/WhisperSegment.java create mode 100644 examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/services/WhisperService.java create mode 100644 examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/single/LocalWhisper.java create mode 100644 examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/task/LoadModelTask.java create mode 100644 examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/task/TranscriptionTask.java create mode 100644 examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/utils/AssetUtils.java create mode 100644 examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/utils/WaveEncoder.java create mode 100644 examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/CpuInfo.java create mode 100644 examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperContext.java create mode 100644 examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperCpuConfig.java create mode 100644 examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperLib.java create mode 100644 examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperUtils.java create mode 100644 examples/whisper.android.java/app/src/main/jni/whisper/CMakeLists.txt create mode 100644 examples/whisper.android.java/app/src/main/jni/whisper/jni.c create mode 100644 examples/whisper.android.java/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 examples/whisper.android.java/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 examples/whisper.android.java/app/src/main/res/layout/activity_main.xml create mode 100644 examples/whisper.android.java/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 examples/whisper.android.java/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 examples/whisper.android.java/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 examples/whisper.android.java/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 examples/whisper.android.java/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 examples/whisper.android.java/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 examples/whisper.android.java/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 examples/whisper.android.java/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 examples/whisper.android.java/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 examples/whisper.android.java/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 examples/whisper.android.java/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 examples/whisper.android.java/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 examples/whisper.android.java/app/src/main/res/values-night/themes.xml create mode 100644 examples/whisper.android.java/app/src/main/res/values/colors.xml create mode 100644 examples/whisper.android.java/app/src/main/res/values/strings.xml create mode 100644 examples/whisper.android.java/app/src/main/res/values/themes.xml create mode 100644 examples/whisper.android.java/app/src/test/java/com/litongjava/whisper/android/java/ExampleUnitTest.java create mode 100644 examples/whisper.android.java/build.gradle create mode 100644 examples/whisper.android.java/gradle.properties create mode 100644 examples/whisper.android.java/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/whisper.android.java/gradle/wrapper/gradle-wrapper.properties create mode 100644 examples/whisper.android.java/gradlew create mode 100644 examples/whisper.android.java/gradlew.bat create mode 100644 examples/whisper.android.java/settings.gradle diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 38e476b9..974ecda5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -396,6 +396,32 @@ jobs: cd examples/whisper.android ./gradlew assembleRelease --no-daemon + android_java: + runs-on: ubuntu-latest + + steps: + - name: Clone + uses: actions/checkout@v3 + + - name: set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + cache: gradle + + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + with: + api-level: 30 + build-tools-version: 30.0.3 + + - name: Build + run: | + cd examples/whisper.android.java + chmod +x ./gradlew + ./gradlew assembleRelease + java: needs: [ 'windows' ] runs-on: windows-latest diff --git a/.gitignore b/.gitignore index 9ff35d00..00325823 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,7 @@ bindings/java/.idea/ .idea/ benchmark_results.csv +cmake-build-debug/ +.cxx/ +.gradle/ +local.properties \ No newline at end of file diff --git a/bindings/java/build.gradle b/bindings/java/build.gradle index 8f7a5fd9..75f3a9cd 100644 --- a/bindings/java/build.gradle +++ b/bindings/java/build.gradle @@ -9,6 +9,7 @@ archivesBaseName = 'whispercpp' group = 'io.github.ggerganov' version = '1.4.0' + sourceCompatibility = 1.8 targetCompatibility = 1.8 diff --git a/bindings/java/src/main/java/io/github/ggerganov/whispercpp/WhisperCpp.java b/bindings/java/src/main/java/io/github/ggerganov/whispercpp/WhisperCpp.java index 4a250403..4c1594d5 100644 --- a/bindings/java/src/main/java/io/github/ggerganov/whispercpp/WhisperCpp.java +++ b/bindings/java/src/main/java/io/github/ggerganov/whispercpp/WhisperCpp.java @@ -2,6 +2,7 @@ package io.github.ggerganov.whispercpp; import com.sun.jna.Native; import com.sun.jna.Pointer; +import io.github.ggerganov.whispercpp.bean.WhisperSegment; import io.github.ggerganov.whispercpp.params.WhisperContextParams; import io.github.ggerganov.whispercpp.params.WhisperFullParams; import io.github.ggerganov.whispercpp.params.WhisperSamplingStrategy; @@ -9,6 +10,8 @@ import io.github.ggerganov.whispercpp.params.WhisperSamplingStrategy; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; /** * Before calling most methods, you must call `initContext(modelPath)` to initialise the `ctx` Pointer. @@ -160,6 +163,28 @@ public class WhisperCpp implements AutoCloseable { return str.toString().trim(); } + public List fullTranscribeWithTime(WhisperFullParams whisperParams, float[] audioData) throws IOException { + if (ctx == null) { + throw new IllegalStateException("Model not initialised"); + } + + if (lib.whisper_full(ctx, whisperParams, audioData, audioData.length) != 0) { + throw new IOException("Failed to process audio"); + } + + int nSegments = lib.whisper_full_n_segments(ctx); + List segments= new ArrayList<>(nSegments); + + + for (int i = 0; i < nSegments; i++) { + long t0 = lib.whisper_full_get_segment_t0(ctx, i); + String text = lib.whisper_full_get_segment_text(ctx, i); + long t1 = lib.whisper_full_get_segment_t1(ctx, i); + segments.add(new WhisperSegment(t0,t1,text)); + } + + return segments; + } // public int getTextSegmentCount(Pointer ctx) { // return lib.whisper_full_n_segments(ctx); diff --git a/bindings/java/src/main/java/io/github/ggerganov/whispercpp/bean/WhisperSegment.java b/bindings/java/src/main/java/io/github/ggerganov/whispercpp/bean/WhisperSegment.java new file mode 100644 index 00000000..da970b58 --- /dev/null +++ b/bindings/java/src/main/java/io/github/ggerganov/whispercpp/bean/WhisperSegment.java @@ -0,0 +1,47 @@ +package io.github.ggerganov.whispercpp.bean; + +/** + * Created by litonglinux@qq.com on 10/21/2023_7:48 AM + */ +public class WhisperSegment { + private long start, end; + private String sentence; + + public WhisperSegment() { + } + + public WhisperSegment(long start, long end, String sentence) { + this.start = start; + this.end = end; + this.sentence = sentence; + } + + public long getStart() { + return start; + } + + public long getEnd() { + return end; + } + + public String getSentence() { + return sentence; + } + + public void setStart(long start) { + this.start = start; + } + + public void setEnd(long end) { + this.end = end; + } + + public void setSentence(String sentence) { + this.sentence = sentence; + } + + @Override + public String toString() { + return "[" + start + " --> " + end + "]:" + sentence; + } +} diff --git a/bindings/java/src/test/java/io/github/ggerganov/whispercpp/WhisperCppTest.java b/bindings/java/src/test/java/io/github/ggerganov/whispercpp/WhisperCppTest.java index 66e18f9a..ccc3be89 100644 --- a/bindings/java/src/test/java/io/github/ggerganov/whispercpp/WhisperCppTest.java +++ b/bindings/java/src/test/java/io/github/ggerganov/whispercpp/WhisperCppTest.java @@ -2,6 +2,7 @@ package io.github.ggerganov.whispercpp; import static org.junit.jupiter.api.Assertions.*; +import io.github.ggerganov.whispercpp.bean.WhisperSegment; import io.github.ggerganov.whispercpp.params.CBool; import io.github.ggerganov.whispercpp.params.WhisperFullParams; import io.github.ggerganov.whispercpp.params.WhisperSamplingStrategy; @@ -11,6 +12,7 @@ import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import java.io.File; import java.io.FileNotFoundException; +import java.util.List; class WhisperCppTest { private static WhisperCpp whisper = new WhisperCpp(); @@ -20,7 +22,8 @@ class WhisperCppTest { static void init() throws FileNotFoundException { // By default, models are loaded from ~/.cache/whisper/ and are usually named "ggml-${name}.bin" // or you can provide the absolute path to the model file. - String modelName = "../../models/ggml-tiny.en.bin"; + String modelName = "../../models/ggml-tiny.bin"; +// String modelName = "../../models/ggml-tiny.en.bin"; try { whisper.initContext(modelName); // whisper.getFullDefaultParams(WhisperSamplingStrategy.WHISPER_SAMPLING_GREEDY); @@ -99,4 +102,44 @@ class WhisperCppTest { audioInputStream.close(); } } + + @Test + void testFullTranscribeWithTime() throws Exception { + if (!modelInitialised) { + System.out.println("Model not initialised, skipping test"); + return; + } + + // Given + File file = new File(System.getProperty("user.dir"), "../../samples/jfk.wav"); + AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(file); + + byte[] b = new byte[audioInputStream.available()]; + float[] floats = new float[b.length / 2]; + +// WhisperFullParams params = whisper.getFullDefaultParams(WhisperSamplingStrategy.WHISPER_SAMPLING_GREEDY); + WhisperFullParams params = whisper.getFullDefaultParams(WhisperSamplingStrategy.WHISPER_SAMPLING_BEAM_SEARCH); + params.setProgressCallback((ctx, state, progress, user_data) -> System.out.println("progress: " + progress)); + params.print_progress = CBool.FALSE; +// params.initial_prompt = "and so my fellow Americans um, like"; + + + try { + audioInputStream.read(b); + + for (int i = 0, j = 0; i < b.length; i += 2, j++) { + int intSample = (int) (b[i + 1]) << 8 | (int) (b[i]) & 0xFF; + floats[j] = intSample / 32767.0f; + } + + List segments = whisper.fullTranscribeWithTime(params, floats); + assertTrue(segments.size() > 0, "The size of segments should be greater than 0"); + for (WhisperSegment segment : segments) { + System.out.println(segment); + } + } finally { + audioInputStream.close(); + } + } + } diff --git a/examples/whisper.android.java/.gitignore b/examples/whisper.android.java/.gitignore new file mode 100644 index 00000000..aa724b77 --- /dev/null +++ b/examples/whisper.android.java/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/examples/whisper.android.java/README.md b/examples/whisper.android.java/README.md new file mode 100644 index 00000000..44675ab8 --- /dev/null +++ b/examples/whisper.android.java/README.md @@ -0,0 +1,20 @@ +A sample Android app using java code and [whisper.cpp](https://github.com/ggerganov/whisper.cpp/) to do voice-to-text transcriptions. + +To use: + +1. Select a model from the [whisper.cpp repository](https://github.com/ggerganov/whisper.cpp/tree/master/models).[^1] +2. Copy the model to the "app/src/main/assets/models" folder. +3. Select a sample audio file (for example, [jfk.wav](https://github.com/ggerganov/whisper.cpp/raw/master/samples/jfk.wav)). +4. Copy the sample to the "app/src/main/assets/samples" folder. +5. Modify the modelFilePath in the WhisperService.java +6. Modify the sampleFilePath in the WhisperService.java +7. Select the "release" active build variant, and use Android Studio to run and deploy to your device. +[^1]: I recommend the tiny or base models for running on an Android device. + +PS: +1. Do not move this android project folder individually to other folders, because this android project folder depends on the files of the whole project. +2. The cpp code is compiled during the build process +3. If you want to import a compiled cpp project in your Android project, please refer to the https://github.com/litongjava/whisper.cpp.android.java.demo + +![](README_files/1.jpg) + diff --git a/examples/whisper.android.java/README_files/1.jpg b/examples/whisper.android.java/README_files/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..035cc105f3df98f282585f8507f2e7959b84b4db GIT binary patch literal 68988 zcmeEubwJZy+wcSgMU)bxK}LrNqf?RYP61^ygfWnA5K&}wH|!<{3`7_ut#nH_C?ll1 z>zns;*Ly$D`@YW??|StcW66Q-Yezdz5x&aZxl|HU_!?&0W9>HxqH z@4qPj@3L;buyO`tk+!jK7bh%ptgtVz@LlVFz#czg%YVROKVc_5ohMkFU$8KT^;YiF11ux}cnIJJ z2wzPB-U4uOaBlqD58+SDn>Vpvw{8>OxQ&HxT*Jk~!@rJy10Vm_!hevu^>0(WapS59 zKzajr=UN-?HD&+~=`~!^Yga7*daUbkaBzP{_3sbCjqCVWZZ~nT#ik_K(rdW5*KqL( zvD}GpZ(hT}#RFU?CBrAbPjHu+{{{u+V{JVupny(X=D^VKw(N7qfS6bokZyD-tB}5H z`QXiaf=?`+T-?wVpTDySJAc^$zbk7Nc@c;uPKV|Cll5PCWB-E01M zlNxR#O{5I;S%f-mv3A*X9`Z?exz||;w)ACyl){#6-LfON8zk44m{GQt1Lo&f0A<7m zSC7IgB?jdw9>XsAeDzG1yv!%6s9f(?V?AFDSi4hF-U{6Z8AZ7lDFGw>oM5GH<6QCj zZTkq56>-+kcJ;T=Bdd@w6Dd?2ZK;VTmG>)gp9q$e=3Np)D}(#5r=r{!3v&C=s7{PQ zjp0lDX(JlOK6D`{o*>J;B};VC&fGEHG<7 z`Lt0M<*imIB<5niSt46RcxvZ0#S-&*f*d^P`5!-8cE5Q-3kvs0~21pN7ujeyG~LXt)=9qWg%9>*b+Z7_VsJJgxs0|8@p}e zX1ba>n7m4JH_vSz?26(iPyZV3)1x4IA?+i>u(5w;2SdOknyvs4^L~>{jkt~a?DZ&{ zhx+B^g|;RzW%jo$`Y5HZYvL{d<<3brlgW7Vr_=3t=QrMVk1pSFA6&RMm{+7BBz*p3 z4qaNxXuV+yvZsJ2Vc>0)vu9EvJotB>mKfC279;mB^8;0aKE7#)w2wzUW*MDXx6O-B zcBpZn*I2ie^K!XkaM$w1q``}blr(gzSYO(A)m598R%n4>SdWIIiiJeAeVqcR#?sO~ zZa8##;vjMzL)GSXkE-qAzf9B})NM`=#TU~RTjTs~09Sxo%*7Recr2}`%WdL4;(heO z1!4*zE3J8Yv06|1c)#;`iIXo3T7*_$35HkblHVM&RD)4%)N&f??S6R6eH*F0`o!x_ zY5Dx^`62{o}wY`Y%z=Uu^x!W>Zq))46(G4tR6Z>-U6) zKV(2^X|vq&z&=}Hf*2EPwA&VA|CzLnz!gBIe-~8g^&s~NH>Z{2qpmJv7P{y-5)nhQ zkdNC<;Z<|OTkvC96Ld)YzPAtt>k>I>M=#>`wXw&9T!yJ3UH4oTRMvd$(}r$67`1pU9Z&RMGlIZ@5PU&`wOGkP>M_&D6E-O1H|jq#}*=^-0I% zxyxydP0;65+xc1*5Z&rr%+C<@JM1q)@Z5xaX?lU4~Mw8G+9a6)VNqk^zD3 z$ZQUQGJz_7l3l96U36Xr$M=l7p{B~L2&0??G@4nHM%4OUy3`^W2X_Z<AAZ}(qm9S2YriUI9oF$Va8K~$3Sd4tMUK=EO@Rz}gkeB7!A0P~a$OB? zp7#lJ@GC%rjMyP3zH|C1B%P(FfXGR0T|^H~qX-LH4q<}8-p78FmRxFoGIXn7yMh?v zC@aCKVal3)i*8rom$0^{>fP?Rj?`K{WH*3P@dx|+nEncAa_}5Kd825HCUqr0>D2gm z>L?JzYiQTy(yylJPUHw4v2R9p@r+FHMmpLqC@~JVo8D%D2{T3Cv$3Lv{ZA>1rD@xDp~G+TTfW z(4SZ3GN!cIa^FU)&c`aq>KPI;@hwK~y5^glAliZ{2!T*owFRen&`G2EClzDfUP->Z z9`l)_IwPrK6)GNu5}-Fo-53x}CK{haa?grd)5&M3dpY4pUk--f^8?t$gx^#?3nU5; zRRDsd3T!4}v^`+z2@^0?6PO>Nq`PN&;5Y4iU$fU?8JezTJuW zJAKE|2-{1r>YNO)TepFl&FtLw6R%Is1^E5bTAdz}UYyavmn%JJw-dp5iRlINsp^fY zSB?u$JS6fvG=F0dnZtQfeA;LawLqL?oR*JfXy;ZSO-E~&sfH&ed%&A*5>cruJ*{Y3 z^acOL;GEfMDn&gxvb;P*KUIOIDaG*nWa={a{IqWdPoKm~QL$*l*<*ZMpV)fpgOWVK zaD$QkAt9YN<)kbA;RRA5!X}(#8!_y0vV)W_4&9#f*}048E!@?2_)P}sDCAPr>cywN zk!6M#!-JKPDj|bIDB+M*^G~~z83rdU4tr?QwE4=E?PPs^WSD&EN&8XcS$o96hZW5@ zX?X5xcY)3RK`QgG2-G%Px59LiEn>^At^)G9Ve~Fa1Vf0w*dF7jv+9@c)to0CYWQft zb~Ym3yT3pcteZek!Lmgc6+9^Zt@*A> z!N>2wP8lv%4kj0O^p+Qulbg46Bx;X=_YFfN+^xUdk9Dl>+#7a?-&jNIqcwmzlOINN zVr(mOql=_E3Y+<@qH%JpxZ2AllmcEmgambc-+w0~)M&WTK6v-nAQ6|$&hU(kGf5m7 z{aRmsc<7b~DXlUpMnAQi;-}*!-!c{%N`@!DUGrX~=SjIaa9f_2DV%MXVR%rJOR=E4 z@bz=6Z!A!d45wN|-_5zs9b&rHi+{wW<8O>QckO+a%+>FO)BDo2Ziy$%+4OFZl1pG* z)Z`R^E^ZVJ>CW)*^$Za8-vkp8ALzB{vGHDBK>Qxo|B(29BCI7LpuYv{AKX1{-J99N z+J0+oziZWQB8EB%=kqy_vO?l}Y#UfQ>L1m9Z<@t)Bj;_24StG0Rrl-)`@2!wjZgk$Z-P*l)~Cq-;o_3a zW#nnIx@}88KKWoeTG8XF$Dy^M@eN*@@DTc-&DzQ-x2oNmmBr!`w$$u7l(kubAUS1B zby1lz-p)-q<>xI=yvbCVf@FNV>8l`YalTByb}+xI+%JOal6il?Cm!3yO%9#X8K6lF zPBj_u>;e106f9}Rhh{>-jPH+vI)^3~D%juj-2P&_+b*cSgQJutzA2NFlYiY0&t959 zI&k0$@P*=+DHQIim(S&x+2EHiAKNX3DoBp3N{CFe!Y_!&UoVEI9j`?`F+MwS9VOQCJ zV4h@F5!|yRq6Xm&TlS}fOghtwh52zKcB4;RAD}3NhM1~U zRQ9@AN?sVaByd7I&DP4fhT3`TS!Uj5!$y?MzI@F0{Cg*yv_QAf;}fXa`4YM!=h)LT zIn-e7n+FP>OjE1~G1Z6;F88y64)W>sa#B_ll$;N+^h(vF_y(HTzlJo2xOnpq-sz{~<<3q7`up~6E;||5y^#M~d*jLQ zT#_)Kj4jRtd96>f`*>TElT&Aqi}tkLPp_9;ewgBmbC52WZqIApqccuuZKK2RHGb0N znUT{*dn4QQ<)h<`%9GNkCr_Ft88UOd1?NpuCPv82FdHligT@;o^*x=N+edoH^rK@d zmp|N!^XmWJ#i%(RHdvGat_IMK9DIE2o%?kmN)by zX2!5b$;;Rr*0F`Bu*p7~S4q_2?*Fa-V^kvlbu^8@&RTgt@ySK#stiFY;FHPac0Fni zNr$Wz$l$QH_5NkClddtZomGl^LpdcBg_Rpdf?$Es=tW2fJdHCg38-X1MN>zb)eTfy zGMo?07>WJ+IO??dxc})u$NsWL8dAgAro5+TrxjZ3_9M7RPF8<77rgQ0pL?0U#NWG_ z*x$$QN578U%c6hnU~YR;_kkAKZ?=Kbo;ijsQ_+FN+6bun90UI6GpPIcU$NH@X06UIo{DT~{$F(4DDbf=@Tbd1e^8+`dpxB$vx7vDH zQMc~K)GHlY@pl!zCR=Te3lXLj#uM+Yv~rz|t1_(G(C5|P;`H#%s~b7%5T2Re%yK86 zweS>SP(+B;|BzwtEZU#V91gdoIpFN!W)YC*?zUBYn01&eHG*&4;bc0BGr?GW9nH;df?JqZOZ$4l#u)S z*bj&VSK(__5E%eKrVQ`|${279fPg4q0awRr$7=I@Q5Y^KqY)Eb8R>cYfnVC{4CR~g ze3&MLx_oNp6DyJl;|_2+EgXOK1{#9(?yX;{o-_NL7m6THC@V1Y!`mQZMyQKa0;sLN-yx^3ZO7-vo#6foYr&UIddlgwXLuOe>bwvv2(YN zKEXHks^GD$=}XL1&3`BE%iW`%eDdxay;@Z8g5?hh<=Iz8I22WnD7>kq_WL{9^-}XK z^47b}(_4yJOC@fy7xUa~;fcO`V?BnfiIPySZPoCV$lmD{U{@1?v^7YX%~ohFSg-Kp zFqp|Ls$-32jSk)PzH97YUbzSz2|tOS z3uc{35ToMY`sF^)58*kx1Mq@B?`{5rbQy!8iWvcFN^ae+?CI7J`RiXLJ35G>_oPN# z?iA^U5;T>JcS~fSzqM;f&s`mLEoRpos-PK|h-n@5kT>6dx&0YkRt(SK(Yx*uhX?N7 z)m!oB;ETzL_W3%a(Hu<#c&82~Rjqt_eBGaS`yUlD@7PV^j26u(F~wF9OrjG(BL zAgiU1nA%7r&y;?QUO9~j8$IY{;MKRcIl8p6@tE|uUmhz8?+!|MOee5TS*`GL;>r50 zuq!|p^6RIX$dcS4=y(V!ouNKsye0WSq9ah?qb^cz6UaEYx z+bw)UQ>)t6XG*&&rfPm69+IK(%0MVWU@)V@=zM+Gai%i4aof!p%qo8IICFCJTbQzR zSgb%_>UC@L?~qv6JGFxGVBMNcFcB>d(lBqUCrcvuYv+AAYtpvHc;F7fTWIynlof4w zM_*B;40A^En0`$Ly>zPznPFDcD!)bHUgu_N;M>Ra61s9aR+1-e({VfYTu#y_lIJln zXdH5Q;ghb?pzUU)-{CopygpB92}gW(WRziY?xW((*9#A|f(>&wXqLXAtjC)Vd;6_Z zC-73=ZKO&{+&>|yyS2OUXvzs6OtrCd+90-1FkGT$siuC$1miTHJ&miyw9tD;+^jjF1@f{aa&=LpMqbZcI2Iw(qd2j zSahAEXHIOiTsT!Bai;^&|J&Bb4I%NFCs`(=Ywgnr#Rz3ey{Q1#&6q*^x?%eRu#PdV zTk|GP4u*Auc45TU7gu{sr{&2K%0cHaZAYmx^W`kIeQZtLH_%$Rc0o-(|906tboUjS zPh)&lvGlG`b~%?pk!6hPJu3ml8zga(hH*VRue(flG^R)^@l?YqP8BbSLjn5lb;ex6 zxpnJ84qQ+~FUrr)nZ=ArocT6M3H#~-^sRw*A1?E}V9tt-VD(0-h+YWcchfZcA~8iC zzOua#`J=d~B1sgNKutdMU#^S&%S<-n7y|R#A_e8%YCWZ?=G_lkUW03CM<&|73BXyy z4`EFl#h8fslLO&Ki)_9hr*;}{W2Xr1#{9@gF85nQ?<`))X}28OR8icZMlpI{Ja>rq zA6Eie-jE=9j7Opv^%(F*GHk}z;2=?FbY0Ow7rjBr41X3PFBkguJzaM~56-hqsf7<+))j`Ihu_s@2@C z!d!`-l-00nw3aOh-pX;?_OoTggJ~WT3aO7`F+lCkDrrer!2YeVtt$Zksm2W$D2-N~Wo6gqC~iA55-;|`nk>9mW!V0g6E&zo z(Tdx0WeHf}3Xq!im@L>wLB#4-n~t3J=2OPb)4C;TM)L!gk(@DL&5mtM6{npXiYn?= z1c)vwVA8|bQbSuHp7n_x6*r)whB41XxHfyO%7vz;UwH;e>^IG4JMCpvALDu38s991 z1aD{OF=@%PEb})Lk=@hwp-~<$o57i2wlsMsPF50mmNvmdUuC?AQWS-TJ-qkzwRHTm zvaM^Usk<$!yyj@#TK@}*E5K;8$&EFh=FgL0szv7G>C9)v%Tdpiy6+YHWG)Y!-pfr@ z9=;!{74bEqg>XRaBKG?3iY40K+xIi6BIN_n z;97Q)lEQ9L@HOW96?g~UV=zPWWcCqQMG#!&GnIVE4O{?i1<^Au^tzbX!tyhbYkIe< zhGY^9b!;dS(s`d;Q1X}a8wW`+Uq6kZjB-|LA7Nw;{WyOQ_VIy#PD8$Gq3dQkYdFJf zxTKVbpe${Xi4C+ICFCi!U@(tYrr>G+eN@jkMMV3t7bXX0J|^@Qsna_}pgqF+a@8=Q zXQrZ_!;|{Hq9aX$*3$^4vhX7&M)UC9=rN`dS`#x_Wg!uGCKObtew@gZl)z>_>{y_s z57g6_h>}{GYyHYlW1C`jMw3n}m0FqbMUyi`I)C`w87C zzsYR;Fir76qkUyY9w=)N4MCelKNC>i;5HjnxNiJh?>IbnftVx)#ZJ1}dR*EZ2V1C;%<+pq-9i1Rgj6A)dO*-ufOu!U1a9`& zYMc32`pM?X)Mg%KiLi*Cri}~3-LI>rUuTM;T4ZC5q;FdA>u~r<4P>J@G+Q>QVww_o zmwlVY*&^eo5SjEuBHPeMZkMG?ndG!`pWIxfU5RJ)5%H)(|mgDDYV+Pc`9)wY0YGV(V*H9NW!zBZm zd6OR*G7>Uv`Z9-Z10%H)6TUUypxF*8*xHP;I= zzJI7>f)rpO1k1f`6?MvBZk82cb!%tWnc{~`o_M+6FACVHmWOM) zf;V}+X9E8%+ILGf6L=8r8{dF?7(PdwgiQ`1dtWEW$I;pk2_|mn+3+wOo{jr)-LwjN zZS%55ce&eFr`R?u7UxEshix4SDHJ2i_vG=kOh(rSG_nYYg_{WjbzQxIPLD~aEY`>0=TV>^la;rF)iu+ zX@_2*-FKtnKHYozXdJbBwM4<_$3TwxGAhIjSpcTG&M1=L)m;u_Zz&1Os$8AiB-Ri8 zMT%T31LYc)(4AL)cPQSJ5@DyB4BOj}1-tXTRHi8~8Lg^Xk8Op$(ETPapO>e3Iv4W< zm-f{u!R8^fp2wb@jKjq73XO%srHWb&cB5!<;~3$f`{mJPf>3gqqG;t+4&?m!KF+@=RPUr8^R19D|FtgJk1~OJzgC9 zU@^R$V9wGuDCmZ3AU(xi_wI$|K-DXN=QW%w!1rqhqnxJ+x#2V`0xS9ne0GJGt0n^I zvR0;<7aN&3*`~8Cx5%9^%uDe6^M=zh!+^egk};mHQvCojgO`)M>m_9DgdbY9E9IRS zBUxoLAD+jZW{8O+P+nd`#8zWVqnS5agAbEnCffiY#H)lKR-Hh?hj4ZNyo(-au+JW( zryvTwTk6I$6K}Mh29^1lSUZ;Aurp~u~ zGrz3aA5$Hud9OI6n|C1Pw*nK3_~foLV$^mySgQgj=9=azOC{q@0zd9Fa)iuchnX){ zSAeC?#?@1q?Nq_>hNm-n6_imUh{}*scrCy8b9tbZaP^)v0oh!Fq3?JMmQKMb+TWyV zV=}H=Dx>F>J0(jn)%=4F!e0O%Hyzt4IGOCI*#OI}jPt2>Sns|H_d6 zWFGg;^Vfo#yi<+&-0L36Qt@Bu8*@Ujq2bDBG!X$JClB6Ag3F$#>O@!Y*Y3O}4g-s->r%Dh{!=0t z=K)jibB`wVXc`J|3e!&>ySJagtn>^X9SwGU#q8OJV67r;obMNjjz#ByWX!Fp!- z+esU_pMahL554t0z8Sn@d88`pNeTQYL*JHA!aX=xr?b~!$?jq@Kne<*A`NMChgcI9 z7nt<1S4~{#vVoM|561{u59S3{^nFCDw7b+y>5m&};InKf3(2}OA9my%M zgKB-;KQD4$_=sKs=28^fWW5Z#2r-H#3#Wn~Z4xFQjO2WxfDcOGxj`~=9PyPVlpq+M zpoa?IPJp~?_fXq!r`5xU`0=@kh+Q>D^$+XOfd$j5SqT-8N15=vhT0~TMZJSytvvwS#k z!$@4Pmxh|CG@#!}($gnX$~W@yo15zX(1eye!Pn~J6Qu4jmKOz)cx-~w%Zo}4+nuI~ zFGq!VL6>o@4mHl?qHVyb1Z#un3dG@NTyfZ12|KI7v6GhDnFwIxV(#{3-SA5<`1DAT zjcx`RzdkbP_HN!`@YA-V`S$Y7lm} z%bxA|_|mAO$ETl)%uSrb@kI+wNt8DjC1p40Ju9U)+slArO8xu{t1aPGv`ePN7v?G4 z*&0Ln!nv-N#_N3Lxk_ALmS3KsmFWUMaT2^$sjEI~e_pv$!Z(njfJBgjNS}^Ql=nb{ z!PJ$TOFk8B!c~}U%rQ$HBzh&X@K$~(X&tZ5h8UiRO~)GWGmwwv{*YA+^*v|0U-+1i zU!oN3Pik{f>L#r#V(`l$Z2We@XZX3f`Cs->wFwMk+bd+8{18GQ+AA{{spaRAQzao4 zp=%skHNCm9e#jD-C_U}Hx3N3*8dJ@1IJHFk;leEDR`g0aM`gC}w?kxTL;SNhjnd;W z-elab`@X#k*G}5_^o=lKIKwcPMt`I~)y{67-FE6JGcc^@2+UQN>*iL=9<;v5f)V6X zlQN_UG$|2s&-Dv!OIYb-R@1F{qm&b2)MRWKtr_&~Eo@QYTbf0_bnTNTx|v8pUZ$!l zza}S?lFCI&G4o-%k#U+N*pqqQ(0A&4bcSiRndnHWW$eo$4Ur8s3^{#L`cSPKyJ$;n zfl-l(hZ<&Y6Y-e?BUobOs{rE)omROV{$jgbM8DY%l|xszm`dJy6d z4T?Y_mGg|s87E99EkBtwE8q&MNFw@EJLm_B65!s4js+ih9?)F&K~-hlBH?k}bMIcn+<#6CA-NXA zt`mqL1Ms+a^K}XCZ>dXY@D`*eZM8t$x5H;vdQs#Q8KBRc7-ks3;x+nozrOeM;0hxX zg-^3HCUPW+@aLrdPwJaKrTVET5(6b_5=Ws7Q__gs6{Jq7I|ar!ip2iizFToV1xi2?RSA0%+c z@lGq5i@i+cH${T3$IuGtheb(-(7VCvR0^9deP=#vBVQsoC2;CZCnKWvUDkXv?q%0f zbj(uxzVZi6>eFUY?j<07N#8RhX(K?Y`_9Au>HDjv@2w}!F8W)Jyr)H#cCG+L8?6oJo_>#}e5)iJQuFLq zRt*hGy)>XN;qWc^cg|7E0dhK)j$4b3SAd6|SAdG4E5Ljh!U^ZZ;R?_r;J@K=1vqXN zOWU{tyf4PC#p!m*3{`zQ*>?=Hp63(%c@XBG(%JPeu=G2xQS_>L?p;CNPAOyiM<1*F z$~j_`dDvSr!O<8)!guM3NvztHL~i)CHWy$EHDAz@l-1T~trKCpVun@q*D-`WX3UCV zz;r_2C+EXJ-g08>mdVB5XG7$-@Sf<;gi3Swl%k(r0iaxg=NF!xi#`#jW1)isPvsOw z*=-x#AkdddJ!xt&hjNT;9Ec1y^~8ilJ@R<>X@8#S@wt1Lrd@0G*X?z zOyapTenI@G(xCgw6aJ^92(_p=$rE<=LT9Szlk;xiDVENF^Qifj!;sep-8EfJhWXe4!#OZkRG zyeEwyOW?_qxRKoq?2gSP>IzWSc&>N_FzY|uilL(+x^+MP`8z8aYD&R7?#E}}Jv~p) zPfm~e^+US8p$cmdAs>m5XHRg54@sO&Rin&nOlq88UI7GZ`6(tRiq?D7T1>1-ZPM0T z=0#;|e)Pzr5EWE2Uqry~4?SEM&r^u60J40SG>dX4LaQcrCRPNc!+S$9!q&br%Y=@P z>LB1L4|8%3Wr75{qx%sp9+Qhj&u+g4z&_%BBWQjmO?&MdaS+%qRR65PKWJ{1dCCF3 zD^2}CyQ8E4#ytqV)Mu-%oE2Hu09!&ODn&IcoqG54hK$4P;o&D6?Jn(3w+_(RZP~@` zY7BfbnscPmQ%OavHM@p!cJBCMF(TKMo>zVgEbZ_fzWgQCa8O9fqy={GW1W0J^2w34|qT7rS-59Jb6b z+wXjUyaJ#iuK?OIm(kJpG_g38t1<^te3z1+_yf{qvw|K_ct&0G14e6aU$0!OyaE`U zSz8;*x=JP-@9|M0?73~P02jhA<;wnrxU@nXh!Ls>17j}=yz76Z4{jkGVgLH(DGXW@L2#inKnV8$X;^gqSoy7)zN|Q zA!ut><)I}zc?3V{^k{@?1o^~EM1KX5PkIgEmz7ibSd>K!Pz4>L#&!14@U>&4<5Tr7 z(of)hwkxDw96L%js`lHI}w*_mjjQ%10ZIRDAuWbs4n14&IM9#ygId&DO}cMNSX;WvUih)G7n~zQ z=?FK?b`Z~y7DXY!=n^QhJYVlBPgq!3D6FqVq30W*(B$vtx91)6LnB-mCYD@LY!g0^ zItdev=;%4@DIWUJW9hL3@dr`0!HPh8eAwfu&#wSMex*rCJ%nq!umdRepm`3ZD~*;$ z<-r}TUI@nmT}fYe4Jc?LJ}#2s@m!zPg9Aw#Yn{o2y>^p^?7UHv(IpopwTc&xyv{(o z!Ud%gG^|>C`FkwmfLEhi$pLodg!%MRW?*jzyFojbQ9J44RB}_BMg3@gwW#(A0LBqv zw4Z1}GLL>pNM{@FiyL)W;6#m7P^=c0j!>Ee}^T4K)8`9-?`tK%iBj+l*B5p^ei+ z6pMCnw|~hK{z-WvWeeb}@F~|z)MtHtz|?Cka`1s8gYac>-STRM)guZDZN-H`4t++8 zcV@5m8k-%k3%HD8xAtDF>Tr7ZfeO6WTNE4$GCJX{mc0D*wR>q>?6E!bZ0?>zGGWW7 zRwlmc2RB{s#1?o?=%=(LZ%AyS=(cCx@w}mR%6y?Hy~9EraT0sLUI>MnB)l_JEv3&x z=C>D^rl8^XOU$c*VKMAA3~o{tk6IAYfX?k7+dG_-p#hR1ZbJbMecV-Kb7Pe!6PlV4 z{N*pB0~g-PKhm6(ea$a9hw==rSVP;|7@Q$(ed2mH=Yl6Q#Nh2<@@yLnKYXI!XMK5T zA2)93@tB`YfY7&hn36)MiSM_*M)NIB2TkWeZKoi^p?3XtCJ;3F#1|+$VmzZYrAF5i4sQdx zW8$s%Q*#z_KiFvXd78vNuC<*a5fehWCwRigZ#TJfND|-LrY>`_Fc}%F@b$W_^0L5N zKZ#F2qq)d&A^=rn2XvP-{^i9{Vzc)wz9xQ(GZ}?0lTKz=iL?y;Cs;M!2@`!)U$LrU z&@;SJMGoB>IH?8v^1~zSkCKr%n?2WZlm)A8%b1RXS^l0IB$X=7p{MP<-NCZ@YYN7i z8lD`sJxBG3AzgKYLL8|j&uwrK3e%1ZkbF3@o=PYq0FiC_I8zsUn69dIz!e`6mzy(^ z!R-~_bvIvLg}|6iIYfc3r=Yv+<2#-WK0vLHW|OImwYj8d$?fW$aYyXjzkSzZ0|Tq< z3-OZEPRE3ZcG8jshbWzXp2z=ok$lV9K99Mg-cj6rsIJEW!RcG84wea1bSBTVlap(o zNJ=|{We|?+CKuCrC91H7mOLg|J8O)(tElU7L)d0ER^FyM!_G>$q82ny&S3ZpPoOb!U!HubN8X-e{QG=%B9}cQ{;^ z?wQYsCVG)HA0nX8lJoLyz&2h!?Q5Uc{w2aT9VO~eTe&gF_zb^qek*f&ELsvI29I0I z6{?%!hqu!$2T8CqjJTd~cva+%ea-(2*3Knbo4rj``8-0OS0es8b)fxYJnsf~YAa_d zar2T9ljBTXopoA_KHh(hO->pLNI#*JQ;+ng%~<@d_uExmAkckT+1^x_IIxpD>A3DWJGJNBB}j`LJJbvtL2F2zpVhz0(6 z)`rbFof(`_zTLHB{9%nnm${}as=2>~zUnCM`1uYV#1MX|;1e=baYpjy<9I1yY)28O zn=Ii>rET@*Wtqrj{cGHj{mG-b62D~Yqfe*wGNC}yirnNDYswi!AM03z#gUJN$#I&a z*sktM@m8bxg+Tw7t?~OLF(`oL47LY@x zbAI)wR*2dEfavvKATGQ3US?Ex_?HBCWIEc(5v+cx=4S_6Lp+y9qJLVzwXC7gkK+QV8L8NMPy%l!KMy2Ywe%dZN_v`BA-u32Ce$A*#NO2uzsXn;W?;wHcHXgN`7e0we@0Vr z++VEjqq)nEMYPBp`%SXGKW*?I2tw!mqa)a*RsPiWuZYM$IYY6Wv902-n9hGRxau`C zDZO*;GYteoWaye9U9rW3zQQ0WE^(`XQp@27(Qm|VWU?v7C(Z+>d7hojq!H(DsQCTi z6#l9Qoxpnu%B)TRKJL1RM|022Ca|j>r+ZHl{mERyX;%z2t?l_ip!C^};CCi+0N(oi1xMzx|sEd@8H~zN*VXaKNRWzK_;`->(wn#JLI=9ez`$De4yBx8S+g3GbMS)0j4n4&|Je z^pw~nZTWdOm==KFbZr|z3?R-YDs23cFj;P@!(3Yl;Z8=hfogXY#PF7VHrJ<5fPX~B z-$Ti1^Vf*etNCv{%m15af2#AJd)E5j^6dXz$fYr)R2-T9+QxhxF==70npc3t-@Jy| z%fu_VZmiK$bi)cbPp#34ou>i9Fc18@G1;h)@{%IkO7&9h1+8l__ew0EeNbF{id2*m2!c*FC17uEX^zP0$5(LY z8VH7P%8Dl+#yPY5Z80^iyHocr(jg5s4N_cl)XX8wLw1&yXq%Z*NoEm;IjJsQZ-W6+ zI&bI5_R<&Fy?tqa$c;;X#N{W0i>ITucM6PzcxAm@{H%=iSkLZe3)dHq_%t)yw@LfS zPsspP!Iw7-`~7 zmd>s{3>o15VsSwama~9V7Z&Np9dUOB1TVJM2~X>7%5e4DTMf()tqVbQRzpfZy34t% z?5RZeHk}PfWCiI^`C4gAy#S}JEw~D0p)bv{gq;sjHsPs?*6HL)=XavXO-&gg9&SCF zwr+iHU2EVGg<-Z8cyz{1M(qkyt-=q@RS?1tY$aTQTZJlhiBY?`rVgS9UDoOboI=2Y z+XUm4#)3!bz@RIDV|S;^uvHn5c8ED1GP zrkttKP%7*}`Udm}2mZ)S-me)$tr{v+2nuc9GI(HYn`i8ywq36$75y~x<(ndw zd_p&g+si{Gu%w~9k-Q9@0m|^<$%$Te9}`$x?L3=k59=btQWy*h>;InSzhWueCs5~g zR@ju12dB(-Q*mWM<3$cm}SuM?>zL&M@LQRWEa-dk&pKUivQ#d1mX8I@yao@$DGDd1>JuCwEa1{T+0w8r4FjUaDqcoIU0WLg_MOAoGjoOX;kxp6JFt;(RhuoV7{Svxj)fi`ixoO=KR& zGf3Xl-O!gUa&~HRJyVo(MwOPSk|LN5h2!}6(cY!#@R_~FAP+wqzLQJ8BFw=H@NP)R z$x!6STX^tq!lTKPn1MUD;|tx*@;JB0cgN`)q=gt2MgwQbY{YXj2IZM81|5`bjZZcu zoZo(8N~nu2ok-TkxbA#gZx>IigqF~dnhk~TNr1#Qax~V~F~Y%VEnreH$Ygc7A^o&( z{oIT+_ByutE2mnYtOJQd7_YjpHGIF&X-(K${t7_L#kemXL7Et&{srPO<=e!7ztZo% z)>nD>A*Cwcz&f%%M;*eZqoXu|x_6v8^CCQ=J(Ph*l7Gr4%=sN;vI09ji#Ii4vvxfM z_hawQgl!_A4CWw@_K8S;|4*FOO#U+cSAeI*jqFzdeTKYH=jEFjo`U0)yQ)BRw`F{X z)CZLPMn`FMPDvGCSJ;EgJJaX!i*>YS5$C&_&shq9&hA$5^aC-QhEt~a3B*L;IzK>w<{Bk;5N@iHx zYyZ^L%zkn$3*RG4-JoSisNsb?Y0jY1Th+~MZaLpkc2^0yG++5Kdp9qAB=7Z9n)GXK z923#rkvaw`l!Pq;H$`S{T1^v2G%bot?$E@g+HHSGFEy2)2kK1mo2nTI!{{LBiDro% zn%%JWJ00mmNpqq{$fpYhXP_PS&kZy1D}Z|%(l2Uah{sUdtF2~?rOKA=VQhaY@|FB6%w%>~|YX#YKjOf{S|Rhn7ttKx^wN^0Z|aaH)M>&Y-1;hI6|+ zkV6unAY>)a@3X9mOb~&%32QqqTvm7Wz%Kc#p*ZVy%?XC&Eis!3HDT_-4x^d zO6qi@sSt3Ocdp1rnw8Ni%tBrKvXr5?U>tc@Yh@{mE!#G0_hru3w$cl&-m(_Ad7t#d zqljJ2PH+8B!YLcDr38MbFE)Dk!C#ARoy zfRz7b)u}T))sVW#^rCi&l@JOuwWhzQpV_$}JLC`B5Y|U5VpmE9vAb2p%vXR?^Dgd0 z)ZcRuQU0X<%NCd7{uk!{3H~UV#Qi|3HqV6U|9FfHcb$^I0rbPL81#65ERzjsZJ1&Z zmV5DJGLsZdX>`WPFo-Wlc$O)_=~&9(?ANC4OEWuMw=9^Pf|)Wdx&r7yqCw$!canKxPeSwmkH&|FpIh0*|XausawTD$Ma0pFnbFhZzJY`W5WJz z(aG^tUHeNNH*2a&{^s?0j~+EOYd`8;p}Fvh^pgHRnv@64tkk>mTFi$v%a((`^Pb zTT|YkEhX@6(=_V30yMj1RTM)GTClqbjv~cKTgxHZqbWZyIejd(WVjQdsRc{9V5!f#g$ zawykYJ6lFzq82vcTA5*=Iq@GE=yNpN6U0g@8+C)DZCcNBtf3ALVsptKTE?b~gZo4w zpjHI%>9U_Qf{0l;y^C*j5zz&!#cp(> zx7GWu=qyW!=)Lz&5QJTI1wr&4o#;gG(aRFiq7xFmA(Znbn|vJ^@PrK2PexMl zAes~LVqnsqZjaA=C92lvj#x)Xm5FU=713Bp?>vP{2A*6VXXYp(W(|k_r09WwFD<9C zd;5@w6V0pPqsau^nQHu>5A{>MJCWzU}N}w)<{n282Y@bI*^BP$z7f6F;+BSW) zU(_N>CSUiANv>6rKTG<1xi#&S85pPg?XzI7VxrdQP5jdh`kDCtd%3B47tIEUM7xQu zwvm2NEa4hbLSRI2QSbzR35+HXiREj{m!&zq{Vq8Q*`RAfY3iH=sDGWCemy=q7CgE^ z5vS?R65wtO;}QS#53v9K4mF0q!^DP@F_#~pXB}YRM6LO@)w@MSLu`(761tSsDXJ)I zSex+lhM_HIRl}dlxg)0B_M0jlM|+19cRE(|=%uj{f9BPzLnlq=t5KVsszKd9tLeuRRDSKJwl!>{3Z0Y=I$1D@Zy*_9wXSV( zBxs9qoO;yC$1*<7JwXGLo8S+e9@;mZ=T@DhIpf$LdJPiyBAa_Sx(G^+txY#OQCgM_ z!Q!zN3GbyZ9uyP#f2F0dBLm{A*K&}4+qPDl}x$Zq|P2ckQsFCTJ%69k`_Hel&RpIVI>(ft%ytEBEGF7nW}_Tlkis+-GOHOO|&&=JP{Vqib2$ik+Mz0Z0Na_k&=%t8`IIy zHZ74mP5qqpb*A12MI+W$!$!dzL2F-?X4c&|X;z!gm z;B*fR^*D>Ofr*B!`~zhCq3O32Oz?;^llS+LbufCJrVEn@icRBDcYo0i{yhS7z)u;( z;fJxkLm#}evRZyOh-N$4Bgt)Y^4ze)`dQ~S%6)#=7F#Q^h}b+W7c9fY1!)zVQTh&b z(moI&o=z>c3kGGvAG|R>=`^j{R#-F!{6UgMYi~Nlpdb`9(WA~W>yr}l7|w=+&b1ZT8pBQ zuUeuDIbf7H#6nOq`~kw5F;;J;YziB_JPP_8 zzoj~i92&*B8fGyZ6mZw5Am)TD=w2Q4%l{q(;IMSz9{ht>q^=+p)njz3K1o^I={*zWwS(JHY9caVPbv z=~2IhMveG`h95;i)9>;H9+EbHJFR(N|KYwbfc=YU)qgz>JUj-=-9D7+Ss|AXslR;U zbmGkB^gh9YWrxlSWa(W;fQ3Rkt{6ghh7-K$Y}OI~P*-7(rT zZi7Fvt$I^bdatgz;U2Rgr6d!5W#vFMW0{(cf=NXzr@H$@OICcgmqOmd8KLtV4dj9~d#V23? zM+iHbf6mnSLYk>`S~%eLc|ZtB7U(FW_)1tNe&BzEG%MC1|KrDLq?fO*5n(O^cbPnA zWrj*`on`(zWcWp#R~q_m;{BfInCU+Nf$3_`e?j8*KSH#*Eh^fYjsj(Li|j8h<VGPQE zJ2U>q*3giW5VK*PxU!sH$=b$-#yszu77I^{s8Q!$5PcP#4Mp{TJ}}1raT3Z*{vSb` zaAJ$=G}*+}AiRxSP6uBwShCn3F)V488o#Ne7{Bmn>?0a-g0aA}pxwUGlG zZ}@tyd~Bg}kvTrwG!$ncto6TL1uoL1p06k!c53?%N3^H%TSeWXMz~seCofGos(+Kb zJp7Y=-}twxzJ**Dx?NLlH)&-1zGKW>85F_%s1`sd8S|L3jt}rD@lNinPJ8+ofb*&S-f$)%Wcgt>fT_ zV+vDtXUJFWp)9QUTkMgw?LdO$*MGSTD-o3w#S`=d<={9YJm36~A-~U1qh9E?`;`2H z|G|Q@$JtJmLU*Z&y{=F>xc*33@Y%e38;`k=tA3LTQd|yL)%MI{6I)O%v}vxj#12&^?C;eQoBfV5 z@pi7>$&UIlX${xD$v%0lM5?AclpSeLfF&Tq5%i`XZmCo!gYDtF5By-lo; zo3a?%UwiZ*wrC@U2G8t^em662SE$#js+UCFT&0vaC}WLC->;iRqc7Yo*0#`>$iqtK zNodvcuO=5`6UFU#*;KQ5l{&Cfh1@qYcIB@=MVwhxgsqsRB1e8;8u%VBH?3owMUe?p z;A!UFJBb^x_iCBL9@*>z$5*eQrr_xmO@``cidXuxepueTvI~ioo?d67{*#OIenOSL zT+~c)(yL6Qgy5QW5iKHy_lsd+f73QEN`B<1r|%3eIb}A$5dn#cmL-mPh$Nw7o@AO2 zUjrtU^OeeRlSO?|=6s5N%@72=BB@Omnh^{mEa5Z_vx`YX+r~>Xd@c z>vvn7fG(rif0sWx#&(h5JCCV-m&r7nb+q4_w1?U5mQrvs!1lwy9{rjgcVt|(hQX4^ z@u5K>N5S&)pHd1l?sC`z#1`zuILP4|__QsX%~x?pvGaVtR1wK+txg2U?@Y9G_Y9Y| zT$@#Nw{KM)Y6;$xP;?u>nYbJxf!Lk|$TDNS_07k42ZgunWJ#HioR=G9&$Ow1+*jZ} zb=AR^aem=LK*Y|i+>;dU0}6q6PKuUsHnbRUme=Sx=yMwwn2B;$3faWgelE3XH+#c>Zh}8HcNaS$Vmo;2J#b>ov$g*TGD_J(;wLFa7-sj@wRF;K z$dg&mnGGk*cic0@)+mL%0rNEz$x?NNbKvD6OcX6hX{Q9fTI5zq%q-mI`?>*J{) zLEiG|ym-=N)BFlGr@eIZSSaUGNeyBCQ43c@>f>tSuYjyJzi+pBtnp4$rSvu9OU|fb zySgDFeAJQyILcFnaX&%;_k|k3$K(PI5I$<(xh$KXvWjbVxVFc9e6<|?hw7KXf4@C;bM6WkKN@d!X<`C}1oZ5a+^bB7 zoY93WRruW>th_Ops=s^oD%WG1gWK40zDU9(Hi2eILwgcwU4yfhA!hLl1f&e{MR9~N z{hcTp5+=6!YWYEbO3cvyrV785KAflrg%*IPobpx}SkRo(S8vPh*pK9^mn|QYFU8yI zy^W4IE^`w3eEX5?sC!rp(}hsUHg+3wG1(igK4Z+OQ27TCIb+aDGxv_k!Y!n9>5&L6 zD-u@<(ER!+GyRRv^4PQay%?M&7fl_vx1Jvr8T!1|Y_w;xxHb0rzPfS$?xl?;tmAmD zoA_k%O3dJyRMisrTJYeFMBv6l?M;;qtK<_4n7=EIqZh*p%xZRg8lMaQo>ziC2BKfo znY(5Laq$hjc$_Fb!c%I>DlozNpvXeSaj=wzcm}er@sL@Yo2mQ`HM0CNNtyax`H=o>tu|Z{Cya%=o5=+?HTp4u_N4AqC0r2! znraY@fB5@qcfr6w#nDxPqDt2TNwX2$-@5cZi5Y&-*K^|LOhWJvkk|ThHVk+j z@cqQN=023Qbudu9`z}3M&p!H^~378}f ziy4lXSL73P{fQY<`N;L+4rByznA_5P5Z>N`okNS&5T?l#m|N%#Ip0Wt()!34YW%bb zVZ;_iiL>n9;fqA7lv=hgDYk3vF1aE0hh2ou5fRk|I<4n(R0u?tLA1q&t1^!I?lyG5 zWt(x$?I1%PbK=VEB5Xl|8`S+naM@YbF_=+#APA}*^ElRIAyQFG4N~(+Uv?R7{ahAs zq}j8Uq>|T3?D7xbefH*ZcH+%Y?I)s=+cg(dt?h4U>1f24KTv`VBbpv20ilCh-r*bX zwy>|vf{abqFLYIsHQa>IoRS?QWfHZgQCrqAU;3D+E_}ik*Kx#GNYE!8z)~r4-|HAp zRYVqB(`$HYBW@{zNMcmu*0|wyGq=dEN&b!Iwamt>id_kGW`$jYfv&OUkNLc0-R!t_ zOsed8^xC;CS8CES`2%fjxZ4{@`e^KLv9YX>YQqL6gYK*8N?Wq$e}s}X!P>ixDy)AN z@+Frs<2OknpcsOyJKkJR2W>m2w_ohg;EW0ZfssE^>5mS^&DSY!V5&ufk4e}!V}*5k zYwx`#RJi7w^LBYprHk$Dd*J$Bhtt%B8Y_xg#dj=XOwFJ)W>F>|8!d>`qr+V{^%-4K zjqLs}=>{L+O(hd#>dRO(b5%GfaG~nfF-bsE-|329UXMs+(Y{bstgu)5oLAFxilM?i zm~kiNpe1oPn-crMEfwu;Z7((+{@l}?NxQ;m`>AAPmS|ja8$Ty`*u&E-nr!v;5wGPYd zlnmSk!v+SK$nnpfJW+mEY^z^tP*c)g-pD=lk~1SH+T2OH?Nu4L+VY91U$Y020;7}f z)QyXzm>rLxGc9%xO&M({&`PR}w1%=EJ%8nQc|ql+F;8IwR;#f?_YwYTVr36xRE8Tu z5Nn#!c17ZiVgSmerEcQs+5T{$MVO|z<)ikQN4?NJ>KK0_4`Nl(##cdk)_!l(3k1pi z(!w@BjALBk^s?BoNapuJZ(v>sGD~$XUEdXj{GuGU#L1|=oqa+$9EWsRmHFtY*R|E8 zdg&hJj`F&o^V?@WD1N#~J8&{`erRF=&_^drniaA^pC{YCHwg2 zu6B0tmrA_{PpU_-IEQ{FMZkJPjBiqcl79!kn{ zFtn2O)mI!n>&7_sFBbZ9+!vR~T=*aV1JE_%YktncHPCJq5{0Zp+rIw#4PP%c-Ykw( zkPAS=f87D`V^sE@M#;9C#~TTssan>R=!8SLjVd__*s@qsID0=u6iDw4bS+$7*NLC1 z%|_5=m8!9%Og69-Y0wizM3<$8cwK-_pWb!=>HGg~S5TiZs3=k2C6eGMioa%ckQjR=M;?w$gzY3Q{rDXZuVLg22y~Jnxl_;ASpL+s1&$4Q zc2urJ2Tv3_<<@$rvN~>A!r3z53)GSp@d%6+PFBjnBN^=x@!W^R;vm|fN-m|-b_d1& zW_oZPnDVv;C+JXGo6=QDfQFF@`SS0(CIM-PgaI;cR;O@qv$pZ;w<+#&*g&1pQ>uIx zE^OCKnJrQgt4-;eu>Pjq`&@lUqvy?8-ZBTekBd){wv99WMGPsmG=R-LkoXUF5~}3B zXtK$$$HZ%r&0Um9%>Eu@Yz4{sxh@*i`TT;OTBL$K$5=})_%7T^ZYREF0z7({DYy*gFg-e93+k9ge=yuP_IECv$_Rpx_h>G7 z!qIJxiXu5$)$k2ObTDuCCyM4&5eP9-SWB&|706Y8Hk4B(LE%wTf=6v1b$o1#kaH*=g0z1}1P9OH@vsQz z4_TlCl2TMjex3g-IO=wl>R=Ni7)S_siMBK|X zS=GU+LQjWhXIa!y#b4wqXEN5jV}a+qdERdm8$W8&2fpfZh2ZfP$wD_>nV~}BaH^DU z&iN2RK?RnsTU2Om3Rx4Vqlaz~9YV1jz4cXpJf|JeBq zQt_U>7!&64|^CjE_`BpvDj`%qri@sD-5=re_3FvPVm$t@tfEZ zbNOPH5;a^?C5t!1*6W0ZUDAdJ`G;fqGrUR2lo3iDqtsV&Bd@17r!AN3h|z>r>FEIM zlTVFoZg)xl0J(Pok}aBAT3n_QXV<})l30Q%gr!qAy7woob+Xe8dABUY?J|jK)0b%m z|K|N!Q2JQx3&QvfcD;ki7D~=whyWk1=6nrcTYX|Mfl>EWVXhhiH6@Y8yOh)PoT~T&D9EsSbkqIQ0^SyUMvZ9%M%c`Y8`3EvX#-S{j25TFN+ocQ2Wz(j8dElS#0y zl~RGWPZ*KNd-2|khzt1^5QOS%wY@L%w{B@{FV-Z$>iBx(l-R;eVdt_i=kr@EjNW9% zg+9aNf<86cfGfiS5T%-~P~tn9So>ZX7z5(X&)~zhF`2nM^~gwBT+|Wru~@T+r(HfE zT-1JKQN4VCA4+)R-gR5sk^xD!Z8XzLP%9KP&>|puZJcTbBqfkaRqqBIZ}elVOHW_1 z?J}m;U{271w<7-lvQ66|3Ey7V_AMV2^K5$5 z!4N}c;tc1bxb5QW=1IyYGFy~MBJp>}%(Y^0`uACbry?@eud?g{bg_i%W>Op284s=e zUG4w8_P9?<;u7`)>2CnpPYr)Rm;H?gmEk^0b&_8)Dy9w4GBJ5P#m?pjj_2O3&FXl( z3;Fi?a+8w!LeET}dfG$}X6}WQ*SF$t836I#lnv^EY7cz$&6NiZ#IMfr2!M^>1p2Ul zv#{JaYe_E?om_l%>GJ!}JBeH3xIktSspFdV_n#2r7gR0fDnm!%)v?2`H@;NF3!={7rsOpB_bcp^ zsFcGixp!^g-b%PS1Dc3J!j;fltKtS0Wf90JyALyQ`&t-fK#6oA(=y)GP6nQierjqI zrkn!~3{{B&T;cGYY?21!(2sedN<@U_llt_lR{1HRHdPE!?*^SWiC(f#xc~@h=}RsF zfxk{p%Vh3ZW<%~Ch$J@4_IDr2T&%wSSzi`+-R}MU>si{<0JrUd9}gFf^aDh0c<5y# z{g_sJdC791_hja&_9jucS`ciM$ajI^R5{^6ko-2};(;Q4rRc{On6?@Amq8~hxt~}U zURflR>1>{~MN3})*)g!N=Fw*0OEeguf$Ox}4x4e})H_ORFtV>L&?YoBnu-)ueijj5 zLr^gqhLAs^d5ZXzKi$Z>+-KC=4K$xLKx@STAk^_RT#j-J3Es@N1V5$%<0TAt=dBvt z`5LX-JUQ+CLcT<+iInV#Fdn#d)5Er}Uyb~{bi^;#HDZ7`&d%me(R(Q_S7ot=g$^nF$iNzN%^z zM++{t7smdP1gpynJ3IA>V`p0O9;++AQ<>c}O!?9(+#K;Z8>6OG$L&J1xlWbdt0U0A znN;Zg1j+~<0&O|$@#Y?`=Pfrnj1}x!!^(wg$&=gv?L82c2&hg!((1K zk|xuny>sQx_l|=$k^l7Le>X@1G21<$gksE)FhVgt6|2j4j}iZQXKaHF*RNcxvVU$EaoYfB+H0uL4#yZ0%}?m!HQXa-1#|fWtSwI4ALl#^taMjI-=@zw_P0OAC9k z-47Io{K;r;3`~J!&zGM&@<4-;dqJ(5AoK=@Kir;%M?Zyw5YU_K@C(IjN$TB{jQMAl(A)=|Z3CV;ft| z{8X(o9luJVTqkS~Xf&qoM*YTCmHM_N%W&%*3fumfVG90sElTp+@Y5dGMG{m0@xB$V zVxC1SzieWgMAeN*kqQtV%T{=ZF;IQq>o!=}pAwRHJ}W@ z2sS?cy?WCdZhcZ0qH6W!S`*^+DFX!CR<+%n4tCxQigzGphXJbYtG!_q>@b-Yk3Jy)So!#8n4Q&v^$ zby#fzRB!cQTxddhr-4J5wjmeyK^7^;K6NnqXRtb^H!CIGG7=$5|C(Q?L$i(VVf&Zi zv5JM>tX1TkYH_ExYN75Bqqt3C;tVRbwwn1qKhv~zzqfLR+@ZmO?^zPL!{G3`s!qLd zQ5C;Q>PD7@Q~!W7EU*?!(geEG+B-wnzJ%Vj51NGEJS)a(54y{Pm_I1Cm3$jdX&QgV zH(rvElx3$ea4j@78?CRD&7qCj;sD?YB2L)atDYpL{s<+|wG4s~OEX1E+>6J5B~~Qf z#Jxh;h=5yx{x`Rq5|E6m|0LU_6Zm|Ap;3l@%H<55X=H@1H@Z$$OLZ(*H*VZtM8&;D zJy;V&wr_I+eby&0o;fYh@6ERJ!csPB{Qn9&8NFMwvkRxSZV+O-GalcAbIO~EE57(O z+L|Pa!}~r=Wd5ub7CxF$DaGRJ?1@B(ak(^+AZ?r4 z707LCcGW5BKa@dB+M}#E*I~zmam8Cc;^OYBNy`5K#J65vY{i;y9=x@wQd!?=9rw%U zb5vT_xh1tlB+;khDY}wU5pSIOY?rnRqZ@))b=IM0w%NXRWrtw5i(V?))pX*+)(cWM z4Wfr$II{?{{p?g-MJcy%Ats{_Nc6nrHIr?W#Hm4y6}}&xrHYb(An9-p{KhgU&nsv+ z7hO5xr9Z)DG-vaLA#_17It!Z=uPbYiQPB1*XL8rmn7G98Dbjjd*81 zcDXd2i_{G0-~Z{7_&52{33G@Y_PrP_gUy>7t6sFDrnPLYBAONL^H};Zqk;G1r`yrY zeq;I5C~fKpiF$Pkm=}+T(PRBoMIN^7eJqj}Oi#IL?mMN0Y=7bZl)LZgSj9B;fLGP- zNQ2agZh{`KerN#eZ>BZ-yI96TE!VU~tWdU}E(#J2-hL(d-4(~fySIA%;Ch0cBwBgB z4B5wNY=X8fTyp3CuQrT&oE>@+J z6BLFuUU8-X5YaHu0{S%cgy**m!}~70t+@~mb$H({kAAaz9G2lyC41$ohM!S@4^Ma~ zoolK$=+b4iz98=NvwyUl4L6i6jgn6J{{ujNlm@k5j=HIl;MIz8#`m5^C5DKmFW(`M zE^>tu4a6MWpxd>F`?B(5ta8JF{GYaXCO71FZk_UAK0M3Yf>p2I27j$x?B>dcID3V} zcqYu1)zMX?-&TqT)Z0K4PRHbrsAK+^^Rb36qMgRd-SN6ecl57ohx!NJ#jN z*Vf9Hv3_yd*yt;lt-5&ZEn*&jy1o}55@*=5jXbC?c6bK3f=G|Q9vl2Du^{S~J&+@A zG?&1iB%+JTDz(=hj?VCP=A*Pu!VHkqYb0dNTi^gygXX|wYr$*`yQ@X}-azz$F-*wz z@wYy7lris=u@AD1PmX#F)U3ko=NO75hm%0tYNDo~t1i+$qGM-QWM`+Ue&BA;IL$eJ z1RKEaon)h4er62`m#)b^N}cm;r@mu=73K3*%x@d|1uRZs@Ll6G zcS6Wbu|(-L4!7YnH9DWF%&0P*Q^{j-GZuEvf?E(fCJxN_s=-}oxxR-v$~X61)wz>N zd(~;7>296$Oi3l4CIB`Z{ksj?O3wPUO;3=~IDUMA=JDLaN;an}LGHHT9|HIroo9yw zA;*}#A&l~set>jkg*{aBlYfT%q(N$GMte(5e*^b3AjhGMXSH_o6A@_=o|p4X3WeVb zN}tS;`|%hERwD(%sSOBzCz;&D#i{a&`RRyVjaf{0L}u!?;?mFwC3&!Iya21vyY_+q zz;5LvEn-ETLRU~!4|YYwBZWccRSZ&`g)MbPCNhR;`sopa#9tg*6#c^eeB|rnwrxxm z*4>J43(q2FbpEWiOn&H?`rH1+0`b$%Z^)qjVA4BIY0c#O3B3lU1k_jNkI;ajEA9Mi z?OEd-q)He+{k1xGU#9B|{v(!7mX2NAvPUnP^^GB4_KI800{#K^EKfNNih@&oq$xo> z4trhfr^#&S!eJ&Etn!H<({RoJ!&;}=s&6z}H%G)qTh29jOjl9kwzvUb8xp&fw8_^w zqtH&L<7i-zxBcXJqV7FYBX$`&Im875$U^v}WyKe#+p*2Z^QqBg)HCQY5qx)$@1|uI zCK#vSIoSI)NzsxxhL*W=XjWHm_V3jhIF^SoV@ zChvdYmM_1ty}CL$_qnw(+l?X3ME&xSL#6ek#ogKk=X=*X_?s7e5RR9xygUR&9tH?m zVTFnWuR#P_s)=f@&&TM*yDv#Ys^2}-&xtJ-Q(qZl?In88> z^Oy~OgUD#*#1H&VUD_K7E)uK5IZMJ$OgjF7plNbtbla=gIi5vjV>EVRH3eTx3g2cf z0r*I4Z-s5o^rO&X{fctx?&O)lEXW`JIka5rt)cSWj@4H6FHFgs*jo{h(5kiswUfZX zUmNx*S-lw0Y7@6R%Mj({Dd2aKDEw5CVIZqXCcAbQuV_}kfII-6qo#$!p+V#>$R2$( zHrOvUb-&|+qUyRm8@&#UlXec@@{O}HabAcWjgr@FtEAM%)c_MmTQ3KCk8PJoSyVb0 z=a;E#z^imP3C%>$5;|ai}k@KNrH20itC);pJ9S1JRg3YpcUK-%`(Q-xXC3%wb zPH)sgBy$quA@-JFd0)vuuyGS>oUsG>WPmqgc^_AunxWz;&5gOJ?FO!I?qlD}(TwZ5 zyrJTvOdIu?_H1JLm8Fg$_j(?>MT`Dt0&*{TXPgGSFhOC+Npo2?oXViPvqTaoPJgP~@bk$8p*2ngcGj z8@N%SQoK0xHnz|HTl@!;uWaVkL(Y1ZhKiXKy?8&d$u-FZWcHt?WJH!zRzy_teBZdy8@0u-qmpI;r44F#w;-iq|~F z+b=KQAC6T`XNP6#HZzDBiH&e44G9%Qf3Cu!Wy{upB-MOgjg4P8kXji5T>LeyqV)G} zrx=Xk$!KvmP=*MNJuNr2gY)09Byy>C1yWLEhiFP{+1_ppv!-F{Gh6IW%-Wmk%~`Bh zPZJ?FUT=I)fq{Y?v(jmbDZ%d3XSg$Vg{h@h?Pz-sgY@r`N_nX{pD6qYnavbP3pF<5 zLlk%Mva@qtRVnT6`-TMy(t{ekL*1OZOcza6T>V^{;)p|Bwy#!2kR|&s>J(rFELIYp zK9if=kH-$1h;w^MI6?SfTURQB7^&dQYkQXu9#Cruy}y^#0q6pi%*I-ABp69B+%`45 z7XH~18qGsiMSo`IXRDXcmQ*^YCT`P0o?*DgnqfK#zeExM-;?oZt!9^r zoi1oeBC=B2?<`A%V1GV~hefAmK|Zy(l`wAXN3@{JVgmPj^Wf*Y`nzxRa)o|PH~4a! zl=C!p(l-a-%KD7ev7FZB>HGzrh z4XURfuYFJSO^>~V=PukoMd<949DQv}#RyLtg4?ixvkatqP)0?D)&Duv+pIid%K!Y% zw^#r8M-?eJ`g5~2O_(_%8Yxco$ofSLz!}OmG*+eXx8+le^Gr*dn_WYN3Y^Qd4}xfx zOeBOU$hHiJ3oo~}CRQ)|N*@u;1cy3F*jgM9*aL-`J9S2FPh!dzw=v$an!&V>kV?J_ zP2`;vXLwDeZ_qY=R%obo!qFcnXo#z^3(t{0Y~D_M9!>Y*Rtxi+&_Q~v$Sxs%-x+)F)A(Qhq$U)0&|r@?8F(9<6~1p3h^DbbzjQj;p^3&VKi z(Nyg-qNOSx)ZmkXQ!=;P+$Nse!x~~A+RG^QE*V?=yQ`c3-=42FxWKEDvnAwJWftR? zyu(}rOLPGBn(h48o=`&ut@tjdML$1j?^XUKyc(Ev%C`Hv<;d$;4N@gFyg@(IPnjL|nA!PJsIRu&PY->5q!ge?&$wJ|P zyYZpaUMwB43y4yyxqQd5v}EH+2j1GAXz8~Dp*@3=6n1U$j5D|$cMaNmTj+ElReRG% z=25qN{5>SJS<^V}{?%^Ib*#%x5Ni8;(;P{#?8jt#D%jzqzey0!HCSEc47Q_FiW2CH zb1Ua3dv0!+hpPjHn3-hxAR0VMSPEvi)Tif5Wq-LTiz1D3MDl^C5?33fQJvDFW`A!@ zgx*gzE;@zhvW6gP*-?=9?+F@nvcb#nk-lh*t48mWL8W=P4qi!9MIn5XE)SFLz^LLw z{92XedCZG2^^eINH=hu7f0kCG_V(U04R)kLcfm5vGBdht zf^r<-Dz<#NS zff<66Ce#sTEez~GG1swlh8myRYcKrhkp}yq#^+&+qEb!2xf=(f&BffXuWbZmUTpmX zpr8E%m_5v3u1u=7HxlK{WXkuL3@%6x@BDTOONWR&ViC`S(#88gink^R6Y*U5`tA_I zXEDuHj)rFDq3t1a_s|KkcrlR@a=V6qr&$oP_>3hTfU&3sI-Lb#sAqmXEkd{_7SI+ z0MT2C@7Bu0Ob^#3;azkEXg@cV1w2}Klv64k15+W)(4~Jhr9_xI>ITXBp_<3@O~m?i zJhl*(1np`h+sp8dx~76il+WZq;dP}`>e78*4>U{J+-Ka!C~G3P;2KlmNDCZs{BPDd zZ*m)?sQX*$-ac%Skp2T& zkYopj41vd|hMq4FuWWLUdjp(%vqG7)CW4&5d`+s@bzgtjeW>HrATgRya`6_b7_Jnf z@{O+>e=*GZ8b8m0Sf%hxm3^!L>YHb+hgau zExe`LVLBC5n0@E)J+alu=*b`3&saYxhUQo{1IB2dQ2GfwA0seHp4rBYoAsGA2T*&T z+Dw-wXX}LF9Ez(a(rkwl%(I1roIZtQqx#z#2VtyM&h~$*cm-;kf6>YI@^2Eb1rp{w zlx~t|E%v>Y!^U)q$-}EpbtWuYpWmOmdtZ093WX@zt|seN{dxVpr$m`GqbB`ZMLfOw zI1Vlt0nVUK#Nh@csv%+Wzg`x%E7ZL%U23mRBrr#5f60j8QzT(dU9THs1|F_6zjKOO zyxBwPzGwu+f1Y*_a*xq2(W+E-wy!83hV9i-=`4Rc1xkPd^}PuV40dQ%%nkwRcBgt{|kJ-ydBfNI=KQqE@=#>(pt(>cKt+DQRz&H&2k&M-`oK z;Q$m9sX(z8zKKbF7|&|6^$X@Qqb76TncGK%LUEy?fecNw+furgH3^v7*WCxd6s*nr zOMwC6?l&1*!Fp<<5CbZ)6WRoaQsR>vWajvicRt$aQlni6?y zmNkCE_{*}T_QTMZvwF9D)x+tUG8PUSYhj^kDm^SHTbFk8jp%TKu#N2;bW zlUyjh;}rMnI6F{Sjcab^q4;stJbFFr;Khv_B2gYHv7E)N8p;`w^p-hwhLid6s4T%} z5>H3+?Br=`q5G^k)4w|U5}#U3{P_%3V(peH^R0V_h$4fRwVff6vOGU2Hi9fB$E(i8 zi<7s-g*x0=e9Wg=X6Z~mWiQ6U=v zk98kz72>GX)NPJg6NuD!`uBL%f?9jbT-gG$%|$42!r8a@3!~WVM<0-mUgCDrVJG+d zW>nM0rr#y1KBV)WHtl;$WN z`Sf(TTFs?8u~Vzha7$qf)sLXRWiHL2T6auvz*B#f{%W5*B9VmE}Jp?60}_mu{(< zPDzDCm`em~K;A@-1e+2)F*pRIL9R#IjXg3L~<`G@)xaxdOkEi**D{oC%W zT0kx*nK0ZM1}1OQVP`G}oYc026XIY!V&vm~@z#%JTIkL|<-?nN9{+8PW-SCYb0s94 z<(+moDdO{es-GSXkn0(omh8H3a&q^&@|y2KblLEZ-n|C*G8KyBPC80wbbIr1)7|&u zL2D{aJZJ{epw;t6yVXcQ<9D}rFImzm!#YyW1mn@!+Pr++vCUVjb^^Y$L|beo`G9L6 zNzmno&pTggF4MF8W}L}8&IUf;0*&rn|LPb&&?B-eD<0CIZ$5w(arf&wy(=zuCz$`w z67(-rx$YN2OlnNoJ*TjYu_!s^qvnOnK|k)&-!v3=iWJv%jf8`oJADXMPfzoTiUNTL zYNUS3L3c{{(_XSLft{9D4HBCA)OqMu<5|za@&QQ~is3W;f}B_}IE(P_Q9Mj3@OVS` z{=v{CT>|wFu=(cv$^$kp(K6ep+~7Rcs3g@`XKnl>R&}+}%7?umauwNiAiDZCyOOK? z$K$5bvRrkAMM#{_uZ})JjjZ-I##b1TwMLT;;7=Fn_whPHGfsA;rc+8dZhLf&lMU~@ z3%;n_t3Er~C2}4eVu6xELc60OQN`a0?TJOTb9HyNcF!VL0)8i#!*Q zrM5b7HC%e?)Z@11jx{Zu&YWl+ZpWPB^3CvtuN9LEEy&7kW1aFaJNxrqfXJfJN;&m1 zR@q#I+S~A~COtt&0!jS0gQqM8{g<{V&*(O)9Bk-3UK3GH$t3 z*YAqs#3+X4qzmBPeRc)!OiW9<}X)Gzik2 z;hO5@nkZR8cXU1eV^Rag;ayg-hZ+gs$G>NY^7mZTK4T{{f3Gw1HU@8q(x6g_j`(5` z1l8Ab*|q|hIkpQ z0$9&zA8M89ofglccYu6#-~`=dJQaw)kFLL#MS_qL=I4?)X8N_AehPs+TR0f0Mt?J| zM!x_6@(^e_aF{Fr-+CRd$FbkE6FenUI|QB@^zpOuMjJRau57xGZ#zXD^d44USF=`W zNEw{_Ysvjh>U)YMcaZq^FktBEsi0$HwvwHK%Y?hs591&Nec*L5965s~j=1V4{4xEl zA+Ds+!Y6&dlnZZ;+8d!a-a_YIzW5-F-4qc5I)ZF~q>fmoHL)!RI7&^60J92X+VC*8PestZLvppXC`#%VB-1m{DKc|IuCH&EuB?d5C zud*>*2mgq9)Jyx=O^SDxb6mahoJaCb)tn+?A+It%P3=1sz$=aQ?aZ#Sv&#?qK8Iek zx6rBCq~IO6V_|8isD|rPI;$_fN;on?H@Ha?Wzphf}^E};b zIe*5V!NzXA*aG(vP??^y;O7kW^c2=Ea%CBny(})Z!m=Js?bcETSX{k%@e2+Pc-0Rp z7l)eV4BO2be5Ss0kjaSnik~Bv?LGs@1jz&W6(p*T&bJe?^g$DIw;I|mjC9sdJSlMd z>9gLIZOnxH{tr;Nq7?jAIEYQXoVA9h`S4AHkS78{^0F1)=8dv{ysop-_rY6Qj0sT1 zSZII!v|`34O}7aSyS9=LQ2GN|q~CwPxzFA}%AB??@@D6_d9}Z=$ZqM$Y#fDAo$?Iz zw(S{JsZ@RYt0d+vb;p<03({i_@5?vhGULr~lWe` zVLP&1fRq|7*;8W|pBO3smj#LHlsZ{H#rx@Gue;5+>a)pvf|my~0%L1mZdMz3Jt1ie z7N6fwE8(}#h(mu?QB@S)SDXrEP3KYuQH(!=sPSSF2UE2|v%0Zc6|dwRsB&Wooa7UF z=mzK-vkHgxN!_$8(aD3G0DKOHk2x=pc!EEY0>7ypR*JrZJAkynvp$3*SRs9ux$efy!Owrh5 zWoVG!G6(aIbW(f-Za6F~0&x0}czh}Ym=Z3-T>+--hK>M$Wmef)W4yjsD_zhB?}~jb zO}NA#J>hxH@XhrAWyB~FZAlLoYgNtP2RrxT74EcCCaLSG#@_k|7$y3#I}5xp)1G(r z_LNp20qtR=I@!3qFuE}K9LQKdDoA`uUw0Ahi&?aek6r;AV+r9tPF}tq)C&9&J&2Rh zxJi8N@fzD5Z*va#r~{LHMiMi4+Wua?3P_nqYeFBW{r!&$(_FK~PP~1w97K%%-;u1W;gS)%CQ>19H;*|E|dH3vhp1ps4 zGf8IUM>1=c-Itu#c@+EO&Xzenggx`y^a-O4r3Q|F7hY1N&)Z-Km5~Q-4omZrHGV}T zLc1Nf9A-S9803MoGjMt7`5XV6dK%}1z#4%;{@-Cgt0RohQL*iGUNJmVV4SMT`$0UP z>Obw(GibRr%G&kuV)U!_W+>4s#pH!>dY+VZT{`$Tg#MnLV{3F(sxfzZ)YeQcLa;-y zF?V4Od}_TbzM-SSC-A-#Z|3ivvkm!Wt8)nTV=;9v?i_Q5Vk2?S)pHg%Qxgi41;kHA zTsC}7p3-HL>_zP3s=8T0doQ4{=;WV{&5wL(VruWMQuprzBT$J4VyhXy6e*>T$~DiK z`JdL9N<WOVmwqul-8`Y%!CrJXNcYE8(#oLB)E-!FzC*I>8vX2S6%fmH?g9q zXIWjLv%=i5xJ&dc%~AgIcG~UF3=ZxY7BBSaRot5F!u*vZXC|lB6HIVv`3oVC2SoPu zkKrvY+YwErmmP#>luyg9qJ|m+TmpbBBzY2?$|Nf8$Ne$$UEH7}7|dGsS6T?v`6f3B z>~rfK&W)bQx4g7&W!%fC)2R8g@!9wYU2LM_=+&7P;O4E&OfbU?*Lw_}Z{`J|lKJK- z3b)qglS$3LL+rQ3wiZjTno9Jq%9txaGsC8oHCBCBeP@0ZpU7VDa$IRbrI@X;A4C?^fepp<;X-SrxpL`I{FdA zGpShv?1ogY)Up;7X$QGH%Ui5ILcEQGrI6}#wU}hFO;g^vx&7J9Hw7yMW+KxNh+(Lf zh?0eBSVe$gmYq}O#ceO5yj-WR#^`h8b%n$6(d=@xHG3e)Nyv3itJ`l+`{r+?4P9bq zd?TQ>&NO9e7lKFNo6pKf^rs@}U7H4D4-4H=C1;u#=#@GOEZ zlC;YCRaClO1q!&{$k=(Uz)3jjaN&d|-in?)MJKlZ*`@xwhB zHnMp2hOP#}O{qAd07OAgYnd!hwoFne(S~ajA3P-c$7Txmux9KcUAT)=c)9K3Bw5s= zxW>XZlUyWl`q+<6%~vJF)WzCaD*_2o%WB`HhE73B!-~jlDr^-+Yc-f7(NORDyq$;W zn22=kAG)Rx*wdwY)wWgkYXO3X{rAyyDraV?RMJ(lcOt0Oo6Z9&rV_CEz4{8{PWH4J zFF(GPY9hego|dr5Y=si8t0$B~gB6&c5^QM%X4)vLv~LMdDVS^gGXCmn$XSJzBlVWe z!^$RsA_4pjr9AVCmh0%zDw_6{u4dfPHJ^VUMM0fipuDyKIdNEtG1uHHE{N<8pQs=F;*Bq1w6#mf%)q95 z{M-guchxPk^Rrl6F-+zQ;03)rafi=Jm$)mLLtS2Di|_Xlt8U>knciy~r2dMOQUj0- zddnJ^zo3ytzP1c+wpSZ4RE}IDLP`HpTexK@m82eF->$jM)!RSMOYTCVq(hb(qzVBe_i6zl!DYyYWQreM58R@O?>ArkLWBFu*1*YJU(8Mj^q0wpMes6iTUBfFJ(f@I`!fL()GkKTk4Q`u{y@jWqh9JRx{IvmaWe8``5f?r=0nv4pmM; zNmb4e4j2^~qG`P2AF0{Q6D2iET~JojhdL2b%n1_KgQ3AbsLA&P7fc{$sgkz`^Bn>8 ziv6SFUKtDJDj@&%uzbpVsd49`TJwd`;x$$hp3v5_i`|yZ;q|W8j}VmmBrshrqqgc& zkb@k`!?{clXHMjIy?N%SnOmNI%*^Km&v29iO#7Pn{r&~yM@n1lRA+AKP{)t!!5QvN zjb<&Ka5@UUj+}Y!K?`lEKEZV6lVQfO^KT#xwxl%qBlM4vvJBVQzgn+Pe~e8R3{ylX zeG)Ow;fS*D4b-l~Hou{t_TiCW`*IjpI_+s(hI9ePN{9Fn&p zJX{_xVIX8!8ab3g04wq$&>_s3vY9~)orqIilhto4(x>r#*aKyeM#PRs|wUvCKN9Pn_XN8h}>i8iCiMA^8Ln51#@GW1# z^SmVUWWy@t?Pn~kmPPYy`^#%7Rem7fxkZEM@-2Wg4ZvfG{qo+0{TDHT-rhsi%LN( zYsgdY8uO`961m%O^Tif*0$Al0wX?}F0T0jhLcbGkxx*R>P)97lViUtwZMPn@o5-a< znJ7n{GR-o(Q2)6M^$}%96~2 zMN>>4wFcDhTIX^|OnVSEch=}5l1XQb^2WotC*)1>_G7BGsI~2_hP63R(3)t;3bZ>Gw(e@P*E$bc3$v zM`)1t?yToV<$pRShp|+362wY_9K~xND*vj-jj`(-sDP0d#JTQS9JR z?p**1sh_27pz1Nl;ow&-sDaK8HPyn0Zw-g+>0&R$*&NtSXjVPp=sarppJ^22F2e9J_V4 zU#qRmTpD8n`~67O`~8Ykw-1G-c=>k{R#VfCiAuj3b+Q*lbfd)4&Rg2yPaDBQC5@JR zg(ceAkWKizXx*<_&YqEsQDA7o@wzN-BE^`zXL>q|vLxz&SkQDL8ca*e_64^1b#*U-l!nMW}nD9rK)_8;t9Ln`GYL1s&y=H(g zv#1#-4-<2@a7G9Pb$I>9J>XvYc%5jGRDaI#=(TdKaYIA840Dnz(zz(yaxt4(m}wTc zZauxwt9EOU-Yp{WGyiVz_zxmQ0GhLbJJyptETkFT-6{rk|Cn=NlkCP)HTs-sIcxt# z*4F>Js(f5`odW8`eS@gSiQy3+k(}d8yN!2DH@jjky0U-v`wO3kCNqU#KN9{!IUlw2 zJkT8n*+%ga6<^WJIVlPMjzogEScO&9Px;=iSY_TL&#vvF3G>BU3uQ9}U&}sZG-T=& z6-Ln(93A0rn~FS2snPMNSbyp^f|?jlY+d3|d;of5klmn3JWnFqNhv8v@EaAzB@R3K z!k}_{Mn#`O(v3HGaMwb1_evI7Lg#g$MOF*BR@x%E8TO_6Gfz1Ttn=b{lok8Rsh_@y z653!E(#N+*y!}0OlsZ%Il0M^5q-#}!m1O6Bou7E7>kYI)eBiMqqGSE>Mkku)-Qhv?tNuT9Lc1A}gj&!%6q*Azu_{*~}~(R+u(grl3oc~K|08+g_^5kD)D(pb3q z^bH`fw^h8G{2W!^`hO4T1gRIN3G*KCI#;3yo&wyRjp zn?5qULwe`CVOlX;6&9V3d!q3BVOjF&MVW;wwcb{hkr3nRL!3qb>EdhMDY~F7_YFdeWkbE)w_ zffXxAgChIru#K~a@JhG}M{N}TqRk0^GwiIaw1`m8ZE zeoXd~D|aSRoMw#i(wLh;M*jv^5ailB{^sL;y<+GhI1n>3-QPVLu$H#Lp?G{$t2QUA zUFr@6(MG>!(r-376{D;H*0MnLc-6$po~5sv;&H-#Oo1squ&afyV#E0rAQ740jW|{@ zrTkr#JVohxAIo)9(Fa$n<#>6!YCA@T`l(4}7lB5}**9x~Y~2qXZO!dMaJc`zrfCJA zhp&4h>ZU>fP?bAlREm<~ij`6V*UmV#AQrf0!4*Yn^B?jW zIYU|}A~AXN`z-hPSjBQ2*yxze)3YFG?XcL!HRqcrvmn=A5p#o?B%s*BAZCEr`=-Qj z4&|s%11!QR7He?WWWx($cwu7GhWEA7Y{o-)WBwo+n@ZWC=jmo0MUYLL{WM%`G#rEJ z04ZjV%1lz1%!o@9e|TDKt}V6jsQSIRq@Uewm97_0dl^p5FlNiEMhYp}mKzVp3?h`H ziyiC>Oz-nRKWBbMrD6Z2v6d3AFq&;qH8~Vp%G6uf=p$C($QaYt7$T$oE!92PW^JWL zy+l;~&@z(bm{K;hfIxzhjzy0dR4d*WZ{|}pkkoTKMp)~c&t0ksi13Acp(6-Iz`L?T z=$CO<5>5|I#6YJX6g=Nc84cq!Fz~u&^7*=sZkyEpo59 zD-xlr>9TAt(@m@Xkk#Zu!59{C{tVge!buW$1t0UTZaRH2YP2!z7$z??-~s;&?Q3kP^H1WwNn~h|qcy(Y`1%XP2&Uig4qt zIyDKZ*zMJetXsO0-lTx^w6xuRXJuiz{DLAaX}rc;y2VtsY4fLu^U->AaOQVcm)7Z)qR)}o;+6h62 z7XQ_28CSOqZQmeSSOUESWz`w0$h3Z1%4kahC?nfG-yvI&{n=)8Gk>d(3^zu*EuGz! z#7c`p+oOJuftyoc<1cz@-K~`RY7)ftPlKTj(|_Oie;YM$l6vcYL}Asah-maRX-HDF z#icig4yY*jQLzfI^0eOb1|Q0+!dF7ggeTQ>swl!sHs;`u!ozOHNeeMNnqwE0<7^Z- zd4COvNAiEp1-_m8C1GmzabG`N>Q*AEW2T$vp)^sM#c3cHI8Xe?gJ_n)#4fqmvG#|p zyvEdo(`r)3=|jw)iZrTNQa$SnT8c zRE@)Oyk^g-6=H|9r&|UPS0zGSmtMbDAtperxqf%!Zt?JHG(6Lc2jPWZ=qIsdbWsI7M zTMS+62cNU+UwPq*&Z(RBd4}hYX%67>)UrXw5c*&MwsOpD@%sFYw<8BKJRI9q@P&JsP&haYhQkRDr z{8g2zo?6*R*$xkz1N+Opw7(IO3ZW%lP$l-_7QUc)nd2&7H=nX=_bp!uZ@Ax&YO|>` zsgY9Z_MJbktmaK6d$!z|SP6+)TuM=fvuttVLJK&CC!W8UVcUURJ*G8l0TA@?UY@5&K0&Gti=F+Tpdu05fcKvBr78@pWS+Ahh9R+94IM(3ey&|O&a`q{3z945l;zYU1 zBczV02Ix40P>9%EwrEwT<=BlRp@@{zBJz=WdVjE&k+R*8G&9FDEKz%VHHx*O;uP`T zRF6&7gq~rC2M!@fh zMJO(IV71Ijd=+i-uiljuXKlHx2fgf_aBk)jz5)}V{D&xZYtDAYODwLqE@E7RiYxhd zZ)fd9W-{y9RL`HwU&V3YtZ?6=a8C#~Nd)$WcK0QYbk>sp=(HI*kCMxH#tS3~C6R@<_|8pF9%eU*h*G^t^#7M$^0Ji7=v1#i?iGu{&-6ld`(PZ+YvbJeZS-T6L- zvdupxZo*1OM(z5gj!kP4NyVZhVZ#=kt$3;oFnHKp$H@saNlrbZq+q%?@3)5MY6INq z+HtX6)8#UvxT?4MGMQ9ax;Jyc`r%$cAkK+)bGTJ6^u*LYH7t5YW?S>RB9cHd4-aW~ zEX=wP0(IF=yB!IZ^sxLvvJp}_39^STW~jpzx7GIz?)pFB>@{PMzGi8}5CjmvW)nvS zEv&2V%-;;N)ex8ot(d|r>P;`T>A3AOq!M}n4vNVi0+)CN^)ivU6#>)t$9S{wIWats z)uO-*#Hw&UyGczH|L_b~`m-;i=2|dK&cYL39X}MNN*R}Crjrirt6NPtyf#&6 z&$w7gmHUHa0hA)c8K3LKRAX!wB}X*Q%QY@`dt&K@IZDlJ$uVY8XXnZotODIehD0cK z5*$p)R0_R{9l?n&tpP38?U6^d!xYUJBQWch4tB!tULG~u3M@KXmIYVaepg8L)u-0cdtBeT;- z{8@Qt*-}ya9@3v_t<(uFaLKz(83TV2H~!5$q&fR6@3_laEZYqwj&&SxXq)xa#so@I z<*X(0(f6YcR%}d)&AqrIn)n}QqK}$?7goRxxsnwwXo|o1Sp|=2PDRZ(Ui&=U7zAI< zmN2Kj)VwYoOR?Acpt7NC;=1hhT_@T%ot>pnfR~HKx6g+yp=-u1wep=Nv_ws*OouUZ zj^A-;+li_#l_N(Zd*KzgdU-fceWGug9$hY3|FH88gVJnQ3#JRjjJEPMd$#B(ZFq)d zif`cy7Ccjt=ZU7!*dga6>uP22Fda-5rxDm5jpa`n%VG4a&OtQrIqdz&yGb@ekLuiB zFK|H9XvxJJ>v$$CbMjvF3pv<}wah=&PNXs9JtR{!@rA2>vC#OM_Gd79R{!<-4_^xq_65~NzErA3c4JeOyYN*3U)Ca#s6 zNN#Nwp2+vU%&0#;?a@i@H{B7MB)?0t%kL6!kLZ{R&^*MNiPg^gxxUN1)7!j)Wijc~ z+(lN+7KdRRv*B+%9U#H|=!%#sGBsgrHxXDCi*24GbO3vArV-qw(-&Z4`k~>{Ey6lt z(*2`BWr{p=M?lkAUAEdqs4AHzOnPWbP#*yQ0~AZ#bzen87sL^vdG+w&VEU_3I4=x{ zG?TI`v4fvC)#(g{Cyzn;`i;JJ3s)Yqsvd0l$`Lx66I=$hy0@FOJ_Lf=sP#gT9plk^ zI3RmsHD;W*$)FS{|Kmrz>d!d)H0=*&Q)O}mQyZYTdW?D}L-UcLlmm1m@#*&;KFRDs z{W)gCr>Ba)Vny@Qe+W36q^HWMv^w^s#3uq8Q54;+Tb-LPb+GsU-${3L>H*BqqoLAc zVTX*!`9p{4ZpCb~so_`gdN6jhpBc%h8d*a){-SB=tM5GEgfkkhVj5M;6R#O zX`EQ(xV2pa)OE!gQG4MDu|+97|2p{v;DWPYOd@f`LQAUEqs!+iP-T}dIX zP1 z$99`&uzS_7;)sAz!lx1IY%2NcdP8OXtO%f1Dsx|5^}IN5V1Ew2V$8@{L{J+iz(8ho zr(K^aP*qZO<&zCZzQ*@mUYw74`I;>8@V3@0>yx+EJcO2qjs;bdUEFQydOmCvc^y2x zF{u(5$4jZ4(Ln8_dtop#6ZPmajk83;EKRwEz31}GeVCrnY5wH65_sKSa%5=#8fYz6 zHc_~7((scfytWIPoHd!Y8XqhX#&d;?WUUcaVpB3e@C`SNV*8D``6v;0wU#M}bGp^+ z{5X4_qe2V(TH)~LcE;oJ4MT>hPBcMtYxL?fIB$tejrXipMr9F_JG?6~Xy{1cl`U4L zR_bt70)-#d2{eKQm0Yq9Z4&uikD70e@@r`<(!xaLM33i^uV(A= z^`JiF#dP~Ox3Wg}>+nzk$LcJSfR~aA!|!7B|MIYFH^nw!lr8#47sF?NEH>h&>>H`t z7cEJ_qZo)nRVU!oAHQ_M(0s0-^QJ~H$Cp;N;+_?Xr?3LERO+lR+kL1rG4SIW<#CtTg1j{w_ z5YayczhI?K&n^jzhoamBCixo%?VEl5bV`Oi)ecbRw3y_bYV6}xtFq%Z(JN=Ec?XK? zh^MdGIh`ym)6?*!<`)Ura#ZMxl^eb%!9+rgUb8{Z7j4j*Q0MReT*Rkzm_@l&h>R!g zyg|)z&*`h~_X}SftJOH5NN-O3e<%3{{$HSOf5iJ(*JZJ4clxXl|4l}9hgt7N=r(w!F!%oe8O#l@O zVhZ7jJ(=|QAqh05AC_*%x5rt-!lj9TclcK5C@-PjpvbT1R-Ci{LorY>S3S*1d=9#*u?CENs6b1W$w|4!lK8M>wN9j#PJws`8ugvd1taIP zi;Dg8e^xzn8XGXb<#8XbD3fm*&VF2e&??zTQY+18vCGzo_3dZGFHD~(SKTHZKUjNRh z$Z>4@xg8$YRKX`VgI?Z(z80UrGgcU=U2pNc&-STtVSAy)8O#&jx5 zd$r!oK@|&>VTD@KFYvuL11>Vk`Wrwv#v6KTc`q;ibIUz7bTu<@&iNZ@!PvE)UKyy} z;96Ia4txds?apopH`=AjJ^2N||Lj3*L8w;CG2f#8q0Daqr#;&xoi`eE>V2$;7~R@U zNfvTkMx66M+w@!diZ1iE#rK zl%tnoXP(uE7TR}Z#$JlJ=U}2LUcAcXA^FLh&w}jnx-AWO4-$H+Gb%_j@C={=y_&$r zcrIY({2lu|vAcWT#k*G={H-O7&rSXN1<2fPV@71=Uyw<^++cN-r3A-maC^6aQ-j7Y zZ;bi&e<->8_jiFmHqqP{D?p2g_E*|Z@^9)&G)h&h+7DxRrqTWJp@=)l&}$McI(Oz$TMkQTGU37Jgd>aY#pT}4KI!p#``Ox6?1rvC|8To-Ttp>$G{{;GTF zXsSI>e4^S|I*w_;@pr0)9apRIQ+AYCv;dxj7V4mpb6*Y&kaE-i8Edj5o2 zL`zE?R1C*8b@9|e)NzszW?Jt*E{%v^`E;pigkf(oNnGCRKA9;|WNr;EjgKoxwbr%!;6J0K~ou_^XT~pE0*mTkQ43 zhxF8WC!v%Vtod;?c~)A%&l3!nSd+yM&V?GKJzrc`vf8Ozrpu5bq%TF1IuVq@r(|mc zJ6!~Q5?S-?i=IL0(1nipIZ85MpvbZc>`Tu9cSX0AdUUE^ibe$58aU^Jdnr}pyz5)g z%QqS&0pz1hy8ZcY&Ih@Qntv@2P>{0S6xbPtr59ovo&xT6<wP$?{RJWv-DzogTP?^h(cC00 ztxJ3pMTG1!=uq3Jf(9_pw%UKAx(;%>56x<;#W|C(M#9dG(^Z*fy@>*0#Pcv0Fz zOLUULa`phK_zA|Pujl>jBb+zVXL5@T8D~nmtJxw@gSM1AQXZRsjrT7s6a730*v%35 z!qfl(4NpcspLJkl&l7iBHG)k|56zF3W)~=;G@7sib$mZ0reuFbti4M3(<4Hf<*wa1 z)dI+P>c}x?d1?(6SdvOWmZqGIa%}VJ1L_|e0z0>fAqX+witM)%oJ|g zT)x2eZQ`jBCITjUp~b>HE>fp4Cb<;c=DoH{wU`+sqfv1K3owja6*|vc?s;Sd-Ws<7 z*7~BJ<>;T28a56HZu4XTMWuw<9o6J{Q*X^ZvDiV*JmTqlK>?5ecj|!=#RLUZnP07! zh7Iv^a{%z8TU$mqQ*kGTyoA{e}7!mS^^aAH~HfET{nEtnVp_W zC-v2P!ON7UH2XP?u_1BJc(MlU&IN7K^pb)jBOZ3jxu2~_iuVw;%G#M6$!`gfTB248 z`w$mHH|@4d4&L!-i{o_o?ChbGGV82LK031-+v8gpU-rzzh;J=QmcEQ@=gZr1pZ|I+ ze4wV^p&7uI9B}J+{t*0MzvBO!4T*K$DFzo`h_C0UunGXziqm3o=Y67d#05)IvjAYZ zNwtDCR6;@X>(3)1wQJM*uu)OI*M`Xq%M@zp+$;Pi?_;Wc2)hHO2b-BctJ8>@BEI+? zXlFNQOO)I!PYe&=Bv}%U4*3RsXGU@EqO*F?nf86S8|shl=C&H|?8$JqZ0ZRek>2c#e# z+?T<@`>db9i9cK-Y*HZN38y%bTCW(rmts#+HWKPQ%%`v0B?l>EF$0BCw=$^nXOHIM zpZK%N-zD=$uZI=Hydhp=oS8WJV%UA|7N7elV^>&;a$7wUOx)hJ*K57yeD zaA&6)q)QFDU7?RrGIT``FWGJVXD8dmn0$%&hpO_cj$HA0!gz8CFWloEFEZ~T6K>u0 z-MDdppay40+YhWyt3*|3RsNi_ZWi;REvc`lomawXnuKCU1z zG=bL(T%PkCzR&V63-J{PqHs8jZR72L)R#IsrB}b6xS>^!g;V2#1ZZxgx7{qSL-K?R zH|}2`-t3a1qPwL62SOOeRgTgNTm94C=$E*}Ix8&DPUI7@*O{6{bWKz%AK2=R*$TxD zv56ZB9wMhJT;Nhx#_i?G=K&fG$wfKb(r8+w8oa!?fN*c)qzelf0eT$8wBea?XA#rJ za)TViJnwvJqyxEtTQRrPP>poxNVx2Fnw^E&1>LUEOgoF?RZqLrrOA)b^`^mYs!{1W z3iWBV{#{%B^G$cu+$;GvUa}%8M>Sr*xUG2?QRjJ*_Kg# z+4Ege8UoSsP0R}RaVhV8?wcB3<0N3^SzSNS%hP^8g7GYTJ#C;bvD%R;kZqo=kpY)$ ztdT=Cm72)a5320PsBS|_ZKI;TYXy9^RF;!=4Xv2{-R|JRXItJ4(ASpa?s>(5(HGl_ zwcZ6FF7%a7yk2Q1Q;7<1o;f-bBc5`!Ur||&v_I6fs`$ZzYvZRbpgG#ju^eKw^j9w) zd>F4=Vc&vT2Q$9;7IY!Wnn(R6#49kR5g^iWc;f}jX&aF!eM zQsj%}UM?dm=7S{(7_7QJ|eF6AjSp7QcKlf6EarslODJ@ zYud&g^^#D1Hz#oL&=GeCilH^f*`C9%UtnN|DY1gXdnnmyNyivj=x|@fetlj$38ud! zJDNtxBs*x^iCV@!KNap1n_?M_V{MXLWt*NmwxoH&smF)%TGwCAY3?=g(F0VYGGQZw^p+o$E4Rh*zX|=yL^6;aVCBnI8EDm; z`d5gk0Q#E(5HS(8OW`_p+`)w8)H&MeZSkHV0)J+$UYZ${fehzcTU5Xw4^3lhkhO|^ zKB7%kJx9e8nWr^g&g-~zLQ8SUhamgz$~lw zSt(FhZ~y@r<=A za3;SUAEpL0n|uKekv;|%MLrvaTOIaz#z-)KiychR`M8dqH|ot_oo}Sv$Tx1PdEGlu zB{jFep7CWn1;)q>^Fbh?#!1+sz?Q8)+yS71@ zwa}j-tI$7t+mKYrJo`UF;(Jv4bwt~TA-J^KR7CC}Uh zG9U7mYk3&^tdmDRlWv zN{ipgS>Xi|mogeJ@={(V`5}$grCGiQd(5kGLSxsH-IlSVDh(8za|DMMMHvj*4U$<9 z!`Q(1zEzfeGk}hpBVVEIAl?Y+-I5vFq(@G3RJJcdhPj_Z|I}+tceGDjKuVCn*B9xP zQSCgS13%^(Q#z8-Io=m*oYlJZgQSaz-*<5nSEyILwokKk{=CL9gFkW2A@*X=3^Hmp z$7))=H^jZ@YJFJzNNmuDW0&7K4`JJ_8jcbi6RFa!C=Ih~TyfjZB{U>Fgl<%Qu4VMG zRDH=u^!Cnf_u#l1-`j%X|C6x&SCnS`|Jz0%`>iU09kF)LDDw}+UVix|=-qO5Vi+?q zdC0vS>gv(OKa|yvbzc(wH{s$*b1o0Dss4uc^{V_5!RgCHu9ny-da_%eeII=m*k{U2 zW{lr0z>gOBSDLLt3k3{$5yUyYLo=#0KrsbsUdYA)3L%)-)jsg+Hc{^}x@8c;#akY< z@7y@?KYAv4VxqyvFPrc{>+Vs8!`$JC)FKM-PQ01J$3@a?!`JjuZ$SYB+?6%p5Y8HN z^8k(>q1`>r?8b2=)q`xxj@aU(l_W<<{b6OF6VSgjidU;kW5WJEj!Q!5(q$*{ml%E_ z%V`g?kz(~^$@yvL`0tlj!{7YY&_{PVHi8@Fn2+n=&KY$sIxHy*w~?_~CnkTw8|! zhuLv5gVtgd&HkfN_xmE3l(fp5?RbQ(mX}?e2u)brgA>Avz#|H|iw2!s`XOPa6L_{%*~`?yg*Vq>e#9nnb)w6;;U5Y^!^Op&?=Vr` z6F(Iq+DE_eX2{iH4%S11K}vTLc{in>oA6;-b0`mW&uqe(!D)&X(?N{jmGeftUOsu> z+v@rL7v5VcA4(HhecqGS2=%hPatUw;__E;Ps4d9>{>Yqu;e0fE-+Y%{J;i-j`nyIB zyN@8`@00|3;LYZM_U{LW{8|5GFKMZ7#?FRSj!td?lKCZ>Znu_dQOY6{+gGue@tkjp!F`;@5ID3@LMBoHIxz zw3(T2-0on`U>$yS$oHg%h@miMRhOUACz&B(2a_s@rG6Kti+)%9{eznXj6z$P3>8#6FcppnE{!S+PY>OqE zS1Gj@E~xlh-RCF0z4;^sAlV4xv0P&F2xUKnS^If(&RZU}hKZyzUf%U~FY8|9>aEc= zVR}nEuU2VqrRdNezP$tD8cKp`WXXWd`D?!#;?#es8th?gq*yU0y5b=yMs8TmxcIrWrEj9zjzAkU4^6UNHo+N{y zBn0&FAf^dMp7XgnUV?PpzmQyC!RNq^$#qbkJ*AQHI1t+X55@QI2|_kt7UD=WLw-8R zSR@M*>9qz`2za+gRe3Wz78QY4OkfI?^exc7F$#^f>dS{spw_t~T3i)oMhovPol2Gd zk#UKsxY+#BNY~3*v!M({svmq?L~b}BDD}lwKqnCs(aED+Rrr!nyc~9J{ByNZ|50te zW^*v6Rw^hzgQl~@fk?ap*;2&g&00;HTL(%V_f9v0B5^qsht?XGN}18rg&EjNTR7E{ zwC7F^C(C@UB6;8gKjH728DQFmGFo`NT_eXsyp*@%*dAvnk)*_gTLd?&fgSlmTKB;G>U5ZlDJ?$&*k*B3!m z-H(E=284qDFJ2)nS8DHyG^P4gBiHLS=eN{AdeuMJ9bQPvfd!p|b(!N|4U{cLa(p+_ zKsG@LRL2)Gy|#vBUQ)*ze|g>QJj&$b`u(|{rtMEtVp`!i7pO$!5~Hu}5y){a2p{iO zSYK>C3+;@?zVunOP8{f*9ZEuY{t8qiZcFVm;_G3G!b-EZ0VOI+pX{EBqqbpDlf~=) zDY}|(Epbfsd_42d2A6y1HIh0U)GR`p>p+zHTt2ZA^Ske9FF7;GF$?$$j+1j$W<`r) z=MS{MsHR7qr#Ls;Z0Dr`zxJXL?r-ThI_ZzuSwg6dLPfjjd*DF@GssoYrR+@Ca}W}9 z0xQ;HL6a;>sJ1GQEOhUdd8HSng~4gZ_NhZ+)vZY@;~64l>b0(BQ1233ANP9>(w>o| zL|LM!SwDk?R9`EwKXGGK-tCYiY2N(aXmGC%T{jl9%hZ+99ZR*Yv!uIqc-(_)J5WWf z;;+rxdeRAB-rfV)P3xpFVi`546KV!xE`^wqDaa|=zf@!GJm0zaf24>t6O?#AalIy_ zXJ{g47IgmO#w`9a)sO=VFR1U6yT#0jY+OQak0bgkH+* zu18=GFSKaQEgcyx=G)tNz(6MhhQ(8fGD%4syQJcupuuvV|A^?+6>CDgbGU);FqALU zAqhT95zETo7}|(CJlKmpr+hzZiB^CI(R581E56@sxERN{7exG2z-AHs*4KAJ)Ut{h zcP%hSz^glNZ=RYxN*k-qs|9J&B90<$E-1jRt)R)q$M0|Ic`IZD@LOfc$SLgRND0zy zrud!_sC;yNV(e>#xlqOKo@$+vxgYG3VsC(oPAoEOhr zkKN~->;7CYMt36TG;nr&Ym$}Fk5i2iHq+nlmno!}Af$9&#D-*j9yjAtm)HAWPpa_I z70r>__ow?_J##XjB~Okw$tpi~ebEi@tizc)I0GBh{uH=Pbfw=SkG1iSJK>>@Pa6nA5|v=GKKDLDQir>Ddvc~jM@2d$q$~zuwt!4ih0JN6@91@PSY&S1EN(WQ$eaw@h&{GhS zg*JqT0R~5(`d*RB{Vjzj@=J`nanoOy!(;-U)o=_6fN+XnMOI84gr6Tk zImocLSYwFlPVlsV+|jz&guk2;5BYTeH|;W`JbMDprIAG%7B9s#V75czYSO-KhrSq> zkr{&&jfA60YO#OfA^~Y7n_kkWBA!t!E}41+{MCmyiY!rPPh!+l!X6*4W80H^cDE#ns{ONBSKdS&1;a2y&>+6sjypE0}7K2LLHV5Smw6)t88A%vM)X z21*Xp7{X(C?c~idlShwE=uh6`F}&eWp!CQR|2G1YfP= zdNe1t)i~*Xm;!eD`xdFEz4W$ih!;0+InA2)U<$COwDO0^@lOGi+t^y)LX~Hv%xRog zYB?MD=UU6H)(t9f-JUiyAzV7+j%jlQM@P7N!{IlTA1o9_~q zFS~Wj8c)E-o!@~o`C1PhOF1$lm0Ge6lU{rkium??El{VCo^_8u1+2gOl=376wJel{ zy+g7nJk&Naf7fT};c8IYAk{}^X*c^>I4r*>8}TXKj!W=O3NX9*a@PP|pzWnO?!7=Y zJzG2YoUzIBTfc~U*1_2gCkGYd03Ri_xQSzRcqSQ*NAzAq{onCFuS!A2cxJjZ#(0(% zKh1OoEFMbscW)8cAye{t7+$j}mKq`#3$*?<&Q zmr}!+p#g%)TL-OQdR>{T_f0c08dWV6=)5HM<9ONUrT!1!Ay`7H1OX(CYl{wu?9eg% zL&qOiZTJ_q{1h!&r@itnmLpB+UCdd##7@N)>G3)^Iq+WL7)CN#-KlONdZ{@zcs^2M zlKU(*jyiLv9#uRjn?h;Z;@$NRz<9Ci(sS&IS>!Qefcuh}%VHE6Mb$?St)~pv0mtQ> z?>o1mQ#|Yc2va6&W#mvP3pp#gWGM4E8@@3&!Z#OtYUWkoq;7vsGSDLa$2xXn`AuN+ ziiKyL9jqtC585(Q9&RcY-0YBfqUG&RB6rm1Us6wyN8dm7u{3i1Jih)ByG7hPojciX zZb=7Z5LeMr$?6Qk#m%^Jw7F`wM6nWR(DrdBmz}OnIwn7FId5T~7-X68PQQ=2r4mfv z>M<0TI(z~2UUKMuj2bJPqPo(9n&{RpQ#(J2FR7;FsQPO_-YW>fw%2phOe4CV8@C>w zeoi@z)-HB!>U0z#DMQH`7*|rI1-K7^^eN16FDN>WGleP`-A!VX{77?xpqO&uG0PfL z#FK4y`xJFY=dIHs4XS#qk;hPe6ORcfT>tx$aXDg%hIt~Vwks({Um=HlG+-r}?HX{- zBLC93=MflPTc79)noSI95B4w@TO*sg;dIU1iOdyWkBfdjqI|5t@@6ly*uQN+-K=ui zXQ{9Rl0v=xxM9`JW5r1~y(Q4c%^ZcZ#g{G2r$c&il7a}J8IEN`pr_kI^AKEAptCRXH_y-F z&;+5MIQ^{txN}jq1jOVi+VHTVi5y@=E<|j54Bl#5l=wppY|LK!d_QT zp@1>x4;yBj#)&g;qk&%4U9!Ic9`G!`1G1_P@Zbav`v_BvrEX7w177cnyB_&YXfe)Y z!o-7P`c_;Rd6&9 z7^PM4SLiLNN+7oj6)%mqamwkJ)jo|5*d>8J^7zybUeqg|`yiuiBD~wl01v%0(pyS> z{&??}eG2NCKE<6XoR4%)UFYNYZsR04ot`+w#%U%~)~=)(I!jv z?HTro;aW_oQhJ*XiCcEj8@-l>nJ*2nkau3v-f+x;Y!XKzi@MBlD#!57lUV$?afRE7 zW#U7^fNkXNSTsqpe8@>((n{;ucGL>`y!r17$w4Sf&Ta7g!|JU4;6s6DOG%SZjl^MW zfvyzl=CMeT#K`>qs#~usTr~iJ^7n#_E|h+9c$GH)pnGWf?OI?CoLJy#KYsX>qrCKD zCd{r^*duXKDN^2y&CBp+-2KG8{;0C;qgY)N={T1$}GwTlNJvSD}=8O$!AWp>cPEy9OT@zohpA zmXi@abi__>hF&ZGb*~ciwx%LIzIY5Lu{Ljb^i%%8-RnIlSAVk^mMA>Q1Uu<7Dp%R# z5FwLzd_nXsfKX?D8`(35%+V?NX4NsnW+vm}I1sq!yaJ-JGUz5r456cZTzH)r+_>rh!sQAXc-O>JxFMNxgAd3iSk{p#w{)EsS4{bbnh@q{p)tKQQ5<2Lm;n z^z>lsv7lil#fCu9bgw(uu;6^f&E;w8!F4gzHQ~C+7#7(g0xRc#=y4l;B$KfoANWAg zobq^X*rZXI)M1|aYvV%%vHo(NXngX{hmAv}iXT)gd{?%0*MM>A#)fzq@vjFR7=co$NDWQ>GT`)VH@ z{aA4Gwn8&k0w_#r9?7b4Qx}d6I?Hy1HVQIL?vsRZtp3WZqqtpOHL^m>i@2KSdR0;rko8`dPu~PF<3(j ze0+rB0>oa`SG+1J<2S2Tz$d3{T?ld%1;D+jpcVn)@;tus^ zkPe5+7?fd*DljR&lv#cTs2OKud798Ex_vtoy*zjq z*wezSJpw&6nBDPrmR5e{@$%iSFoK6>w>A%yEBVWPROkLHOjc9 z(|}6Uu>l_~fF1L*@_Az&w)s|Jjs`B?FXoHpq1n0q5qpcK6LQ`XW4R&&S?36Qm5f&Z zL`7CL#u(hhW+2PGb#0v~TtL?cGY45l%cc+8$J7plXb;1hStYi9{bv00W~Q2GIn(Muiv zh5+F6w4E=In_O91xvY%IHcavX7(gXK_H63Zcq=GxJ%G`=4FIQSQUz|{To|*|U?-%C zl`zSpj7vO;i>Kvo8<~^KeZ5H!>3_nA*LZoPCt-iUXJWnNnaiwrJUyWdl51;tulhhr zo1qg}CK(RLVo{J{DK~Hv4~AGvqgK7pepY?26Tii3SAK0&X6GP_MzxX52`gYpk=a0M zCL3`&#b@KiY&cUwWMm#YMy&k-Ibn??CwE}|O8hB+2`f9K!IZXNA9Gyysf)!jNSq*@ z(Ftg5JviokJ#=K{)C>4DeNs=$T);N1tKi3oc{NP%K;X%Qbf5?_aTBERoH>hxYEYh& z+VH26;Z7$wu@3IG7?gE_K5(1!a1wOL;^TW&JE#i1z|FGRxM?l+kJnKC%f5$}*rgm3TYwYtI z5R_fGsyzjO8&3JG9I@UsW1i49rpwV=>2g%^)lYg+MAL!cgFZcWdq3X?n4ToU^E&V% zSbtXB8V8|=dO^`uRjz)sfQC?Llo{t34k$VyXjA+@VNAujk+9n{)ELU7Wf2$w6& zF}?Xo)Kk^|EOkq%QE)N z)zj+WBCRP0!HKdHbKw=t7D6n&`6nfI%>7>r>@eK4;9!F?O0)de->hS(+LE+2 ztFpH!QZe?GRceS0+$0nM%=$oL_Iz|euvG>Yq$_moU{upl8wxjSt?J!BXR}_{rv+4F zF_Mu&J82L3sW!VM7}H&EY?}1lWmE4&^uA-G*BV~T~3a+1#Xyr8qV_^t9jjc+2hLN$FeX@)8d*+=6=p@BE)pp#a>tq`L@b* zJsU`Vc8bT5Jll+yAJ&#M?#G%XScGEtXUPUiH06F9Y_bo*a;#1(sfOiXK_{`PQ=SHR z!*eqOS(-btnyEiws4akhWJufz(CddM7wc~kd|};(g2nFrbDk^k0=5Dde$7EzgSHXc z4oS`O;g4YwJvS4J38*?#zbpx)%#dtZ(9~XPTF?k^QW!)QL#DGV0FPQBi)W+u<4*Npt*0(;A&y2lb8Q4|0eR?Z}smZrPC4G zwTDx#=9woRElA)jbj92?wRJTFdiV1zhWYA%JIecc*`lV;XA<;?(_ zDVGErlpl9lEM`JXh-Dm-?%rxrbyk6%%^2jE)aGIS>ZT=vj~663fPV zEu*|yjj?&BaO+SO&DtQSuHXX4Dqk~t`Y-CRru@n;IXED#7$Zjiky5`DQ(DPk%;nM*!?T;HGTZ^SAUjv*-&~grWiI0PRrFjYHmfvPi(6q4+77b!91iRoPAADq|4bnVq zLBx?pOHwrQ^CEi+pHheGkFx9VV(h}5+tM%*R?`kz=3h-YG{s5!%{V|5;HjG;3NCvt zGKC-QyJn$aFp@0`oI>MFHT=isVQHn2Bed{2D}_*UtP@t!4z>uxO~q1T?R?{UE2Mt( zM}Yh29uMsWr;>%Y@G0!mz4M=ABJq0<=)A!dlQCDCX_bqmI<@mc5}BG?zZpdD(Mn4t z#bC^tDJJRVRUKnx5#A2bgfG6xGE8orb>`#(XGXXqzsfCNcn<*k!JNxs#SW$ zajgAEv-)Q_md`%wa%b6R_gC^4MhvGVlh%$ixzjN>PliPtk`@|s)fr#4eFJq%+93J$ z;SYd>NHr{?P{$XM(Gg?VJ$^B06WzSN=3+DVqpbObHdne5xPS_^gM~P8I`{k6-rMIxDGMgpRCA=@1h3jERR89D#pG!U~) zmg|tjZGW9;D#tIP;i?x{*GwVtsPpr&&Abq11DElE6LVh&eg15u2qOU*Hn84oB!`_4 z8`v=m0ASTG@WWzf(a+>)D?}+=Z!kwykTew<{vmS{qb0M@xvWOn-S}+wNrsAtFyy}| zv%HCT=2TC8q`nw*-tBcQ@-Xvw#N6OlJP{A*P!_TtORg#Tq#p1gd~4oGAKz?W-wckx z%Qd22T>jL^_~ICa?l_^v-XGx-X4aq2uJgKH&hmKh#m>g=1yReoYgWqQkZ*Lecw8ZS zGVe)1o6}8vE)cz_)>h-{*kYyRDiB+2iGsV-TGI>CzZ=g$VP88&Lrr{w@0Ia}53G=U zexE5USm%??>Q>Dl>3W78Gg#XolPCpT~=K$gPMm%Z%bWea2M-g3kUn#gj7d?6cr zz^Lsy&{h+S@n1HqS(syYNE}a{*etQ2{s+KWoNC>>`t}O)((;wls{%hqNC9wqNcd#1 zXST0CNKAoX&8&En(@sNG$q?_4)j3V&A>QwOEd#5ETDN;vXAXGU*FDrQ+J|cS71Q$Q z_GaZQA;L0;Lc=)7m}gi-si8gzrbO778Zl_B8;9bfzEi_pJeW6GDxJ=)ZsH2QfV*ad zE+o@av$Q?Nzk_x}^<*xbY=_1^(u~$mv(UE!&$wxSGdh_L!v^hkSUIHmSqZE()?@2M z^2202_j#2LQTVx7{6&ww`FeofM8gGmsauaDGztJrb*!@3j{-=G>RIUVV&}|2N5`e?{|o3rafxoq%f29HXy5W+T4 zk~3=!m7Hh_vk;uakZtK9VF&?P4aoPDf##?~=E|^bP_OXvX%sSh>WOHVdPshqk7qAg zg`IP41&pu0HM~-ZnCrqTo7CJo#GR7IW%@}8+w*g(O0f*{od~E1+F&JN(QW% zErSm2$9men=cf2uhQ6e%u0q*L#+>meQ8Yy=sZl76gtzIPxqhsD2x#{;6eGGZML356kq}^7i7@Ff?^A5=Lu*@+5n!*-4l}~;HU8Siz$t% zSRx-ow?NyI8$Bd<6`^ExgzHJncAcMY?ZIO{*$rQ@JuJhQHRpEI(GEvatQCnepYg4e z)H}KAavg9a$$!c?Wr}x!#qWuwp1C z3OrXs^Fo{1DToS)*B|k-n-6E!VCNLRgmId+sX?qnN!iTgR!_4xexEi84;j9%aLQ4T z4S|xig`m z(@;5N^+q_UIimoMm1c+A$KPCEN;`+U9rQC9Jpi3u*-PpN97W8{IgR^17U$9AnOKko zJf`*E?$s5>PL@noyFKu$(aGFPEJ;Fj`-rm2DDzXyrLfr{#??ircY2@zpd-X^IykuG zO(Dw3S=VJ9_Oo;QhKK$J^pJS1`@_%Oj&o7j^AGK9?Gx)^eNyne%E&MU+IO znfNKK$7iV+&MSt(?im`XZa1yMQj?YtA5W$x!Npt~#88mZ5PQm06fp?LhZHy9()_La zH7Q`}deUZtaaBCDc(EmFdGzLu&+HUf_`t|@aZ*3G2#F1<&oxmEN2z2vXP~F;~5`6c(1B>zZ zh+3VA-@S|fSmqTYSR3&GsS-N`m4AKOFcvfoA&KqvOa0InE^DDi&4fA~)o=-ZUZe+W z{25{rt&6H0qQV@BNgXGRpDovjRc`rfj@U*}uRhoVkTRalu?Rnu5t@_l>v1CP9+3`h z_rlh)Y+Qw625;U5p`v&FO~5%1sP21iMzG;h;<9MjOaH2hJ;`t|70C>dMtJSaY=+Oc$DJFMYm>L~^`e5+;!CSI5X_nNT@mL{D_&Gzrss zsRu;&OZlDheK* zBxUqOvc#B|wD$|Vn>(0nel{2(}zS^jt_W2t!BB&Tvjhd0xC(L%GD3+l6&rTtzg zf{yS_?M6LB&iOKzgau-q-wU2^!Z!0P=_T32Hy!>9JXJhTB%53u{zMdE&iT? zk`Hg%H_NsZ=6z{j-Y{b2P12wGs=W0$zN7TL!uPJzmCWQ%^QR4U6>buzvyQ<(M+jnb zSj26a&CkWZZ(6>kXC(PZa=pHZ9O0XKt7lcNb9ez_>ZsI^cR`}5wQ{Q|!I|uW*sTea zE-g=eM_5k_qT(&4M$f-opSi|r=CnIz@KTA3U#2X72j;NMkR3yT1@CM$?#8OUPWsTC%HB?beYw(^8xyFq)fYX z%xk{BakrEBs!rosnz)83i0NEYZq9bupVhN#7j}Ncl4Cb;n>5FBkmaYj#HpoCfD{F( zrWWLl6k-b?5SM_a0CE7Gt=zLM|d_olE;r3iTSDKS;j z{c(5KHrJFTo@PIfK&KU<0RJGJTdA3+nJkP%^jsMjHi_kk#r&IIHj@!2r`tF<0j%Hf~kh%hFDp)%kG;FT`C7 zr+Mh`^&IMO0xpiOwyLXKow3@i#`sIx=4pH>AyaqkT$15Ws@2bAs*bCcB1cn%a9*7z zFnrFg(p;O)#anU$oAX&LUj@X3$mQD^IYfrU#ZNfTzNKyF*8B9~<>BM9{a4gmNNL84 zp;eSBrLXGKaUR(%ok#s~K}~+if!P=NqrPL`Sz@%D_LNB?d8#`1Vt8+L(P?rd-B#k?guPm084WRbr}BkSjT2iyjmz)^m{vd_%fFFUW$4~f zGI48p#2jocSgE71wlpo^K*dN%cIb~er&^Z-r(i$O>S)y4ZN;;cW?AT0ee$AHl1x( zwoVDJcT!^8LqNR{8;J#^%t`Sr;)ptA36_a#+lgpr?zA0kh-xx!H1~%oHx%FS4%$Q4 z+oFdCK05k5G2Pf&l`GhtJalP319Mrg3=tjAO>eDL@mLC7o zF-^C-3+`e5}fbTm(fBzs2JLiBL<(n`AKgh%u;(w{`I28U~cvSeVV z(L?=|3a)u$Z0txrI2uQnWDbXR&XgiBn6k!iBOh)YLpXx8-`qP2L?|>;CRVI$9 z3YNq2>P+}IGiSP=^epaM!?iU(WKWaMXn3e&u}m>Y`jdA!kM{Y8*}$XQhQw>(zvXo8 znB^UtsQp@vcYiOL4r?-asBNF5zeo>KjX*=G5-ldv8;1LyT!hUzMVQanhjZmDjeCs{ zVh?KY|7HKe+~G~GaWc}`V+8s^3X)z&WE9T%GbA-6(j83Ba8G#%#9i^?;KH_XQ-vr? z(i+Z2wo|$WdJ_9oTfcYssDlUdD+CTAQz~;#p{f9hn6cFrAs1|qAEATGufmG;rYqI8bdk@> zrd=PpL$gTCcvRSnMyF;Yxgb0_tICZDo%K)1uM@K^jy&+34oQ@lHT!3d%%O$2)Qq}$ zY*rSb!0&*`hlx= z(kfERJI0sqs%pn_XXSY621_tk=5wxQ%`OiKiC~wtAzKJ#x*gA&^1=%iH(@ETqIRp( z_Eb`9osLDyYYM_(IDO-t@Rx89B35?DmID}o>k||>)G?~F5*O*duVa01B7XYmh09e0 z2}*8^W%LShC*hIB`(vhlWjBo{ZYdyDsd+Tc^b}whTsQ4{esJy2z>w?uVmg=3<2h1S zqr8zu!y&RXP0^0af3sz)&uclfJqmSU9mSXK5?j+NSe62^;|-7e05X=|8%eCbv66HY z;;UBz0`OK&SJX@}*2afpMTeK9?Y^ccs3v$;WGbr!KV^|ZmK&eVVS`A@A&#Tb^;62W zv}f?watY(ETF&%^R}=*gkB+={$%wwXQ?&2+IN~>}yVjdWJ8X5@!7Lu&-C(0p!~!q} zxQ-^)h%$4`_GIHo1TA|HpX({w_}Imlejk`&*w*iA%exVMXR7}YnuTtBhdm^sF`ib5 zXJ$$fgk3y%o?oz5P?lMt=;#+vx0qW~nhfC%?|&x6z^Z|_4JCc__lL1{Jn}m~%JUTpM;b2x=cXPipa;UFUbvt$M*cs&cB%d;G-+6o1D!az|JwPwd)6mDiwS z`O9g4%#Ft^`uxFCyRED8d;LHN<+=1wsK6N)mh+qMUZR-m0`NTT*Ux*-R=2lfAbo74 ziHBG$r8>J24Q0jVyHQ3!JPQ}u@rnW@p;V><5gB0lEghvb0xo{r)l^Z4ftWL>z^sCn zA{0rpv2_d<*~10$Rw6SWYCn~B5gxrro%DYIF{PQ8&E*y9(5&)+y>s8ilJ|$ps{wIe z%{T?0U^^%^xPPzt&E;{?C}&V~(-z0GlPP)}G4cytyX5L;Qq~KbLb~9b z>CEr1W{=h_6}Ln<)0xc>K#?!I#2V8mMnKAXq5|#e@-SP{#sm24njwDrdLcB&uP#rm zh#f-davz!9#&&wKG8_U3(An`>A2wk2H@eJSZB6%OnmZo-5KaJcO8s^&|KM-XgvZE) zV?E~5wytH6Pp)oI0bj*Fn_|mTpi!SHO7+yVBPOEz&{_2%|EVdk3d#VB|L!N(hW!^o z1NSskNrS7Gi@}R?m8o3>CaK~w!F|cT{yxP%e>Cdt51MuZ}Zb zGI#{BN&KX;oaUSUrbvm(TKa9jTgwyN-BMb0jG6jxjyX_qS1n+I(=_-SHTR zFgy4Mkba&USd^|8)!{iaf)UmQl8z7y4l_J5%1)=7ji)!+ZvOHzN-g(Z5|XEJv>;w! zjLrA#am|_)uNDmG7@{QXy3G zQ5+s6hz(=9=wf;#lt4Ub=s(b2_SC&eAc9zl8@C!ijuYUOs9=P%%Jfn2WFUxgXpnl@ zGj@4mEo>ci_ZG38#aGo9C&1vr^(sQAFsugtQU~SjP|9@OWClgi%NV5mic++0(Jn&D%ziNv1-+sRBZCJ`QY!!Vm)zVZr(}P627>kU0kz1+p>M|#@M(B7C zhZf)ec9b3~bWwhy_mx`X=%!KbiN5A(VG>+!Sx79#X0M~b2v}*v5hJTMN^R0Tm6uh? z_Puu3d^%nLvo;NklAuu`;IcPe1!XtYpZpY9{+rB1*fE3Eow~h!o!IqFHeuACR(qkG zCmf@T+p9nB<4sJVy2}^{bUzGW@o|##JqdnR4%1`+YnNEgXp@t|%{WYJK(f9jaLIQny24l^)N`SjOj|xa@r>yc`C;O?gotLkzuZu=f@SP% zzK9>J`>YQvj>)U!+4M7z3AC~pS~Xw(2tm>&ek3McZ^Y93(oGlj6#olKRjvx{crs4?{g0FP-u-&%<-66c}y>#(hBpI9f6K@ zMnTFGAn*2}ZROZn@Xc7I!N%iE25N-(lFdD1FZeKR7S9L(;8T@*y7o6 zjQxK1;Hg<1IJQvPYFt2~F~)N)>DYU5-Pe&#{DkQk#=qHYhsCS=dFuRqoAY1f;fv*_ z!0~3Tjzs27DEG0lj|713SYedI8N>U-5TA@2$F@bv&6*poUtaiwW{ST?$Z6H9L*~abVtze4IM>!Asq53CF@Ga zy=JGzE|GzU5~Xp?5g%Du#tAX6N6Uk{y>o?{|GfK3*ljf4xc2VkBd|W}bEB)12r#r% z?jOJy!I0gsIPscpe?tHN+A8?JbPoRe_g@A6Rp4I*{#D>#1^!jwUj_bE;9mv)uPQM3 z=1)E(tZq3uKmFKq_RdQH_lrpD9{`B!d(GXSFzknrp=r=9U)VnYK9k9x{{YaR{tW#C z=uFPv{|DeH@%tgGHw1h1?|%$?`mVA2GA%4_y?b=_zu3wrd;b80{sEjp_J2PRRKEU& z`~z6)Y3TkRqlPvFp5FuWi_m8mO>Z71_jf_v<8SX49^lN^Ki_`*2XIr*U-v(Tod3tU z)mXor{JZA)^XcDf`M3G^9qW59?~nfTesting documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.litongjava.whisper.android.java", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/examples/whisper.android.java/app/src/main/AndroidManifest.xml b/examples/whisper.android.java/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..f4980ad0 --- /dev/null +++ b/examples/whisper.android.java/app/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/whisper.android.java/app/src/main/assets/logback.xml b/examples/whisper.android.java/app/src/main/assets/logback.xml new file mode 100644 index 00000000..1bd6d921 --- /dev/null +++ b/examples/whisper.android.java/app/src/main/assets/logback.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + + + + + + + ${CONSOLE_LOG_PATTERN} + + + + ${LOG_HOME}/project-name-%d{yyyy-MM-dd}.log + + 180 + + + + 10MB + + + + + + + + \ No newline at end of file diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/MainActivity.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/MainActivity.java new file mode 100644 index 00000000..b85d550d --- /dev/null +++ b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/MainActivity.java @@ -0,0 +1,107 @@ +package com.litongjava.whisper.android.java; + +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AppCompatActivity; + +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.widget.TextView; + +import com.blankj.utilcode.util.ThreadUtils; +import com.litongjava.android.view.inject.annotation.FindViewById; +import com.litongjava.android.view.inject.annotation.FindViewByIdLayout; +import com.litongjava.android.view.inject.annotation.OnClick; +import com.litongjava.android.view.inject.utils.ViewInjectUtils; +import com.litongjava.jfinal.aop.Aop; +import com.litongjava.jfinal.aop.AopManager; +import com.litongjava.whisper.android.java.services.WhisperService; +import com.litongjava.whisper.android.java.task.LoadModelTask; +import com.litongjava.whisper.android.java.task.TranscriptionTask; +import com.litongjava.whisper.android.java.utils.AssetUtils; +import com.whispercpp.java.whisper.WhisperLib; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; + + +@FindViewByIdLayout(R.layout.activity_main) +public class MainActivity extends AppCompatActivity { + + @FindViewById(R.id.sample_text) + private TextView tv; + + Logger log = LoggerFactory.getLogger(this.getClass()); + private WhisperService whisperService = Aop.get(WhisperService.class); + + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + //setContentView(R.layout.activity_main); + ViewInjectUtils.injectActivity(this, this); + initAopBean(); + showSystemInfo(); + } + + private void initAopBean() { + Handler mainHandler = new Handler(Looper.getMainLooper()); + AopManager.me().addSingletonObject(mainHandler); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + @OnClick(R.id.loadModelBtn) + public void loadModelBtn_OnClick(View v) { + Context context = getBaseContext(); + ThreadUtils.executeByIo(new LoadModelTask(tv)); + } + + @OnClick(R.id.transcriptSampleBtn) + public void transcriptSampleBtn_OnClick(View v) { + Context context = getBaseContext(); + + long start = System.currentTimeMillis(); + String sampleFilePath = "samples/jfk.wav"; + File filesDir = context.getFilesDir(); + File sampleFile = AssetUtils.copyFileIfNotExists(context, filesDir, sampleFilePath); + long end = System.currentTimeMillis(); + String msg = "copy file:" + (end - start) + "ms"; + outputMsg(tv, msg); + ThreadUtils.executeByIo(new TranscriptionTask(tv, sampleFile)); + } + + private void outputMsg(TextView tv, String msg) { + tv.append(msg + "\n"); + log.info(msg); + } + + + @RequiresApi(api = Build.VERSION_CODES.O) + @OnClick(R.id.systemInfoBtn) + public void systemInfoBtn_OnClick(View v) { + showSystemInfo(); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public void showSystemInfo() { + String systemInfo = WhisperLib.getSystemInfo(); + tv.append(systemInfo + "\n"); + } + + @OnClick(R.id.clearBtn) + public void clearBtn_OnClick(View v) { + tv.setText(""); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + protected void onDestroy() { + super.onDestroy(); + whisperService.release(); + } +} \ No newline at end of file diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/app/App.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/app/App.java new file mode 100644 index 00000000..afa3452b --- /dev/null +++ b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/app/App.java @@ -0,0 +1,13 @@ +package com.litongjava.whisper.android.java.app; + +import android.app.Application; + +import com.blankj.utilcode.util.Utils; + +public class App extends Application { + @Override + public void onCreate() { + super.onCreate(); + Utils.init(this); + } +} diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/bean/WhisperSegment.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/bean/WhisperSegment.java new file mode 100644 index 00000000..e529fed4 --- /dev/null +++ b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/bean/WhisperSegment.java @@ -0,0 +1,47 @@ +package com.litongjava.whisper.android.java.bean; + +/** + * Created by litonglinux@qq.com on 10/21/2023_7:48 AM + */ +public class WhisperSegment { + private long start, end; + private String sentence; + + public WhisperSegment() { + } + + public WhisperSegment(long start, long end, String sentence) { + this.start = start; + this.end = end; + this.sentence = sentence; + } + + public long getStart() { + return start; + } + + public long getEnd() { + return end; + } + + public String getSentence() { + return sentence; + } + + public void setStart(long start) { + this.start = start; + } + + public void setEnd(long end) { + this.end = end; + } + + public void setSentence(String sentence) { + this.sentence = sentence; + } + + @Override + public String toString() { + return "["+start+" --> "+end+"]:"+sentence; + } +} diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/services/WhisperService.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/services/WhisperService.java new file mode 100644 index 00000000..7b97d3bd --- /dev/null +++ b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/services/WhisperService.java @@ -0,0 +1,101 @@ +package com.litongjava.whisper.android.java.services; + +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.RequiresApi; + +import com.blankj.utilcode.util.ToastUtils; +import com.blankj.utilcode.util.Utils; +import com.litongjava.android.utils.dialog.AlertDialogUtils; +import com.litongjava.jfinal.aop.Aop; +import com.litongjava.whisper.android.java.bean.WhisperSegment; +import com.litongjava.whisper.android.java.single.LocalWhisper; +import com.litongjava.whisper.android.java.utils.WaveEncoder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.ExecutionException; + +public class WhisperService { + private Logger log = LoggerFactory.getLogger(this.getClass()); + + private final Object lock = new Object(); + + @RequiresApi(api = Build.VERSION_CODES.O) + public void loadModel(TextView tv) { + String modelFilePath = LocalWhisper.modelFilePath; + String msg = "load model from :" + modelFilePath + "\n"; + outputMsg(tv, msg); + + long start = System.currentTimeMillis(); + LocalWhisper.INSTANCE.init(); + long end = System.currentTimeMillis(); + msg = "model load successful:" + (end - start) + "ms"; + outputMsg(tv, msg); + ToastUtils.showLong(msg); + + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public void transcribeSample(TextView tv, File sampleFile) { + String msg = ""; + msg = "transcribe file from :" + sampleFile.getAbsolutePath(); + outputMsg(tv, msg); + + Long start = System.currentTimeMillis(); + float[] audioData = new float[0]; // 读取音频样本 + try { + audioData = WaveEncoder.decodeWaveFile(sampleFile); + } catch (IOException e) { + e.printStackTrace(); + return; + } + long end = System.currentTimeMillis(); + msg = "decode wave file:" + (end - start) + "ms"; + outputMsg(tv, msg); + + start = System.currentTimeMillis(); + List transcription = null; + try { + //transcription = LocalWhisper.INSTANCE.transcribeData(audioData); + transcription = LocalWhisper.INSTANCE.transcribeDataWithTime(audioData); + } catch (ExecutionException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + end = System.currentTimeMillis(); + if(transcription!=null){ + ToastUtils.showLong(transcription.toString()); + msg = "Transcript successful:" + (end - start) + "ms"; + outputMsg(tv, msg); + + outputMsg(tv, transcription.toString()); + + }else{ + msg = "Transcript failed:" + (end - start) + "ms"; + outputMsg(tv, msg); + } + + } + + private void outputMsg(TextView tv, String msg) { + log.info(msg); + if(tv!=null){ + Aop.get(Handler.class).post(()->{ tv.append(msg + "\n");}); + } + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public void release() { + //noting to do + } +} diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/single/LocalWhisper.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/single/LocalWhisper.java new file mode 100644 index 00000000..bbf628ca --- /dev/null +++ b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/single/LocalWhisper.java @@ -0,0 +1,66 @@ +package com.litongjava.whisper.android.java.single; + +import android.app.Application; +import android.os.Build; +import android.os.Handler; + +import androidx.annotation.RequiresApi; + +import com.blankj.utilcode.util.ToastUtils; +import com.blankj.utilcode.util.Utils; +import com.litongjava.jfinal.aop.Aop; +import com.litongjava.whisper.android.java.bean.WhisperSegment; +import com.litongjava.whisper.android.java.utils.AssetUtils; +import com.whispercpp.java.whisper.WhisperContext; + +import java.io.File; +import java.util.List; +import java.util.concurrent.ExecutionException; + + +@RequiresApi(api = Build.VERSION_CODES.O) +public enum LocalWhisper { + INSTANCE; + + public static final String modelFilePath = "models/ggml-tiny.bin"; + private WhisperContext whisperContext; + + @RequiresApi(api = Build.VERSION_CODES.O) + LocalWhisper() { + Application context = Utils.getApp(); + File filesDir = context.getFilesDir(); + File modelFile = AssetUtils.copyFileIfNotExists(context, filesDir, modelFilePath); + String realModelFilePath = modelFile.getAbsolutePath(); + whisperContext = WhisperContext.createContextFromFile(realModelFilePath); + } + + public synchronized String transcribeData(float[] data) throws ExecutionException, InterruptedException { + if(whisperContext==null){ + toastModelLoading(); + return null; + }else{ + return whisperContext.transcribeData(data); + } + } + + private static void toastModelLoading() { + Aop.get(Handler.class).post(()->{ + ToastUtils.showShort("please wait for model loading"); + }); + } + + public List transcribeDataWithTime(float[] audioData) throws ExecutionException, InterruptedException { + if(whisperContext==null){ + toastModelLoading(); + return null; + }else{ + return whisperContext.transcribeDataWithTime(audioData); + } + } + + public void init() { + //noting to do.but init + } + + +} diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/task/LoadModelTask.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/task/LoadModelTask.java new file mode 100644 index 00000000..23fe4489 --- /dev/null +++ b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/task/LoadModelTask.java @@ -0,0 +1,44 @@ +package com.litongjava.whisper.android.java.task; + +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.widget.TextView; + +import com.blankj.utilcode.util.ThreadUtils; +import com.litongjava.jfinal.aop.Aop; +import com.litongjava.whisper.android.java.services.WhisperService; + +import java.io.File; + +public class LoadModelTask extends ThreadUtils.Task { + private final TextView tv; + public LoadModelTask(TextView tv) { + this.tv = tv; + } + + @Override + public Object doInBackground() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Aop.get(WhisperService.class).loadModel(tv); + }else{ + Aop.get(Handler.class).post(()->{ + tv.append("not supported android devices"); + }); + + } + return null; + } + + @Override + public void onSuccess(Object result) { + } + + @Override + public void onCancel() { + } + + @Override + public void onFail(Throwable t) { + } +} \ No newline at end of file diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/task/TranscriptionTask.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/task/TranscriptionTask.java new file mode 100644 index 00000000..7477f8ed --- /dev/null +++ b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/task/TranscriptionTask.java @@ -0,0 +1,44 @@ +package com.litongjava.whisper.android.java.task; + +import android.content.Context; +import android.os.Build; +import android.widget.TextView; + +import com.blankj.utilcode.util.ThreadUtils; +import com.litongjava.jfinal.aop.Aop; +import com.litongjava.whisper.android.java.services.WhisperService; + +import java.io.File; + +public class TranscriptionTask extends ThreadUtils.Task { + private final TextView tv; + private final File sampleFile; + + public TranscriptionTask(TextView tv, File sampleFile) { + this.tv = tv; + this.sampleFile = sampleFile; + + } + + @Override + public Object doInBackground() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Aop.get(WhisperService.class).transcribeSample(tv, sampleFile); + }else{ + tv.append("not supported android devices"); + } + return null; + } + + @Override + public void onSuccess(Object result) { + } + + @Override + public void onCancel() { + } + + @Override + public void onFail(Throwable t) { + } +} \ No newline at end of file diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/utils/AssetUtils.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/utils/AssetUtils.java new file mode 100644 index 00000000..d5ac5bc5 --- /dev/null +++ b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/utils/AssetUtils.java @@ -0,0 +1,91 @@ +package com.litongjava.whisper.android.java.utils; + +import android.content.Context; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class AssetUtils { + private static Logger log = LoggerFactory.getLogger(AssetUtils.class); + + public static File copyFileIfNotExists(Context context, File distDir, String filename) { + File dstFile = new File(distDir, filename); + if (dstFile.exists()) { + return dstFile; + } else { + File parentFile = dstFile.getParentFile(); + log.info("parentFile:{}", parentFile); + if (!parentFile.exists()) { + parentFile.mkdirs(); + } + AssetUtils.copyFileFromAssets(context, filename, dstFile); + } + return dstFile; + } + + public static void copyDirectoryFromAssets(Context appCtx, String srcDir, String dstDir) { + if (srcDir.isEmpty() || dstDir.isEmpty()) { + return; + } + try { + if (!new File(dstDir).exists()) { + new File(dstDir).mkdirs(); + } + for (String fileName : appCtx.getAssets().list(srcDir)) { + String srcSubPath = srcDir + File.separator + fileName; + String dstSubPath = dstDir + File.separator + fileName; + if (new File(srcSubPath).isDirectory()) { + copyDirectoryFromAssets(appCtx, srcSubPath, dstSubPath); + } else { + copyFileFromAssets(appCtx, srcSubPath, dstSubPath); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void copyFileFromAssets(Context appCtx, String srcPath, String dstPath) { + File dstFile = new File(dstPath); + copyFileFromAssets(appCtx, srcPath, dstFile); + } + + public static void copyFileFromAssets(Context appCtx, String srcPath, File dstFile) { + if (srcPath.isEmpty()) { + return; + } + InputStream is = null; + OutputStream os = null; + try { + is = new BufferedInputStream(appCtx.getAssets().open(srcPath)); + + os = new BufferedOutputStream(new FileOutputStream(dstFile)); + byte[] buffer = new byte[1024]; + int length = 0; + while ((length = is.read(buffer)) != -1) { + os.write(buffer, 0, length); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + os.close(); + is.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + } +} \ No newline at end of file diff --git a/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/utils/WaveEncoder.java b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/utils/WaveEncoder.java new file mode 100644 index 00000000..fbe57d4a --- /dev/null +++ b/examples/whisper.android.java/app/src/main/java/com/litongjava/whisper/android/java/utils/WaveEncoder.java @@ -0,0 +1,105 @@ +package com.litongjava.whisper.android.java.utils; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.ShortBuffer; + +public class WaveEncoder { + + public static float[] decodeWaveFile(File file) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (FileInputStream fis = new FileInputStream(file)) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = fis.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + } + } + ByteBuffer byteBuffer = ByteBuffer.wrap(baos.toByteArray()); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + int channel = byteBuffer.getShort(22); + byteBuffer.position(44); + + ShortBuffer shortBuffer = byteBuffer.asShortBuffer(); + short[] shortArray = new short[shortBuffer.limit()]; + shortBuffer.get(shortArray); + + float[] output = new float[shortArray.length / channel]; + + for (int index = 0; index < output.length; index++) { + if (channel == 1) { + output[index] = Math.max(-1f, Math.min(1f, shortArray[index] / 32767.0f)); + } else { + output[index] = Math.max(-1f, Math.min(1f, (shortArray[2 * index] + shortArray[2 * index + 1]) / 32767.0f / 2.0f)); + } + } + return output; + } + + public static void encodeWaveFile(File file, short[] data) throws IOException { + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(headerBytes(data.length * 2)); + + ByteBuffer buffer = ByteBuffer.allocate(data.length * 2); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.asShortBuffer().put(data); + + byte[] bytes = new byte[buffer.limit()]; + buffer.get(bytes); + + fos.write(bytes); + } + } + + private static byte[] headerBytes(int totalLength) { + if (totalLength < 44) + throw new IllegalArgumentException("Total length must be at least 44 bytes"); + + ByteBuffer buffer = ByteBuffer.allocate(44); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + buffer.put((byte) 'R'); + buffer.put((byte) 'I'); + buffer.put((byte) 'F'); + buffer.put((byte) 'F'); + + buffer.putInt(totalLength - 8); + + buffer.put((byte) 'W'); + buffer.put((byte) 'A'); + buffer.put((byte) 'V'); + buffer.put((byte) 'E'); + + buffer.put((byte) 'f'); + buffer.put((byte) 'm'); + buffer.put((byte) 't'); + buffer.put((byte) ' '); + + buffer.putInt(16); + buffer.putShort((short) 1); + buffer.putShort((short) 1); + buffer.putInt(16000); + buffer.putInt(32000); + buffer.putShort((short) 2); + buffer.putShort((short) 16); + + buffer.put((byte) 'd'); + buffer.put((byte) 'a'); + buffer.put((byte) 't'); + buffer.put((byte) 'a'); + + buffer.putInt(totalLength - 44); + buffer.position(0); + + byte[] bytes = new byte[buffer.limit()]; + buffer.get(bytes); + + return bytes; + } +} \ No newline at end of file diff --git a/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/CpuInfo.java b/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/CpuInfo.java new file mode 100644 index 00000000..733ca354 --- /dev/null +++ b/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/CpuInfo.java @@ -0,0 +1,121 @@ +package com.whispercpp.java.whisper; + +import android.os.Build; +import android.util.Log; + +import androidx.annotation.RequiresApi; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CpuInfo { + private static final String LOG_TAG = "WhisperCpuConfig"; + + private List lines; + + public CpuInfo(List lines) { + this.lines = lines; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + public int getHighPerfCpuCount0() { + try { + return getHighPerfCpuCountByFrequencies(); + } catch (Exception e) { + Log.d(LOG_TAG, "Couldn't read CPU frequencies", e); + return getHighPerfCpuCountByVariant(); + } + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private int getHighPerfCpuCountByFrequencies() { + List frequencies = getCpuValues("processor", line -> { + try { + return getMaxCpuFrequency(Integer.parseInt(line.trim())); + } catch (IOException e) { + e.printStackTrace(); + } + return 0; + } + ); + Log.d(LOG_TAG, "Binned cpu frequencies (frequency, count): " + binnedValues(frequencies)); + return countDroppingMin(frequencies); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private int getHighPerfCpuCountByVariant() { + List variants = getCpuValues("CPU variant", line -> Integer.parseInt(line.trim().substring(line.indexOf("0x") + 2), 16)); + Log.d(LOG_TAG, "Binned cpu variants (variant, count): " + binnedValues(variants)); + return countKeepingMin(variants); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private Map binnedValues(List values) { + Map countMap = new HashMap<>(); + for (int value : values) { + countMap.put(value, countMap.getOrDefault(value, 0) + 1); + } + return countMap; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private List getCpuValues(String property, Mapper mapper) { + List values = new ArrayList<>(); + for (String line : lines) { + if (line.startsWith(property)) { + values.add(mapper.map(line.substring(line.indexOf(':') + 1))); + } + } + values.sort(Integer::compareTo); + return values; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private int countDroppingMin(List values) { + int min = values.stream().mapToInt(i -> i).min().orElse(Integer.MAX_VALUE); + return (int) values.stream().filter(value -> value > min).count(); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private int countKeepingMin(List values) { + int min = values.stream().mapToInt(i -> i).min().orElse(Integer.MAX_VALUE); + return (int) values.stream().filter(value -> value.equals(min)).count(); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + public static int getHighPerfCpuCount() { + try { + return readCpuInfo().getHighPerfCpuCount0(); + } catch (Exception e) { + Log.d(LOG_TAG, "Couldn't read CPU info", e); + return Math.max(Runtime.getRuntime().availableProcessors() - 4, 0); + } + } + + private static CpuInfo readCpuInfo() throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader("/proc/cpuinfo"))) { + List lines = new ArrayList<>(); + String line; + while ((line = reader.readLine()) != null) { + lines.add(line); + } + return new CpuInfo(lines); + } + } + + private static int getMaxCpuFrequency(int cpuIndex) throws IOException { + String path = "/sys/devices/system/cpu/cpu" + cpuIndex + "/cpufreq/cpuinfo_max_freq"; + try (BufferedReader reader = new BufferedReader(new FileReader(path))) { + return Integer.parseInt(reader.readLine()); + } + } + + private interface Mapper { + int map(String line); + } +} diff --git a/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperContext.java b/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperContext.java new file mode 100644 index 00000000..0e52ec12 --- /dev/null +++ b/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperContext.java @@ -0,0 +1,138 @@ +package com.whispercpp.java.whisper; + +import android.content.res.AssetManager; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.RequiresApi; + +import com.litongjava.whisper.android.java.bean.WhisperSegment; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class WhisperContext { + + private static final String LOG_TAG = "LibWhisper"; + private long ptr; + private final ExecutorService executorService; + + private WhisperContext(long ptr) { + this.ptr = ptr; + this.executorService = Executors.newSingleThreadExecutor(); + } + + public String transcribeData(float[] data) throws ExecutionException, InterruptedException { + return executorService.submit(new Callable() { + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + public String call() throws Exception { + if (ptr == 0L) { + throw new IllegalStateException(); + } + int numThreads = WhisperCpuConfig.getPreferredThreadCount(); + Log.d(LOG_TAG, "Selecting " + numThreads + " threads"); + + StringBuilder result = new StringBuilder(); + synchronized (this) { + + WhisperLib.fullTranscribe(ptr, numThreads, data); + int textCount = WhisperLib.getTextSegmentCount(ptr); + for (int i = 0; i < textCount; i++) { + String sentence = WhisperLib.getTextSegment(ptr, i); + result.append(sentence); + } + } + return result.toString(); + } + }).get(); + } + + public List transcribeDataWithTime(float[] data) throws ExecutionException, InterruptedException { + return executorService.submit(new Callable>() { + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + public List call() throws Exception { + if (ptr == 0L) { + throw new IllegalStateException(); + } + int numThreads = WhisperCpuConfig.getPreferredThreadCount(); + Log.d(LOG_TAG, "Selecting " + numThreads + " threads"); + + List segments = new ArrayList<>(); + synchronized (this) { +// StringBuilder result = new StringBuilder(); + WhisperLib.fullTranscribe(ptr, numThreads, data); + int textCount = WhisperLib.getTextSegmentCount(ptr); + for (int i = 0; i < textCount; i++) { + long start = WhisperLib.getTextSegmentT0(ptr, i); + String sentence = WhisperLib.getTextSegment(ptr, i); + long end = WhisperLib.getTextSegmentT1(ptr, i); +// result.append(); + segments.add(new WhisperSegment(start, end, sentence)); + + } +// return result.toString(); + } + return segments; + } + }).get(); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public String benchMemory(int nthreads) throws ExecutionException, InterruptedException { + return executorService.submit(() -> WhisperLib.benchMemcpy(nthreads)).get(); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public String benchGgmlMulMat(int nthreads) throws ExecutionException, InterruptedException { + return executorService.submit(() -> WhisperLib.benchGgmlMulMat(nthreads)).get(); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public void release() throws ExecutionException, InterruptedException { + executorService.submit(() -> { + if (ptr != 0L) { + WhisperLib.freeContext(ptr); + ptr = 0; + } + }).get(); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public static WhisperContext createContextFromFile(String filePath) { + long ptr = WhisperLib.initContext(filePath); + if (ptr == 0L) { + throw new RuntimeException("Couldn't create context with path " + filePath); + } + return new WhisperContext(ptr); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public static WhisperContext createContextFromInputStream(InputStream stream) { + long ptr = WhisperLib.initContextFromInputStream(stream); + if (ptr == 0L) { + throw new RuntimeException("Couldn't create context from input stream"); + } + return new WhisperContext(ptr); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public static WhisperContext createContextFromAsset(AssetManager assetManager, String assetPath) { + long ptr = WhisperLib.initContextFromAsset(assetManager, assetPath); + if (ptr == 0L) { + throw new RuntimeException("Couldn't create context from asset " + assetPath); + } + return new WhisperContext(ptr); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public static String getSystemInfo() { + return WhisperLib.getSystemInfo(); + } +} diff --git a/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperCpuConfig.java b/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperCpuConfig.java new file mode 100644 index 00000000..8cd2b888 --- /dev/null +++ b/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperCpuConfig.java @@ -0,0 +1,12 @@ +package com.whispercpp.java.whisper; + +import android.os.Build; + +import androidx.annotation.RequiresApi; + +public class WhisperCpuConfig { + @RequiresApi(api = Build.VERSION_CODES.N) + public static int getPreferredThreadCount() { + return Math.max(CpuInfo.getHighPerfCpuCount(), 2); + } +} diff --git a/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperLib.java b/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperLib.java new file mode 100644 index 00000000..38dd47a3 --- /dev/null +++ b/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperLib.java @@ -0,0 +1,75 @@ +package com.whispercpp.java.whisper; + +import android.content.res.AssetManager; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.RequiresApi; + +import java.io.InputStream; + +@RequiresApi(api = Build.VERSION_CODES.O) +public class WhisperLib { + private static final String LOG_TAG = "LibWhisper"; + + static { + + Log.d(LOG_TAG, "Primary ABI: " + Build.SUPPORTED_ABIS[0]); + boolean loadVfpv4 = false; + boolean loadV8fp16 = false; + if (WhisperUtils.isArmEabiV7a()) { + String cpuInfo = WhisperUtils.cpuInfo(); + if (cpuInfo != null) { + Log.d(LOG_TAG, "CPU info: " + cpuInfo); + if (cpuInfo.contains("vfpv4")) { + Log.d(LOG_TAG, "CPU supports vfpv4"); + loadVfpv4 = true; + } + } + } else if (WhisperUtils.isArmEabiV8a()) { + String cpuInfo = WhisperUtils.cpuInfo(); + if (cpuInfo != null) { + Log.d(LOG_TAG, "CPU info: " + cpuInfo); + if (cpuInfo.contains("fphp")) { + Log.d(LOG_TAG, "CPU supports fp16 arithmetic"); + loadV8fp16 = true; + } + } + } + + if (loadVfpv4) { + Log.d(LOG_TAG, "Loading libwhisper_vfpv4.so"); + System.loadLibrary("whisper_vfpv4"); + } else if (loadV8fp16) { + Log.d(LOG_TAG, "Loading libwhisper_v8fp16_va.so"); + System.loadLibrary("whisper_v8fp16_va"); + } else { + Log.d(LOG_TAG, "Loading libwhisper.so"); + System.loadLibrary("whisper"); + } + } + + public static native long initContextFromInputStream(InputStream inputStream); + + public static native long initContextFromAsset(AssetManager assetManager, String assetPath); + + public static native long initContext(String modelPath); + + public static native void freeContext(long contextPtr); + + public static native void fullTranscribe(long contextPtr, int numThreads, float[] audioData); + + public static native int getTextSegmentCount(long contextPtr); + + public static native String getTextSegment(long contextPtr, int index); + + public static native long getTextSegmentT0(long contextPtr, int index); + + public static native long getTextSegmentT1(long contextPtr, int index); + + public static native String getSystemInfo(); + + public static native String benchMemcpy(int nthread); + + public static native String benchGgmlMulMat(int nthread); +} \ No newline at end of file diff --git a/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperUtils.java b/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperUtils.java new file mode 100644 index 00000000..8e803b7f --- /dev/null +++ b/examples/whisper.android.java/app/src/main/java/com/whispercpp/java/whisper/WhisperUtils.java @@ -0,0 +1,34 @@ +package com.whispercpp.java.whisper; + +import android.os.Build; +import android.util.Log; + +import androidx.annotation.RequiresApi; + +import java.io.File; +import java.nio.file.Path; + +public class WhisperUtils { + private static final String LOG_TAG = "LibWhisper"; + + + public static boolean isArmEabiV7a() { + return Build.SUPPORTED_ABIS[0].equals("armeabi-v7a"); + } + + public static boolean isArmEabiV8a() { + return Build.SUPPORTED_ABIS[0].equals("arm64-v8a"); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public static String cpuInfo() { + try { + Path path = new File("/proc/cpuinfo").toPath(); + return new String(java.nio.file.Files.readAllBytes(path)); + } catch (Exception e) { + Log.w(LOG_TAG, "Couldn't read /proc/cpuinfo", e); + return null; + } + + } +} \ No newline at end of file diff --git a/examples/whisper.android.java/app/src/main/jni/whisper/CMakeLists.txt b/examples/whisper.android.java/app/src/main/jni/whisper/CMakeLists.txt new file mode 100644 index 00000000..668cd4a7 --- /dev/null +++ b/examples/whisper.android.java/app/src/main/jni/whisper/CMakeLists.txt @@ -0,0 +1,56 @@ +cmake_minimum_required(VERSION 3.10) + +project(whisper.cpp) + +set(CMAKE_CXX_STANDARD 11) +set(WHISPER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../../../../../../) + +set( + SOURCE_FILES + ${WHISPER_LIB_DIR}/ggml.c + ${WHISPER_LIB_DIR}/ggml-alloc.c + ${WHISPER_LIB_DIR}/ggml-backend.c + ${WHISPER_LIB_DIR}/ggml-quants.c + ${WHISPER_LIB_DIR}/whisper.cpp + ${CMAKE_SOURCE_DIR}/jni.c +) + +find_library(LOG_LIB log) + +function(build_library target_name) + add_library( + ${target_name} + SHARED + ${SOURCE_FILES} + ) + + target_link_libraries(${target_name} ${LOG_LIB} android) + + if (${target_name} STREQUAL "whisper_v8fp16_va") + target_compile_options(${target_name} PRIVATE -march=armv8.2-a+fp16) + elseif (${target_name} STREQUAL "whisper_vfpv4") + target_compile_options(${target_name} PRIVATE -mfpu=neon-vfpv4) + endif () + + if (NOT ${CMAKE_BUILD_TYPE} STREQUAL "Debug") + + target_compile_options(${target_name} PRIVATE -O3) + target_compile_options(${target_name} PRIVATE -fvisibility=hidden -fvisibility-inlines-hidden) + target_compile_options(${target_name} PRIVATE -ffunction-sections -fdata-sections) + + #target_link_options(${target_name} PRIVATE -Wl,--gc-sections) + #target_link_options(${target_name} PRIVATE -Wl,--exclude-libs,ALL) + #target_link_options(${target_name} PRIVATE -flto) + + endif () +endfunction() + +build_library("whisper") # Default target + +if (${ANDROID_ABI} STREQUAL "arm64-v8a") + build_library("whisper_v8fp16_va") +elseif (${ANDROID_ABI} STREQUAL "armeabi-v7a") + build_library("whisper_vfpv4") +endif () + +include_directories(${WHISPER_LIB_DIR}) diff --git a/examples/whisper.android.java/app/src/main/jni/whisper/jni.c b/examples/whisper.android.java/app/src/main/jni/whisper/jni.c new file mode 100644 index 00000000..f8e7effe --- /dev/null +++ b/examples/whisper.android.java/app/src/main/jni/whisper/jni.c @@ -0,0 +1,257 @@ +#include +#include +#include +#include +#include +#include +#include +#include "whisper.h" +#include "ggml.h" + +#define UNUSED(x) (void)(x) +#define TAG "JNI" + +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__) + +static inline int min(int a, int b) { + return (a < b) ? a : b; +} + +static inline int max(int a, int b) { + return (a > b) ? a : b; +} + +struct input_stream_context { + size_t offset; + JNIEnv * env; + jobject thiz; + jobject input_stream; + + jmethodID mid_available; + jmethodID mid_read; +}; + +size_t inputStreamRead(void * ctx, void * output, size_t read_size) { + struct input_stream_context* is = (struct input_stream_context*)ctx; + + jint avail_size = (*is->env)->CallIntMethod(is->env, is->input_stream, is->mid_available); + jint size_to_copy = read_size < avail_size ? (jint)read_size : avail_size; + + jbyteArray byte_array = (*is->env)->NewByteArray(is->env, size_to_copy); + + jint n_read = (*is->env)->CallIntMethod(is->env, is->input_stream, is->mid_read, byte_array, 0, size_to_copy); + + if (size_to_copy != read_size || size_to_copy != n_read) { + LOGI("Insufficient Read: Req=%zu, ToCopy=%d, Available=%d", read_size, size_to_copy, n_read); + } + + jbyte* byte_array_elements = (*is->env)->GetByteArrayElements(is->env, byte_array, NULL); + memcpy(output, byte_array_elements, size_to_copy); + (*is->env)->ReleaseByteArrayElements(is->env, byte_array, byte_array_elements, JNI_ABORT); + + (*is->env)->DeleteLocalRef(is->env, byte_array); + + is->offset += size_to_copy; + + return size_to_copy; +} +bool inputStreamEof(void * ctx) { + struct input_stream_context* is = (struct input_stream_context*)ctx; + + jint result = (*is->env)->CallIntMethod(is->env, is->input_stream, is->mid_available); + return result <= 0; +} +void inputStreamClose(void * ctx) { + +} + +JNIEXPORT jlong JNICALL +Java_com_whispercpp_java_whisper_WhisperLib_initContextFromInputStream( + JNIEnv *env, jobject thiz, jobject input_stream) { + UNUSED(thiz); + + struct whisper_context *context = NULL; + struct whisper_model_loader loader = {}; + struct input_stream_context inp_ctx = {}; + + inp_ctx.offset = 0; + inp_ctx.env = env; + inp_ctx.thiz = thiz; + inp_ctx.input_stream = input_stream; + + jclass cls = (*env)->GetObjectClass(env, input_stream); + inp_ctx.mid_available = (*env)->GetMethodID(env, cls, "available", "()I"); + inp_ctx.mid_read = (*env)->GetMethodID(env, cls, "read", "([BII)I"); + + loader.context = &inp_ctx; + loader.read = inputStreamRead; + loader.eof = inputStreamEof; + loader.close = inputStreamClose; + + loader.eof(loader.context); + + context = whisper_init(&loader); + return (jlong) context; +} + +static size_t asset_read(void *ctx, void *output, size_t read_size) { + return AAsset_read((AAsset *) ctx, output, read_size); +} + +static bool asset_is_eof(void *ctx) { + return AAsset_getRemainingLength64((AAsset *) ctx) <= 0; +} + +static void asset_close(void *ctx) { + AAsset_close((AAsset *) ctx); +} + +static struct whisper_context *whisper_init_from_asset( + JNIEnv *env, + jobject assetManager, + const char *asset_path +) { + LOGI("Loading model from asset '%s'\n", asset_path); + AAssetManager *asset_manager = AAssetManager_fromJava(env, assetManager); + AAsset *asset = AAssetManager_open(asset_manager, asset_path, AASSET_MODE_STREAMING); + if (!asset) { + LOGW("Failed to open '%s'\n", asset_path); + return NULL; + } + + whisper_model_loader loader = { + .context = asset, + .read = &asset_read, + .eof = &asset_is_eof, + .close = &asset_close + }; + + return whisper_init(&loader); +} + +JNIEXPORT jlong JNICALL +Java_com_whispercpp_java_whisper_WhisperLib_initContextFromAsset( + JNIEnv *env, jobject thiz, jobject assetManager, jstring asset_path_str) { + UNUSED(thiz); + struct whisper_context *context = NULL; + const char *asset_path_chars = (*env)->GetStringUTFChars(env, asset_path_str, NULL); + context = whisper_init_from_asset(env, assetManager, asset_path_chars); + (*env)->ReleaseStringUTFChars(env, asset_path_str, asset_path_chars); + return (jlong) context; +} + +JNIEXPORT jlong JNICALL +Java_com_whispercpp_java_whisper_WhisperLib_initContext( + JNIEnv *env, jobject thiz, jstring model_path_str) { + UNUSED(thiz); + struct whisper_context *context = NULL; + const char *model_path_chars = (*env)->GetStringUTFChars(env, model_path_str, NULL); + context = whisper_init_from_file(model_path_chars); + (*env)->ReleaseStringUTFChars(env, model_path_str, model_path_chars); + return (jlong) context; +} + +JNIEXPORT void JNICALL +Java_com_whispercpp_java_whisper_WhisperLib_freeContext( + JNIEnv *env, jobject thiz, jlong context_ptr) { + UNUSED(env); + UNUSED(thiz); + struct whisper_context *context = (struct whisper_context *) context_ptr; + whisper_free(context); +} + +JNIEXPORT void JNICALL +Java_com_whispercpp_java_whisper_WhisperLib_fullTranscribe( + JNIEnv *env, jobject thiz, jlong context_ptr, jint num_threads, jfloatArray audio_data) { + UNUSED(thiz); + struct whisper_context *context = (struct whisper_context *) context_ptr; + jfloat *audio_data_arr = (*env)->GetFloatArrayElements(env, audio_data, NULL); + const jsize audio_data_length = (*env)->GetArrayLength(env, audio_data); + + // The below adapted from the Objective-C iOS sample + struct whisper_full_params params = whisper_full_default_params(WHISPER_SAMPLING_GREEDY); + params.print_realtime = true; + params.print_progress = false; + params.print_timestamps = true; + params.print_special = false; + params.translate = false; + params.language = "en"; + params.n_threads = num_threads; + params.offset_ms = 0; + params.no_context = true; + params.single_segment = false; + + whisper_reset_timings(context); + + LOGI("About to run whisper_full"); + if (whisper_full(context, params, audio_data_arr, audio_data_length) != 0) { + LOGI("Failed to run the model"); + } else { + whisper_print_timings(context); + } + (*env)->ReleaseFloatArrayElements(env, audio_data, audio_data_arr, JNI_ABORT); +} + +JNIEXPORT jint JNICALL +Java_com_whispercpp_java_whisper_WhisperLib_getTextSegmentCount( + JNIEnv *env, jobject thiz, jlong context_ptr) { + UNUSED(env); + UNUSED(thiz); + struct whisper_context *context = (struct whisper_context *) context_ptr; + return whisper_full_n_segments(context); +} + + +JNIEXPORT jstring JNICALL +Java_com_whispercpp_java_whisper_WhisperLib_getTextSegment( + JNIEnv *env, jobject thiz, jlong context_ptr, jint index) { + UNUSED(thiz); + struct whisper_context *context = (struct whisper_context *) context_ptr; + const char *text = whisper_full_get_segment_text(context, index); + jstring string = (*env)->NewStringUTF(env, text); + return string; +} + +JNIEXPORT jlong JNICALL +Java_com_whispercpp_java_whisper_WhisperLib_getTextSegmentT0(JNIEnv *env, jobject thiz,jlong context_ptr, jint index) { + UNUSED(thiz); + struct whisper_context *context = (struct whisper_context *) context_ptr; + const int64_t t0 = whisper_full_get_segment_t0(context, index); + return (jlong)t0; +} + +JNIEXPORT jlong JNICALL +Java_com_whispercpp_java_whisper_WhisperLib_getTextSegmentT1(JNIEnv *env, jobject thiz,jlong context_ptr, jint index) { + UNUSED(thiz); + struct whisper_context *context = (struct whisper_context *) context_ptr; + const int64_t t1 = whisper_full_get_segment_t1(context, index); + return (jlong)t1; +} + +JNIEXPORT jstring JNICALL +Java_com_whispercpp_java_whisper_WhisperLib_getSystemInfo( + JNIEnv *env, jobject thiz +) { + UNUSED(thiz); + const char *sysinfo = whisper_print_system_info(); + jstring string = (*env)->NewStringUTF(env, sysinfo); + return string; +} + +JNIEXPORT jstring JNICALL +Java_com_whispercpp_java_whisper_WhisperLib_benchMemcpy(JNIEnv *env, jobject thiz, + jint n_threads) { + UNUSED(thiz); + const char *bench_ggml_memcpy = whisper_bench_memcpy_str(n_threads); + jstring string = (*env)->NewStringUTF(env, bench_ggml_memcpy); +} + +JNIEXPORT jstring JNICALL +Java_com_whispercpp_java_whisper_WhisperLib_benchGgmlMulMat(JNIEnv *env, jobject thiz, + jint n_threads) { + UNUSED(thiz); + const char *bench_ggml_mul_mat = whisper_bench_ggml_mul_mat_str(n_threads); + jstring string = (*env)->NewStringUTF(env, bench_ggml_mul_mat); +} + diff --git a/examples/whisper.android.java/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/whisper.android.java/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..5c3bfcd6 --- /dev/null +++ b/examples/whisper.android.java/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/examples/whisper.android.java/app/src/main/res/drawable/ic_launcher_background.xml b/examples/whisper.android.java/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..140f8294 --- /dev/null +++ b/examples/whisper.android.java/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/whisper.android.java/app/src/main/res/layout/activity_main.xml b/examples/whisper.android.java/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..f78b4ce7 --- /dev/null +++ b/examples/whisper.android.java/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,57 @@ + + + + + +