diff --git a/.github/workflows/build-aar.yml b/.github/workflows/build-aar.yml
new file mode 100644
index 00000000..1fcd3453
--- /dev/null
+++ b/.github/workflows/build-aar.yml
@@ -0,0 +1,104 @@
+name: Build Android AAR
+
+on:
+ push:
+ branches:
+ - main
+ - android-sdk
+ pull_request:
+
+jobs:
+ build:
+ name: Build AAR
+ runs-on: ubuntu-latest
+ if: github.event_name == 'push'
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+
+ - name: Cache Gradle packages
+ uses: actions/cache@v3
+ with:
+ path: ~/.gradle/caches
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ - name: Navigate to android Directory and Build AAR
+ run: |
+ echo "Navigating to the example directory..."
+ cd android/llama.android
+ echo "Starting Gradle build process in $(pwd)..."
+ ./gradlew assembleRelease --stacktrace --info
+ shell: bash
+
+ - name: Rename and upload AAR
+ run: |
+ echo "Navigating to the android directory to find AAR output..."
+ cd android/llama.android
+ mkdir -p ../artifacts
+ ls -ld ../artifacts || echo "Artifacts directory does not exist."
+ AAR_PATH=$(find ./llama/build/outputs/aar -type f -name "*.aar" | head -n 1)
+ if [ -z "$AAR_PATH" ]; then
+ echo "No AAR file found. Build might have failed."
+ exit 1
+ fi
+ BRANCH_NAME=${{ github.ref_name }}
+ CUSTOM_NAME="com-nexa-${BRANCH_NAME}-${{ github.run_number }}.aar"
+ echo "Found AAR at $AAR_PATH, renaming to $CUSTOM_NAME..."
+ mv "$AAR_PATH" "../artifacts/$CUSTOM_NAME"
+ shell: bash
+
+ - name: Upload AAR as an artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name: custom-aar-${{ github.ref_name }}-${{ github.run_number }}
+ path: android/artifacts/
+
+ release:
+ name: Create GitHub Release
+ needs: build
+ runs-on: ubuntu-latest
+ if: github.event_name == 'push' && contains(github.ref, 'main')
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Download Artifacts
+ uses: actions/download-artifact@v3
+ with:
+ name: custom-aar-${{ github.ref_name }}-${{ github.run_number }}
+ path: release-artifacts
+
+ - name: Create Release
+ id: create_release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: v${{ github.run_number }}
+ release_name: "Release v${{ github.run_number }}"
+ body: |
+ This is an automated release containing the latest AAR build.
+ - **Branch:** ${{ github.ref_name }}
+ - **Build Number:** ${{ github.run_number }}
+ draft: false
+ prerelease: false
+
+ - name: Upload AAR to Release
+ uses: actions/upload-release-asset@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ steps.create_release.outputs.upload_url }}
+ asset_path: release-artifacts/com-nexa-${{ github.ref_name }}-${{ github.run_number }}.aar
+ asset_name: com-nexa-${{ github.ref_name }}-${{ github.run_number }}.aar
+ asset_content_type: application/java-archive
\ No newline at end of file
diff --git a/android/llama.android/.gitignore b/android/llama.android/.gitignore
new file mode 100644
index 00000000..347e252e
--- /dev/null
+++ b/android/llama.android/.gitignore
@@ -0,0 +1,33 @@
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Log/OS Files
+*.log
+
+# Android Studio generated files and folders
+captures/
+.externalNativeBuild/
+.cxx/
+*.apk
+output.json
+
+# IntelliJ
+*.iml
+.idea/
+misc.xml
+deploymentTargetDropDown.xml
+render.experimental.xml
+
+# Keystore files
+*.jks
+*.keystore
+
+# Google Services (e.g. APIs or Firebase)
+google-services.json
+
+# Android Profiling
+*.hprof
diff --git a/android/llama.android/README.md b/android/llama.android/README.md
new file mode 100644
index 00000000..aa91234c
--- /dev/null
+++ b/android/llama.android/README.md
@@ -0,0 +1,54 @@
+# Nexa
+
+**Nexa** is a Kotlin wrapper for the [llama.cpp](https://github.com/ggerganov/llama.cpp.git) library. offering a convenient Kotlin API for Android developers. It allows seamless integration of llama.cpp models into Android applications.
+**NOTE:** Currently, Nexa supports Vision-Language Model (VLM) inference capabilities.
+
+## Installation
+
+To add Nexa to your Android project, follow these steps:
+
+- Create a libs folder in your project’s root directory.
+- Copy the .aar file into the libs folder.
+- Add dependency to your build.gradle file:
+
+```
+implementation files("libs/com.nexa.aar")
+```
+
+## Usage
+### 1. Initialize NexaSwift with model path and projector path
+
+Create a configuration and initialize NexaSwift with the path to your model file:
+
+```kotlin
+nexaVlmInference = NexaVlmInference(pathToModel,
+ mmprojectorPath, imagePath,
+ maxNewTokens = 128,
+ stopWords = listOf(""))
+nexaVlmInference.loadModel()
+```
+
+### 2. Completion API
+
+#### Streaming Mode
+
+```swift
+nexaVlmInference.createCompletionStream(prompt, imagePath)
+ ?.catch {
+ print(it.message)
+ }
+ ?.collect { print(it) }
+```
+
+### 3. release all resources
+```kotlin
+nexaVlmInference.dispose()
+```
+
+## Quick Start
+
+Open the [android test project](./app-java) folder in Android Studio and run the project.
+
+## Download Models
+
+You can download models from the [Nexa AI ModelHub](https://nexa.ai/models).
\ No newline at end of file
diff --git a/android/llama.android/app-java/.gitignore b/android/llama.android/app-java/.gitignore
new file mode 100644
index 00000000..42df58a2
--- /dev/null
+++ b/android/llama.android/app-java/.gitignore
@@ -0,0 +1,2 @@
+/build
+!*.png
\ No newline at end of file
diff --git a/android/llama.android/app-java/build.gradle b/android/llama.android/app-java/build.gradle
new file mode 100644
index 00000000..2729f317
--- /dev/null
+++ b/android/llama.android/app-java/build.gradle
@@ -0,0 +1,52 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+}
+
+android {
+ namespace 'ai.nexa.app_java'
+ compileSdk 34
+
+ defaultConfig {
+ applicationId "ai.nexa.app_java"
+ minSdk 33
+ targetSdk 34
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17 // or VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_17 // or VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = "17" // or "1.8"
+ }
+}
+
+dependencies {
+
+ implementation 'androidx.appcompat:appcompat:1.7.0'
+ implementation 'com.google.android.material:material:1.12.0'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.2.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.20"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
+
+ implementation 'com.github.bumptech.glide:glide:4.16.0'
+ annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
+
+ implementation project(":llama")
+ // implementation files("libs/com.nexa.aar")
+}
\ No newline at end of file
diff --git a/android/llama.android/app-java/proguard-rules.pro b/android/llama.android/app-java/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/android/llama.android/app-java/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/androidTest/java/ai/nexa/app_java/ExampleInstrumentedTest.java b/android/llama.android/app-java/src/androidTest/java/ai/nexa/app_java/ExampleInstrumentedTest.java
new file mode 100644
index 00000000..7f3c2198
--- /dev/null
+++ b/android/llama.android/app-java/src/androidTest/java/ai/nexa/app_java/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package ai.nexa.app_java;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("ai.nexa.app_java", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/AndroidManifest.xml b/android/llama.android/app-java/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8aaea0a2
--- /dev/null
+++ b/android/llama.android/app-java/src/main/AndroidManifest.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/ImagePathHelper.java b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/ImagePathHelper.java
new file mode 100644
index 00000000..a8b0ef00
--- /dev/null
+++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/ImagePathHelper.java
@@ -0,0 +1,112 @@
+package ai.nexa.app_java;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class ImagePathHelper {
+ private static final String TAG = "MessageProcessor";
+ private final Context context;
+
+ public ImagePathHelper(Context context) {
+ this.context = context;
+ }
+
+ public String getPathFromUri(String uriString) {
+ try {
+ Uri uri = Uri.parse(uriString);
+
+ // Handle "content://" scheme
+ if ("content".equals(uri.getScheme())) {
+ // Handle Google Photos and other document providers
+ if (DocumentsContract.isDocumentUri(context, uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+
+ // MediaStore documents
+ if ("com.android.providers.media.documents".equals(uri.getAuthority())) {
+ final String[] split = docId.split(":");
+ final String type = split[0];
+ Uri contentUri = null;
+
+ if ("image".equals(type)) {
+ contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ }
+
+ final String selection = "_id=?";
+ final String[] selectionArgs = new String[]{split[1]};
+ return getDataColumn(context, contentUri, selection, selectionArgs);
+ }
+ }
+ // MediaStore (general case)
+ return getDataColumn(context, uri, null, null);
+ }
+ // Handle "file://" scheme
+ else if ("file".equals(uri.getScheme())) {
+ return uri.getPath();
+ }
+ // Handle absolute path
+ else if (new File(uriString).exists()) {
+ return uriString;
+ }
+
+ return null;
+ } catch (Exception e) {
+ Log.e(TAG, "Error getting path from URI: " + uriString, e);
+ return null;
+ }
+ }
+
+ public String copyUriToPrivateFile(Context context, String uriString) throws IOException {
+ // 将字符串转换回 Uri
+ Uri uri = Uri.parse(uriString);
+
+ // 应用私有目录
+ File privateDir = context.getExternalFilesDir("images");
+ if (privateDir == null) {
+ throw new IOException("Private directory not available");
+ }
+
+ // 创建目标文件
+ File destFile = new File(privateDir, "temp_image_" + System.currentTimeMillis() + ".jpg");
+
+ try (InputStream inputStream = context.getContentResolver().openInputStream(uri);
+ OutputStream outputStream = new FileOutputStream(destFile)) {
+
+ if (inputStream == null) {
+ throw new IOException("Failed to open URI input stream");
+ }
+
+ // 读取并写入数据
+ byte[] buffer = new byte[4096];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ }
+
+ // 返回文件路径
+ return destFile.getAbsolutePath();
+ }
+
+ private String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
+ final String[] projection = {MediaStore.Images.Media.DATA};
+ try (Cursor cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ final int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
+ return cursor.getString(columnIndex);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error getting data column", e);
+ }
+ return null;
+ }
+}
diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/KotlinFlowHelper.kt b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/KotlinFlowHelper.kt
new file mode 100644
index 00000000..0183ff14
--- /dev/null
+++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/KotlinFlowHelper.kt
@@ -0,0 +1,44 @@
+package ai.nexa.app_java
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class KotlinFlowHelper {
+ private val scope = CoroutineScope(Dispatchers.IO)
+
+ fun collectFlow(
+ flow: Flow, // Added missing flow parameter
+ onToken: (String) -> Unit,
+ onComplete: (String) -> Unit,
+ onError: (String) -> Unit
+ ) {
+ scope.launch {
+ try {
+ val fullResponse = StringBuilder()
+ withContext(Dispatchers.IO) {
+ flow.collect { value ->
+ fullResponse.append(value)
+ withContext(Dispatchers.Main) {
+ onToken(value)
+ }
+ }
+ }
+ withContext(Dispatchers.Main) {
+ onComplete(fullResponse.toString())
+ }
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ onError(e.message ?: "Unknown error")
+ }
+ }
+ }
+ }
+
+ fun cancel() {
+ scope.coroutineContext.cancelChildren()
+ }
+}
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/KotlinJavaUtils.kt b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/KotlinJavaUtils.kt
new file mode 100644
index 00000000..1fc8437c
--- /dev/null
+++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/KotlinJavaUtils.kt
@@ -0,0 +1,11 @@
+package ai.nexa.app_java
+
+import java.util.function.Consumer
+
+object KotlinJavaUtils {
+ @JvmStatic
+ fun toKotlinCallback(callback: Consumer): (String) -> Unit = { value ->
+ callback.accept(value)
+ Unit
+ }
+}
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/LlamaBridge.java b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/LlamaBridge.java
new file mode 100644
index 00000000..e48ec8d5
--- /dev/null
+++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/LlamaBridge.java
@@ -0,0 +1,283 @@
+package ai.nexa.app_java;
+
+import android.content.Context;
+import com.nexa.NexaVlmInference;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import kotlin.Unit;
+import kotlin.coroutines.Continuation;
+import kotlin.jvm.functions.Function1;
+import kotlinx.coroutines.BuildersKt;
+import kotlinx.coroutines.CoroutineStart;
+import kotlinx.coroutines.Dispatchers;
+import kotlinx.coroutines.GlobalScope;
+import kotlinx.coroutines.Job;
+import kotlinx.coroutines.flow.Flow;
+import kotlinx.coroutines.flow.FlowCollector;
+
+public class LlamaBridge {
+ private static final String TAG = "LlamaBridge";
+ private final Context context;
+ private final ExecutorService executor;
+ private final MessageHandler messageHandler;
+ private final VlmModelManager modelManager;
+ private final ImagePathHelper imagePathHelper;
+ private NexaVlmInference nexaVlmInference;
+ private boolean isModelLoaded = false;
+
+ private final KotlinFlowHelper flowHelper = new KotlinFlowHelper();
+
+ // Default inference parameters
+ private static final float DEFAULT_TEMPERATURE = 1.0f;
+ private static final int DEFAULT_MAX_TOKENS = 64;
+ private static final int DEFAULT_TOP_K = 50;
+ private static final float DEFAULT_TOP_P = 0.9f;
+
+ public interface InferenceCallback {
+ void onStart();
+ void onToken(String token);
+ void onComplete(String fullResponse);
+ void onError(String error);
+ }
+
+ public LlamaBridge(Context context, MessageHandler messageHandler) {
+ this.context = context;
+ this.messageHandler = messageHandler;
+ this.executor = Executors.newSingleThreadExecutor();
+ this.modelManager = new VlmModelManager(context);
+ this.imagePathHelper = new ImagePathHelper(context);
+ }
+
+ public boolean areModelsAvailable() {
+ return modelManager.areModelsAvailable();
+ }
+
+ public void loadModel() {
+ executor.execute(() -> {
+ try {
+ if (!modelManager.areModelsAvailable()) {
+ throw new IOException("Required model files are not available");
+ }
+
+ String modelPath = modelManager.getTextModelPath();
+ String projectorPath = modelManager.getMmProjModelPath();
+
+ Log.d(TAG, "Loading model from: " + modelPath);
+ Log.d(TAG, "Loading projector from: " + projectorPath);
+
+ // Create with default values for optional parameters
+ nexaVlmInference = new NexaVlmInference(
+ modelPath, // modelPath
+ projectorPath, // projectorPath
+ "", // imagePath (empty string as default)
+ new ArrayList<>(Arrays.asList("")), // stopWords (empty list)
+ DEFAULT_TEMPERATURE, // temperature
+ DEFAULT_MAX_TOKENS, // maxNewTokens
+ DEFAULT_TOP_K, // topK
+ DEFAULT_TOP_P // topP
+ );
+ nexaVlmInference.loadModel();
+ isModelLoaded = true;
+
+ Log.d(TAG, "Model loaded successfully.");
+// messageHandler.addMessage(new MessageModal("Model loaded successfully", "assistant", null));
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to load model", e);
+ messageHandler.addMessage(new MessageModal("Error loading model: " + e.getMessage(), "assistant", null));
+ }
+ });
+ }
+
+// public void processMessage(String message, String imageUri, InferenceCallback callback) {
+// if (!isModelLoaded) {
+// callback.onError("Model not loaded yet");
+// return;
+// }
+//
+// try {
+// // Add user message first
+// MessageModal userMessage = new MessageModal(message, "user", imageUri);
+// messageHandler.addMessage(userMessage);
+//
+// // Create an initial empty assistant message
+// MessageModal assistantMessage = new MessageModal("", "assistant", null);
+// messageHandler.addMessage(assistantMessage);
+//
+// // Convert image URI to absolute path
+// String imageAbsolutePath = imagePathHelper.getPathFromUri(imageUri);
+//
+// Flow flow = nexaVlmInference.createCompletionStream(
+// message,
+// imageAbsolutePath,
+// new ArrayList<>(),
+// DEFAULT_TEMPERATURE,
+// DEFAULT_MAX_TOKENS,
+// DEFAULT_TOP_K,
+// DEFAULT_TOP_P
+// );
+//
+// if (flow != null) {
+// CoroutineScope scope = CoroutineScopeKt.CoroutineScope(Dispatchers.getMain());
+//
+// Job job = FlowKt.launchIn(
+// FlowKt.onEach(flow, new Function2, Object>() {
+// @Override
+// public Object invoke(String token, Continuation super Unit> continuation) {
+// messageHandler.updateLastAssistantMessage(token);
+// callback.onToken(token);
+// return Unit.INSTANCE;
+// }
+// }),
+// scope
+// );
+// } else {
+// messageHandler.finalizeLastAssistantMessage("Error: Failed to create completion stream");
+// callback.onError("Failed to create completion stream");
+// }
+// } catch (Exception e) {
+// Log.e(TAG, "Error processing message", e);
+// messageHandler.finalizeLastAssistantMessage("Error: " + e.getMessage());
+// callback.onError(e.getMessage());
+// }
+// }
+
+ public void processMessage(String message, String imageUri, InferenceCallback callback) {
+ if (!isModelLoaded) {
+ callback.onError("Model not loaded yet");
+ return;
+ }
+
+ String imageAbsolutePath = null;
+ try {
+ imageAbsolutePath = imagePathHelper.copyUriToPrivateFile(context, imageUri);
+ } catch (IOException e) {
+ callback.onError("Failed to process image: " + e.getMessage());
+ return;
+ }
+
+ final String imagePath = imageAbsolutePath;
+ MessageModal assistantMessage = new MessageModal("", "bot", null);
+ messageHandler.addMessage(assistantMessage);
+
+ try {
+ Flow flow = nexaVlmInference.createCompletionStream(
+ message,
+ imagePath,
+ new ArrayList<>(Arrays.asList("")),
+ DEFAULT_TEMPERATURE,
+ DEFAULT_MAX_TOKENS,
+ DEFAULT_TOP_K,
+ DEFAULT_TOP_P
+ );
+
+ callback.onStart();
+ StringBuilder fullResponse = new StringBuilder();
+
+ Job collectJob = BuildersKt.launch(
+ GlobalScope.INSTANCE,
+ Dispatchers.getIO(),
+ CoroutineStart.DEFAULT,
+ (coroutineScope, continuation) -> {
+ flow.collect(new FlowCollector() {
+ @Override
+ public Object emit(String token, Continuation super Unit> continuation) {
+ fullResponse.append(token);
+ callback.onToken(token);
+ return Unit.INSTANCE;
+ }
+ }, continuation);
+ callback.onComplete(fullResponse.toString());
+ return Unit.INSTANCE;
+ }
+ );
+
+ collectJob.invokeOnCompletion(new Function1() {
+ @Override
+ public Unit invoke(Throwable throwable) {
+ if (throwable != null && !(throwable instanceof CancellationException)) {
+ callback.onError("Stream collection failed: " + throwable.getMessage());
+ }
+ return Unit.INSTANCE;
+ }
+ });
+
+ } catch (Exception e) {
+ Log.e(TAG, "Inference failed", e);
+ callback.onError(e.getMessage());
+ }
+ }
+
+ public void cleanup() {
+ flowHelper.cancel();
+ }
+
+// public void processMessageWithParams(
+// String message,
+// String imageUri,
+// float temperature,
+// int maxTokens,
+// int topK,
+// float topP,
+// InferenceCallback callback) {
+//
+// if (!isModelLoaded) {
+// callback.onError("Model not loaded yet");
+// return;
+// }
+//
+// executor.execute(() -> {
+// StringBuilder fullResponse = new StringBuilder();
+// try {
+// callback.onStart();
+//
+// Flow completionStream = nexaVlmInference.createCompletionStream(
+// message,
+// imageUri,
+// new ArrayList<>(),
+// temperature,
+// maxTokens,
+// topK,
+// topP
+// );
+//
+// completionStream.collect(new FlowCollector() {
+// @Override
+// public Object emit(String value, Continuation super Unit> continuation) {
+// fullResponse.append(value);
+// callback.onToken(value);
+// return Unit.INSTANCE;
+// }
+// });
+//
+// callback.onComplete(fullResponse.toString());
+//
+// } catch (Exception e) {
+// Log.e(TAG, "Inference failed", e);
+// callback.onError(e.getMessage());
+// }
+// });
+// }
+
+
+ public void shutdown() {
+ if (nexaVlmInference != null) {
+ executor.execute(() -> {
+ try {
+ nexaVlmInference.dispose();
+ } catch (Exception e) {
+ Log.e(TAG, "Error closing inference", e);
+ }
+ nexaVlmInference = null;
+ isModelLoaded = false;
+ });
+ }
+ executor.shutdown();
+ }
+}
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MainActivity.java b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MainActivity.java
new file mode 100644
index 00000000..29be7214
--- /dev/null
+++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MainActivity.java
@@ -0,0 +1,345 @@
+package ai.nexa.app_java;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.speech.RecognizerIntent;
+import android.speech.SpeechRecognizer;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+public class MainActivity extends AppCompatActivity {
+
+ private static final String TAG = "ChatApp";
+ private static final int PICK_IMAGE_REQUEST = 30311;
+ private static final int REQUEST_RECORD_AUDIO_PERMISSION = 200;
+ private static final int READ_EXTERNAL_STORAGE_PERMISSION = 303;
+
+ private RecyclerView chatsRV;
+ private ImageButton selectImageButton;
+ private ImageButton sendMsgIB;
+ private EditText userMsgEdt;
+ private String justSelectedImageUri;
+
+ private LinearLayout linearLayout;
+ private TextView titleAfterChatTextView;
+ private RecyclerView recyclerView;
+
+ private ArrayList messageModalArrayList;
+ private MessageRVAdapter messageRVAdapter;
+ private MessageHandler messageHandler;
+ private LlamaBridge llamaBridge;
+ private SpeechRecognizer speechRecognizer;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ Log.d(TAG, "onCreate: Starting MainActivity");
+
+ initializeViews();
+ setupRecyclerView();
+ initializeLlamaBridge();
+ createSpeechRecognizerIntent();
+ setupClickListeners();
+
+ Log.d(TAG, "onCreate: MainActivity setup complete");
+ }
+
+ private void initializeViews() {
+ chatsRV = findViewById(R.id.idRVChats);
+ selectImageButton = findViewById(R.id.btnUploadImage);
+ sendMsgIB = findViewById(R.id.idIBSend);
+ userMsgEdt = findViewById(R.id.idEdtMessage);
+ linearLayout = findViewById(R.id.idLayoutBeforeChat);
+ titleAfterChatTextView = findViewById(R.id.textView);
+ recyclerView = findViewById(R.id.idRVChats);
+ }
+
+ private void setupRecyclerView() {
+ messageModalArrayList = new ArrayList<>();
+ messageRVAdapter = new MessageRVAdapter(messageModalArrayList, this);
+ chatsRV.setLayoutManager(new LinearLayoutManager(this, RecyclerView.VERTICAL, false));
+ chatsRV.setAdapter(messageRVAdapter);
+ messageHandler = new MessageHandler(messageModalArrayList, messageRVAdapter, recyclerView);
+ }
+
+ private void initializeLlamaBridge() {
+ llamaBridge = new LlamaBridge(this, messageHandler);
+ if (!llamaBridge.areModelsAvailable()) {
+ Toast.makeText(this, "Required model files are not available", Toast.LENGTH_LONG).show();
+ return;
+ }
+ llamaBridge.loadModel();
+ }
+
+ private void setupClickListeners() {
+ selectImageButton.setOnClickListener(v -> {
+ Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
+ startActivityForResult(intent, PICK_IMAGE_REQUEST);
+ });
+
+ sendMsgIB.setOnClickListener(v -> {
+ hideKeyboard(v);
+ sendTextMessage();
+ });
+ }
+
+ private void updateChatBotDisplay() {
+ linearLayout.setVisibility(View.GONE);
+ titleAfterChatTextView.setVisibility(View.VISIBLE);
+ recyclerView.setVisibility(View.VISIBLE);
+ }
+
+ private void sendTextMessage() {
+ updateChatBotDisplay();
+
+ String userMessage = userMsgEdt.getText().toString().trim();
+ if (!userMessage.isEmpty()) {
+ Log.d(TAG, "Sending message: " + userMessage);
+ messageHandler.addMessage(new MessageModal(userMessage, "user", null));
+
+ if (justSelectedImageUri == null) {
+ messageHandler.addMessage(new MessageModal("Please select an image first.", "bot", null));
+ return;
+ }
+
+ // Use LlamaBridge for inference
+ llamaBridge.processMessage(userMessage, justSelectedImageUri, new LlamaBridge.InferenceCallback() {
+ @Override
+ public void onStart() {
+ // Optional: Show loading indicator
+ }
+
+ @Override
+ public void onToken(String token) {
+ // Update the UI with each token as it comes in
+ runOnUiThread(() -> {
+ messageHandler.updateLastBotMessage(token);
+ });
+ }
+
+ @Override
+ public void onComplete(String fullResponse) {
+ // Final update with complete response
+ runOnUiThread(() -> {
+ messageHandler.finalizeLastBotMessage(fullResponse);
+ });
+ }
+
+ @Override
+ public void onError(String error) {
+ runOnUiThread(() -> {
+ Toast.makeText(MainActivity.this, "Error: " + error, Toast.LENGTH_SHORT).show();
+ messageHandler.addMessage(new MessageModal("Error processing message: " + error, "assistant", null));
+ });
+ }
+ });
+
+ userMsgEdt.setText(""); // Clear the input field after sending
+ justSelectedImageUri = null; // Clear the image URI after sending
+ } else {
+ Toast.makeText(MainActivity.this, "Please enter your message.", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void sendImageAsMessage(String imageUri) {
+ updateChatBotDisplay();
+ messageHandler.addMessage(new MessageModal("", "user", imageUri));
+ justSelectedImageUri = imageUri;
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (llamaBridge != null) {
+ llamaBridge.shutdown();
+ }
+ if (speechRecognizer != null) {
+ speechRecognizer.destroy();
+ }
+ }
+
+ private void createSpeechRecognizerIntent() {
+ requestMicrophonePermission();
+
+ ImageButton btnStart = findViewById(R.id.btnStart);
+
+ speechRecognizer = SpeechRecognizer.createSpeechRecognizer(this);
+
+ Intent speechRecognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+ speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
+ speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault());
+ speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
+
+ speechRecognizer.setRecognitionListener(new android.speech.RecognitionListener() {
+ @Override
+ public void onReadyForSpeech(Bundle params) {
+ }
+
+ @Override
+ public void onBeginningOfSpeech() {
+ }
+
+ @Override
+ public void onRmsChanged(float rmsdB) {
+ }
+
+ @Override
+ public void onBufferReceived(byte[] buffer) {
+ }
+
+ @Override
+ public void onEndOfSpeech() {
+ }
+
+ @Override
+ public void onError(int error) {
+ String errorMessage = getErrorText(error);
+ Log.d("SpeechRecognition", "Error occurred: " + errorMessage);
+ }
+
+ public String getErrorText(int errorCode) {
+ String message;
+ switch (errorCode) {
+ case SpeechRecognizer.ERROR_AUDIO:
+ message = "Audio recording error";
+ break;
+ case SpeechRecognizer.ERROR_CLIENT:
+ message = "Client side error";
+ break;
+ case SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS:
+ message = "Insufficient permissions";
+ break;
+ case SpeechRecognizer.ERROR_NETWORK:
+ message = "Network error";
+ break;
+ case SpeechRecognizer.ERROR_NETWORK_TIMEOUT:
+ message = "Network timeout";
+ break;
+ case SpeechRecognizer.ERROR_NO_MATCH:
+ message = "No match";
+ break;
+ case SpeechRecognizer.ERROR_RECOGNIZER_BUSY:
+ message = "RecognitionService busy";
+ break;
+ case SpeechRecognizer.ERROR_SERVER:
+ message = "Error from server";
+ break;
+ case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
+ message = "No speech input";
+ break;
+ default:
+ message = "Didn't understand, please try again.";
+ break;
+ }
+ return message;
+ }
+
+ @Override
+ public void onResults(Bundle results) {
+ ArrayList matches = results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
+ if (matches != null && !matches.isEmpty()) {
+ userMsgEdt.setText(matches.get(0)); // Set the recognized text to the EditText
+ sendTextMessage();
+ }
+ }
+
+ @Override
+ public void onPartialResults(Bundle partialResults) {
+ // This is called for partial results
+ ArrayList partialMatches = partialResults.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
+ if (partialMatches != null && !partialMatches.isEmpty()) {
+ userMsgEdt.setText(partialMatches.get(0)); // Update EditText with the partial result
+ }
+ }
+
+ @Override
+ public void onEvent(int eventType, Bundle params) {
+ }
+ });
+
+ btnStart.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ // Button is pressed
+ speechRecognizer.startListening(speechRecognizerIntent);
+ return true; // Return true to indicate the event was handled
+ case MotionEvent.ACTION_UP:
+ // Button is released
+ speechRecognizer.stopListening();
+ return true; // Return true to indicate the event was handled
+ }
+ return false; // Return false for other actions
+ }
+ });
+ }
+
+ private void requestMicrophonePermission() {
+ ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, REQUEST_RECORD_AUDIO_PERMISSION);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ switch (requestCode) {
+ case READ_EXTERNAL_STORAGE_PERMISSION:
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Toast.makeText(this, "Read External Storage Permission Granted", Toast.LENGTH_SHORT).show();
+ Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
+ startActivityForResult(intent, PICK_IMAGE_REQUEST);
+ } else {
+ Toast.makeText(this, "Read External Storage Permission Denied", Toast.LENGTH_SHORT).show();
+ }
+ break;
+ default:
+ break;
+ }
+
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null) {
+ Uri selectedImage = data.getData();
+ if (selectedImage != null) {
+ String imageUriString = selectedImage.toString();
+ sendImageAsMessage(imageUriString);
+ }
+ }
+ }
+
+ public void hideKeyboard(View view) {
+ InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (inputMethodManager != null) {
+ inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageHandler.java b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageHandler.java
new file mode 100644
index 00000000..39720c1f
--- /dev/null
+++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageHandler.java
@@ -0,0 +1,127 @@
+package ai.nexa.app_java;
+
+import androidx.recyclerview.widget.RecyclerView;
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.ArrayList;
+
+public class MessageHandler {
+ private final ArrayList messageModalArrayList;
+ private final MessageRVAdapter messageRVAdapter;
+ private final RecyclerView recyclerView;
+ private final Handler mainHandler;
+
+ public MessageHandler(ArrayList messageModalArrayList, MessageRVAdapter messageRVAdapter, RecyclerView recyclerView) {
+ this.messageModalArrayList = messageModalArrayList;
+ this.messageRVAdapter = messageRVAdapter;
+ this.recyclerView = recyclerView;
+ this.mainHandler = new Handler(Looper.getMainLooper());
+ }
+
+ /**
+ * Add a new message to the chat
+ */
+ public void addMessage(MessageModal message) {
+ ensureMainThread(() -> {
+ messageModalArrayList.add(message);
+ messageRVAdapter.notifyItemInserted(messageModalArrayList.size() - 1);
+ scrollToBottom();
+ });
+ }
+
+ /**
+ * Update the last bot message with new token
+ */
+ public void updateLastBotMessage(String newToken) {
+ ensureMainThread(() -> {
+ if (!messageModalArrayList.isEmpty()) {
+ int lastIndex = messageModalArrayList.size() - 1;
+ MessageModal lastMessage = messageModalArrayList.get(lastIndex);
+
+ // If last message is from bot, update it
+ if ("bot".equals(lastMessage.getSender())) {
+ String currentMessage = lastMessage.getMessage();
+ lastMessage.setMessage(currentMessage + newToken);
+ messageRVAdapter.notifyItemChanged(lastIndex);
+ } else {
+ // Create new bot message
+ MessageModal newMessage = new MessageModal(newToken, "bot", null);
+ messageModalArrayList.add(newMessage);
+ messageRVAdapter.notifyItemInserted(messageModalArrayList.size() - 1);
+ }
+ scrollToBottom();
+ }
+ });
+ }
+
+ /**
+ * Finalize the last bot message with complete response
+ */
+ public void finalizeLastBotMessage(String completeMessage) {
+ ensureMainThread(() -> {
+ if (!messageModalArrayList.isEmpty()) {
+ int lastIndex = messageModalArrayList.size() - 1;
+ MessageModal lastMessage = messageModalArrayList.get(lastIndex);
+
+ if ("bot".equals(lastMessage.getSender())) {
+ lastMessage.setMessage(completeMessage);
+ messageRVAdapter.notifyItemChanged(lastIndex);
+ } else {
+ MessageModal newMessage = new MessageModal(completeMessage, "bot", null);
+ messageModalArrayList.add(newMessage);
+ messageRVAdapter.notifyItemInserted(messageModalArrayList.size() - 1);
+ }
+ scrollToBottom();
+ }
+ });
+ }
+
+ /**
+ * Clear all messages from the chat
+ */
+ public void clearMessages() {
+ ensureMainThread(() -> {
+ messageModalArrayList.clear();
+ messageRVAdapter.notifyDataSetChanged();
+ });
+ }
+
+ /**
+ * Get the last message in the chat
+ */
+ public MessageModal getLastMessage() {
+ if (!messageModalArrayList.isEmpty()) {
+ return messageModalArrayList.get(messageModalArrayList.size() - 1);
+ }
+ return null;
+ }
+
+ /**
+ * Check if the last message is from the bot
+ */
+ public boolean isLastMessageFromBot() {
+ MessageModal lastMessage = getLastMessage();
+ return lastMessage != null && "bot".equals(lastMessage.getSender());
+ }
+
+ /**
+ * Scroll the RecyclerView to the bottom
+ */
+ private void scrollToBottom() {
+ if (messageModalArrayList.size() > 1) {
+ recyclerView.smoothScrollToPosition(messageModalArrayList.size() - 1);
+ }
+ }
+
+ /**
+ * Ensure all UI updates happen on the main thread
+ */
+ private void ensureMainThread(Runnable action) {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ action.run();
+ } else {
+ mainHandler.post(action);
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageModal.java b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageModal.java
new file mode 100644
index 00000000..1e60921b
--- /dev/null
+++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageModal.java
@@ -0,0 +1,42 @@
+package ai.nexa.app_java;
+
+public class MessageModal {
+
+
+ private String message;
+ private String sender;
+
+ private String imageUri;
+
+ public MessageModal(String message, String sender, String imageUri) {
+ this.message = message;
+ this.sender = sender;
+ this.imageUri = imageUri;
+ }
+
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public String getSender() {
+ return sender;
+ }
+
+ public void setSender(String sender) {
+ this.sender = sender;
+ }
+
+ public String getImageUri() {
+ return imageUri;
+ }
+
+ public void setImageUri(String imageUri) {
+ this.imageUri = imageUri;
+ }
+}
+
diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageRVAdapter.java b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageRVAdapter.java
new file mode 100644
index 00000000..90977681
--- /dev/null
+++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageRVAdapter.java
@@ -0,0 +1,102 @@
+package ai.nexa.app_java;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.Glide;
+
+import java.util.ArrayList;
+
+public class MessageRVAdapter extends RecyclerView.Adapter {
+
+ private ArrayList messageModalArrayList;
+ private Context context;
+
+ public MessageRVAdapter(ArrayList messageModalArrayList, Context context) {
+ this.messageModalArrayList = messageModalArrayList;
+ this.context = context;
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view;
+ switch (viewType) {
+ case 0:
+ view = LayoutInflater.from(parent.getContext()).inflate(R.layout.user_msg, parent, false);
+ return new UserViewHolder(view);
+ case 1:
+ view = LayoutInflater.from(parent.getContext()).inflate(R.layout.bot_msg, parent, false);
+ return new BotViewHolder(view);
+ }
+ return null;
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ MessageModal modal = messageModalArrayList.get(position);
+ switch (modal.getSender()) {
+ case "user":
+ UserViewHolder userHolder = (UserViewHolder) holder;
+ if (modal.getImageUri() != null && !modal.getImageUri().isEmpty()) {
+ userHolder.userImage.setVisibility(View.VISIBLE);
+ userHolder.userTV.setVisibility(View.GONE);
+ Glide.with(userHolder.itemView.getContext())
+ .load(modal.getImageUri())
+ .into(userHolder.userImage);
+ } else {
+ userHolder.userImage.setVisibility(View.GONE);
+ userHolder.userTV.setVisibility(View.VISIBLE);
+ userHolder.userTV.setText(modal.getMessage());
+ }
+ break;
+ case "bot":
+ ((BotViewHolder) holder).botTV.setText(modal.getMessage());
+ break;
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return messageModalArrayList.size();
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ switch (messageModalArrayList.get(position).getSender()) {
+ case "user":
+ return 0;
+ case "bot":
+ return 1;
+ default:
+ return -1;
+ }
+ }
+
+ public static class UserViewHolder extends RecyclerView.ViewHolder {
+ TextView userTV;
+ ImageView userImage;
+
+ public UserViewHolder(@NonNull View itemView) {
+ super(itemView);
+ userTV = itemView.findViewById(R.id.idTVUser);
+ userImage = itemView.findViewById(R.id.idIVUserImage);
+ }
+ }
+
+ public static class BotViewHolder extends RecyclerView.ViewHolder {
+ TextView botTV;
+
+ public BotViewHolder(@NonNull View itemView) {
+ super(itemView);
+ botTV = itemView.findViewById(R.id.idTVBot);
+ }
+ }
+}
diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/VlmModelManager.java b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/VlmModelManager.java
new file mode 100644
index 00000000..9ebd8d45
--- /dev/null
+++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/VlmModelManager.java
@@ -0,0 +1,125 @@
+package ai.nexa.app_java;
+
+import android.content.Context;
+import android.os.Environment;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+
+public class VlmModelManager {
+ private static final String TAG = "LlamaBridge";
+ private static final String MODELS_DIR = "models";
+ private static final String MODEL_TEXT_FILENAME = "nanollava-text-model-q4_0.gguf";
+ private static final String MODEL_MMPROJ_FILENAME = "nanollava-mmproj-f16.gguf";
+
+ private final Context context;
+ private File textModelFile;
+ private File mmProjModelFile;
+ private final File externalModelDir;
+
+ public VlmModelManager(Context context) {
+ this.context = context;
+ this.externalModelDir = new File(Environment.getExternalStorageDirectory(),
+ "Android/data/" + context.getPackageName() + "/files");
+ }
+
+ /**
+ * Search for model in common locations
+ * @param modelFilename The name of the model file to find
+ * @return File path to the model if found, null otherwise
+ */
+ private String findExistingModel(String modelFilename) {
+ // List of possible locations to check
+ File[] locations = {
+ // External storage specific path
+ new File(externalModelDir, modelFilename),
+ // Downloads folder
+ new File(Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_DOWNLOADS), modelFilename),
+ // App's private external storage
+ new File(context.getExternalFilesDir(null), MODELS_DIR + "/" + modelFilename),
+ // App's private internal storage
+ new File(context.getFilesDir(), MODELS_DIR + "/" + modelFilename)
+ };
+
+ for (File location : locations) {
+ if (location.exists() && location.canRead()) {
+ Log.d(TAG, "Found model at: " + location.getAbsolutePath());
+ return location.getAbsolutePath();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get text model path, searching in storage locations
+ * @return Path to the model file
+ * @throws IOException if model cannot be found or accessed
+ */
+ public String getTextModelPath() throws IOException {
+ // If we already have a valid model file, return it
+ if (textModelFile != null && textModelFile.exists() && textModelFile.canRead()) {
+ return textModelFile.getAbsolutePath();
+ }
+
+ // Search for existing model
+ String path = findExistingModel(MODEL_TEXT_FILENAME);
+ if (path != null) {
+ textModelFile = new File(path);
+ return path;
+ }
+
+ throw new IOException("Text model not found in any storage location");
+ }
+
+ /**
+ * Get mmproj model path, searching in storage locations
+ * @return Path to the model file
+ * @throws IOException if model cannot be found or accessed
+ */
+ public String getMmProjModelPath() throws IOException {
+ // If we already have a valid model file, return it
+ if (mmProjModelFile != null && mmProjModelFile.exists() && mmProjModelFile.canRead()) {
+ return mmProjModelFile.getAbsolutePath();
+ }
+
+ // Search for existing model
+ String path = findExistingModel(MODEL_MMPROJ_FILENAME);
+ if (path != null) {
+ mmProjModelFile = new File(path);
+ return path;
+ }
+
+ throw new IOException("MMProj model not found in any storage location");
+ }
+
+ /**
+ * Check if both required models exist in any location
+ * @return true if both models are found
+ */
+ public boolean areModelsAvailable() {
+ try {
+ getTextModelPath();
+ getMmProjModelPath();
+ return true;
+ } catch (IOException e) {
+ Log.w(TAG, "Models not available: " + e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Get the directory containing the models
+ * @return File object for the models directory, or null if models aren't found
+ */
+ public File getModelsDirectory() {
+ try {
+ String textModelPath = getTextModelPath();
+ return new File(textModelPath).getParentFile();
+ } catch (IOException e) {
+ Log.w(TAG, "Could not determine models directory: " + e.getMessage());
+ return null;
+ }
+ }
+}
diff --git a/android/llama.android/app-java/src/main/res/drawable-hdpi/ic_menu_send.png b/android/llama.android/app-java/src/main/res/drawable-hdpi/ic_menu_send.png
new file mode 100644
index 00000000..f34a9658
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/drawable-hdpi/ic_menu_send.png differ
diff --git a/android/llama.android/app-java/src/main/res/drawable-mdpi/ic_menu_send.png b/android/llama.android/app-java/src/main/res/drawable-mdpi/ic_menu_send.png
new file mode 100644
index 00000000..e83f6010
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/drawable-mdpi/ic_menu_send.png differ
diff --git a/android/llama.android/app-java/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/llama.android/app-java/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..2b068d11
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/res/drawable-xhdpi/ic_menu_send.png b/android/llama.android/app-java/src/main/res/drawable-xhdpi/ic_menu_send.png
new file mode 100644
index 00000000..882722eb
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/drawable-xhdpi/ic_menu_send.png differ
diff --git a/android/llama.android/app-java/src/main/res/drawable-xxhdpi/ic_menu_send.png b/android/llama.android/app-java/src/main/res/drawable-xxhdpi/ic_menu_send.png
new file mode 100644
index 00000000..08108e76
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/drawable-xxhdpi/ic_menu_send.png differ
diff --git a/android/llama.android/app-java/src/main/res/drawable-xxxhdpi/ic_menu_send.png b/android/llama.android/app-java/src/main/res/drawable-xxxhdpi/ic_menu_send.png
new file mode 100644
index 00000000..8f7eb62c
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/drawable-xxxhdpi/ic_menu_send.png differ
diff --git a/android/llama.android/app-java/src/main/res/drawable/bg_send_message.xml b/android/llama.android/app-java/src/main/res/drawable/bg_send_message.xml
new file mode 100644
index 00000000..972981d8
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/drawable/bg_send_message.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/drawable/bot_message.xml b/android/llama.android/app-java/src/main/res/drawable/bot_message.xml
new file mode 100644
index 00000000..8dda5f87
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/drawable/bot_message.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/drawable/gradient_background.xml b/android/llama.android/app-java/src/main/res/drawable/gradient_background.xml
new file mode 100644
index 00000000..6d9a5345
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/drawable/gradient_background.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/drawable/ic_bot.xml b/android/llama.android/app-java/src/main/res/drawable/ic_bot.xml
new file mode 100644
index 00000000..660ed4e0
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/drawable/ic_bot.xml
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/drawable/ic_launcher.png b/android/llama.android/app-java/src/main/res/drawable/ic_launcher.png
new file mode 100644
index 00000000..e3c90853
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/drawable/ic_launcher.png differ
diff --git a/android/llama.android/app-java/src/main/res/drawable/ic_launcher_background.xml b/android/llama.android/app-java/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..07d5da9c
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/drawable/ic_launcher_fav_background.xml b/android/llama.android/app-java/src/main/res/drawable/ic_launcher_fav_background.xml
new file mode 100644
index 00000000..ca3826a4
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/drawable/ic_launcher_fav_background.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/drawable/ic_user.xml b/android/llama.android/app-java/src/main/res/drawable/ic_user.xml
new file mode 100644
index 00000000..725adb58
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/drawable/ic_user.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/drawable/input_text_box.xml b/android/llama.android/app-java/src/main/res/drawable/input_text_box.xml
new file mode 100644
index 00000000..1c132b0b
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/drawable/input_text_box.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/res/drawable/microphone.xml b/android/llama.android/app-java/src/main/res/drawable/microphone.xml
new file mode 100644
index 00000000..75fe9341
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/drawable/microphone.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/drawable/octopus_background.xml b/android/llama.android/app-java/src/main/res/drawable/octopus_background.xml
new file mode 100644
index 00000000..ca3826a4
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/drawable/octopus_background.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/drawable/octopus_menu_send.xml b/android/llama.android/app-java/src/main/res/drawable/octopus_menu_send.xml
new file mode 100644
index 00000000..4254a34f
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/drawable/octopus_menu_send.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/drawable/octopus_original.xml b/android/llama.android/app-java/src/main/res/drawable/octopus_original.xml
new file mode 100644
index 00000000..92048641
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/drawable/octopus_original.xml
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/drawable/ocutopus_v3_full_size.png b/android/llama.android/app-java/src/main/res/drawable/ocutopus_v3_full_size.png
new file mode 100644
index 00000000..de1bb864
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/drawable/ocutopus_v3_full_size.png differ
diff --git a/android/llama.android/app-java/src/main/res/drawable/roundcorner.xml b/android/llama.android/app-java/src/main/res/drawable/roundcorner.xml
new file mode 100644
index 00000000..5c795c41
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/drawable/roundcorner.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/drawable/title.xml b/android/llama.android/app-java/src/main/res/drawable/title.xml
new file mode 100644
index 00000000..a7bad4f8
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/drawable/title.xml
@@ -0,0 +1,11 @@
+
+
+
+ -
+
+
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/drawable/upload_image_icon.xml b/android/llama.android/app-java/src/main/res/drawable/upload_image_icon.xml
new file mode 100644
index 00000000..f4a86832
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/drawable/upload_image_icon.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/font/abhaya_libre_bold.ttf b/android/llama.android/app-java/src/main/res/font/abhaya_libre_bold.ttf
new file mode 100644
index 00000000..6f4a231d
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/font/abhaya_libre_bold.ttf differ
diff --git a/android/llama.android/app-java/src/main/res/font/alegreya_sans_sc_extrabold.xml b/android/llama.android/app-java/src/main/res/font/alegreya_sans_sc_extrabold.xml
new file mode 100644
index 00000000..8112a231
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/font/alegreya_sans_sc_extrabold.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/layout/activity_main.xml b/android/llama.android/app-java/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..625d9923
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/layout/activity_main.xml
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/res/layout/bot_msg.xml b/android/llama.android/app-java/src/main/res/layout/bot_msg.xml
new file mode 100644
index 00000000..5ee58e1d
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/layout/bot_msg.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/layout/user_msg.xml b/android/llama.android/app-java/src/main/res/layout/user_msg.xml
new file mode 100644
index 00000000..20aa126a
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/layout/user_msg.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..036d09bc
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..036d09bc
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/octopus.xml b/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/octopus.xml
new file mode 100644
index 00000000..2e533e65
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/octopus.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/octopus_round.xml b/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/octopus_round.xml
new file mode 100644
index 00000000..2e533e65
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/octopus_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher.png b/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..cf0c3458
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..8acbf0ea
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..12580bdb
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus.webp b/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus.webp
new file mode 100644
index 00000000..29daecf1
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus.webp differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus_foreground.webp b/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus_foreground.webp
new file mode 100644
index 00000000..88bf3149
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus_foreground.webp differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus_round.webp b/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus_round.webp
new file mode 100644
index 00000000..0883ba1c
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus_round.webp differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher.png b/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..b3990457
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..b8a59f47
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..75aec75a
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus.webp b/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus.webp
new file mode 100644
index 00000000..c192866e
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus.webp differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus_foreground.webp b/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus_foreground.webp
new file mode 100644
index 00000000..34251871
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus_foreground.webp differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus_round.webp b/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus_round.webp
new file mode 100644
index 00000000..edb57427
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus_round.webp differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..a6324636
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..98708fa5
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..30de3067
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus.webp b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus.webp
new file mode 100644
index 00000000..372b8bdb
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus.webp differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus_foreground.webp b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus_foreground.webp
new file mode 100644
index 00000000..fcdd6ddf
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus_foreground.webp differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus_round.webp b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus_round.webp
new file mode 100644
index 00000000..5b864a66
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus_round.webp differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..196c1ef5
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..34fc4e7e
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..984bb8d9
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus.webp b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus.webp
new file mode 100644
index 00000000..ad3daafc
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus.webp differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus_foreground.webp b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus_foreground.webp
new file mode 100644
index 00000000..ca878a67
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus_foreground.webp differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus_round.webp b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus_round.webp
new file mode 100644
index 00000000..fd780d66
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus_round.webp differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..1f10f330
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..13f3147e
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..b81a70ba
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus.webp b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus.webp
new file mode 100644
index 00000000..ef8923ce
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus.webp differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus_foreground.webp b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus_foreground.webp
new file mode 100644
index 00000000..e8b6489c
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus_foreground.webp differ
diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus_round.webp b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus_round.webp
new file mode 100644
index 00000000..d0b5881a
Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus_round.webp differ
diff --git a/android/llama.android/app-java/src/main/res/values-night/themes.xml b/android/llama.android/app-java/src/main/res/values-night/themes.xml
new file mode 100644
index 00000000..2bd72d37
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/values-night/themes.xml
@@ -0,0 +1,11 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/res/values/colors.xml b/android/llama.android/app-java/src/main/res/values/colors.xml
new file mode 100644
index 00000000..b15af47b
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/values/colors.xml
@@ -0,0 +1,17 @@
+
+
+ #FF000000
+ #FFFFFFFF
+ #813BBA
+ #FF202020
+ #17CE92
+ #E5E5E5
+ #0A1528
+ #313D50
+ #03070D
+ #03070D
+ #03070D
+ #03070D
+ #FFFFFF
+ #B00020
+
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/res/values/font_certs.xml b/android/llama.android/app-java/src/main/res/values/font_certs.xml
new file mode 100644
index 00000000..d2226ac0
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/values/font_certs.xml
@@ -0,0 +1,17 @@
+
+
+
+ - @array/com_google_android_gms_fonts_certs_dev
+ - @array/com_google_android_gms_fonts_certs_prod
+
+
+ -
+ MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
+
+
+
+ -
+ MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/values/ic_launcher_background.xml b/android/llama.android/app-java/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 00000000..c5d5899f
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/res/values/preloaded_fonts.xml b/android/llama.android/app-java/src/main/res/values/preloaded_fonts.xml
new file mode 100644
index 00000000..56657f17
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/values/preloaded_fonts.xml
@@ -0,0 +1,6 @@
+
+
+
+ - @font/alegreya_sans_sc_extrabold
+
+
diff --git a/android/llama.android/app-java/src/main/res/values/strings.xml b/android/llama.android/app-java/src/main/res/values/strings.xml
new file mode 100644
index 00000000..2ff67712
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ LayoutTest
+ User Message
+
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/res/values/styles.xml b/android/llama.android/app-java/src/main/res/values/styles.xml
new file mode 100644
index 00000000..864fcf30
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/values/styles.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/android/llama.android/app-java/src/main/res/values/themes.xml b/android/llama.android/app-java/src/main/res/values/themes.xml
new file mode 100644
index 00000000..2ef46f0c
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/values/themes.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/res/xml/backup_rules.xml b/android/llama.android/app-java/src/main/res/xml/backup_rules.xml
new file mode 100644
index 00000000..fa0f996d
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/res/xml/data_extraction_rules.xml b/android/llama.android/app-java/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 00000000..9ee9997b
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/res/xml/file_paths.xml b/android/llama.android/app-java/src/main/res/xml/file_paths.xml
new file mode 100644
index 00000000..3d985443
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/xml/file_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/llama.android/app-java/src/main/res/xml/network_security_config.xml b/android/llama.android/app-java/src/main/res/xml/network_security_config.xml
new file mode 100644
index 00000000..f239ba46
--- /dev/null
+++ b/android/llama.android/app-java/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ api.nexaforai.com
+
+
+
diff --git a/android/llama.android/app-java/src/test/java/ai/nexa/app_java/ExampleUnitTest.java b/android/llama.android/app-java/src/test/java/ai/nexa/app_java/ExampleUnitTest.java
new file mode 100644
index 00000000..07b55b17
--- /dev/null
+++ b/android/llama.android/app-java/src/test/java/ai/nexa/app_java/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package ai.nexa.app_java;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/android/llama.android/build.gradle.kts b/android/llama.android/build.gradle.kts
new file mode 100644
index 00000000..53401400
--- /dev/null
+++ b/android/llama.android/build.gradle.kts
@@ -0,0 +1,13 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id("com.android.application") version "8.2.0" apply false
+ id("org.jetbrains.kotlin.android") version "1.9.0" apply false
+ id("com.android.library") version "8.2.0" apply false
+}
+buildscript {
+ val kotlin_version by extra("1.9.20")
+
+ dependencies {
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
+ }
+}
diff --git a/android/llama.android/gradle.properties b/android/llama.android/gradle.properties
new file mode 100644
index 00000000..2cbd6d19
--- /dev/null
+++ b/android/llama.android/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
diff --git a/android/llama.android/gradle/wrapper/gradle-wrapper.jar b/android/llama.android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..e708b1c0
Binary files /dev/null and b/android/llama.android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/android/llama.android/gradle/wrapper/gradle-wrapper.properties b/android/llama.android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..a3958c14
--- /dev/null
+++ b/android/llama.android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Dec 21 14:31:09 AEDT 2023
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/android/llama.android/gradlew b/android/llama.android/gradlew
new file mode 100755
index 00000000..4f906e0c
--- /dev/null
+++ b/android/llama.android/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/android/llama.android/llama/.gitignore b/android/llama.android/llama/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/android/llama.android/llama/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/android/llama.android/llama/build.gradle.kts b/android/llama.android/llama/build.gradle.kts
new file mode 100644
index 00000000..61b5b638
--- /dev/null
+++ b/android/llama.android/llama/build.gradle.kts
@@ -0,0 +1,71 @@
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ namespace = "com.nexa"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 33
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ ndk {
+ // Add NDK properties if wanted, e.g.
+ // abiFilters += listOf("arm64-v8a")
+ }
+ externalNativeBuild {
+ cmake {
+ arguments += "-DLLAMA_BUILD_COMMON=ON"
+ arguments += "-DCMAKE_BUILD_TYPE=Release"
+ cppFlags += listOf()
+ arguments += listOf()
+
+ cppFlags("")
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ externalNativeBuild {
+ cmake {
+ path("src/main/cpp/CMakeLists.txt")
+ version = "3.22.1"
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+
+ implementation("androidx.core:core-ktx:1.12.0")
+ implementation("com.google.code.gson:gson:2.11.0")
+ implementation("androidx.appcompat:appcompat:1.6.1")
+ implementation("com.google.android.material:material:1.11.0")
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.5")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+
+}
diff --git a/android/llama.android/llama/consumer-rules.pro b/android/llama.android/llama/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/android/llama.android/llama/proguard-rules.pro b/android/llama.android/llama/proguard-rules.pro
new file mode 100644
index 00000000..f1b42451
--- /dev/null
+++ b/android/llama.android/llama/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/android/llama.android/llama/src/androidTest/java/android/llama/nexa/ExampleInstrumentedTest.kt b/android/llama.android/llama/src/androidTest/java/android/llama/nexa/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..b13a1fe8
--- /dev/null
+++ b/android/llama.android/llama/src/androidTest/java/android/llama/nexa/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package android.llama.nexa
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("android.llama.cpp.test", appContext.packageName)
+ }
+}
diff --git a/android/llama.android/llama/src/main/AndroidManifest.xml b/android/llama.android/llama/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8bdb7e14
--- /dev/null
+++ b/android/llama.android/llama/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/android/llama.android/llama/src/main/cpp/CMakeLists.txt b/android/llama.android/llama/src/main/cpp/CMakeLists.txt
new file mode 100644
index 00000000..78b4e9a1
--- /dev/null
+++ b/android/llama.android/llama/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,53 @@
+# Sets the minimum CMake version required for this project.
+cmake_minimum_required(VERSION 3.22.1)
+set(CMAKE_BUILD_TYPE Release)
+# Declares the project name
+project("llama-android")
+
+# Enable FetchContent module
+include(FetchContent)
+
+FetchContent_Declare(
+ json
+ GIT_REPOSITORY https://github.com/nlohmann/json
+ GIT_TAG v3.11.3
+)
+FetchContent_MakeAvailable(json)
+
+# Declare llama.cpp repository
+FetchContent_Declare(
+ llama
+# GIT_REPOSITORY https://github.com/ggerganov/llama.cpp
+ GIT_REPOSITORY https://github.com/NexaAI/llama.cpp
+ GIT_TAG master
+)
+
+# Declare llava repository (if needed)
+FetchContent_Declare(
+ llava
+# GIT_REPOSITORY https://github.com/ggerganov/llama.cpp
+ GIT_REPOSITORY https://github.com/NexaAI/llama.cpp
+ GIT_TAG master
+ SOURCE_SUBDIR examples/llava
+)
+
+# Make the content available
+FetchContent_MakeAvailable(llama llava)
+
+# Create the main library
+add_library(${CMAKE_PROJECT_NAME} SHARED
+ llama-android.cpp
+ llava-android.cpp
+ common.cpp
+)
+
+
+# Link the required libraries
+target_link_libraries(${CMAKE_PROJECT_NAME}
+ nlohmann_json
+ llama
+ common
+ android
+ log
+ llava
+)
\ No newline at end of file
diff --git a/android/llama.android/llama/src/main/cpp/common.cpp b/android/llama.android/llama/src/main/cpp/common.cpp
new file mode 100644
index 00000000..b56e0292
--- /dev/null
+++ b/android/llama.android/llama/src/main/cpp/common.cpp
@@ -0,0 +1,33 @@
+
+ bool is_valid_utf8(const char * string) {
+ if (!string) {
+ return true;
+ }
+
+ const unsigned char * bytes = (const unsigned char *)string;
+ int num;
+
+ while (*bytes != 0x00) {
+ if ((*bytes & 0x80) == 0x00) {
+ num = 1;
+ } else if ((*bytes & 0xE0) == 0xC0) {
+ num = 2;
+ } else if ((*bytes & 0xF0) == 0xE0) {
+ num = 3;
+ } else if ((*bytes & 0xF8) == 0xF0) {
+ num = 4;
+ } else {
+ return false;
+ }
+
+ bytes += 1;
+ for (int i = 1; i < num; ++i) {
+ if ((*bytes & 0xC0) != 0x80) {
+ return false;
+ }
+ bytes += 1;
+ }
+ }
+
+ return true;
+ }
diff --git a/android/llama.android/llama/src/main/cpp/llama-android.cpp b/android/llama.android/llama/src/main/cpp/llama-android.cpp
new file mode 100644
index 00000000..260b899f
--- /dev/null
+++ b/android/llama.android/llama/src/main/cpp/llama-android.cpp
@@ -0,0 +1,409 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include "llama.h"
+#include "common.h"
+#include "llava.h"
+
+// Write C++ code here.
+//
+// Do not forget to dynamically load the C++ library into your application.
+//
+// For instance,
+//
+// In MainActivity.java:
+// static {
+// System.loadLibrary("llama-android");
+// }
+//
+// Or, in MainActivity.kt:
+// companion object {
+// init {
+// System.loadLibrary("llama-android")
+// }
+// }
+
+#define TAG "llama-android.cpp"
+#define LOGi(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
+#define LOGe(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
+
+jclass la_int_var;
+jmethodID la_int_var_value;
+jmethodID la_int_var_inc;
+
+std::string cached_token_chars;
+
+extern bool is_valid_utf8(const char* str);
+
+static void log_callback(ggml_log_level level, const char * fmt, void * data) {
+ if (level == GGML_LOG_LEVEL_ERROR) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, data);
+ else if (level == GGML_LOG_LEVEL_INFO) __android_log_print(ANDROID_LOG_INFO, TAG, fmt, data);
+ else if (level == GGML_LOG_LEVEL_WARN) __android_log_print(ANDROID_LOG_WARN, TAG, fmt, data);
+ else __android_log_print(ANDROID_LOG_DEFAULT, TAG, fmt, data);
+}
+
+extern "C"
+JNIEXPORT jlong JNICALL
+Java_com_nexa_LLamaAndroid_load_1model(JNIEnv *env, jobject, jstring filename) {
+ llama_model_params model_params = llama_model_default_params();
+
+ auto path_to_model = env->GetStringUTFChars(filename, 0);
+ LOGi("Loading model from %s", path_to_model);
+
+ auto model = llama_load_model_from_file(path_to_model, model_params);
+ env->ReleaseStringUTFChars(filename, path_to_model);
+
+ if (!model) {
+ LOGe("load_model() failed");
+ env->ThrowNew(env->FindClass("java/lang/IllegalStateException"), "load_model() failed");
+ return 0;
+ }
+
+ return reinterpret_cast(model);
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_nexa_LLamaAndroid_free_1model(JNIEnv *, jobject, jlong model) {
+ llama_free_model(reinterpret_cast(model));
+}
+
+extern "C"
+JNIEXPORT jlong JNICALL
+Java_com_nexa_LLamaAndroid_new_1context(JNIEnv *env, jobject, jlong jmodel) {
+ auto model = reinterpret_cast(jmodel);
+
+ if (!model) {
+ LOGe("new_context(): model cannot be null");
+ env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), "Model cannot be null");
+ return 0;
+ }
+
+ int n_threads = std::max(1, std::min(8, (int) sysconf(_SC_NPROCESSORS_ONLN) - 2));
+ LOGi("Using %d threads", n_threads);
+
+ llama_context_params ctx_params = llama_context_default_params();
+ ctx_params.seed = 1234;
+ ctx_params.n_ctx = 2048;
+ ctx_params.n_threads = n_threads;
+ ctx_params.n_threads_batch = n_threads;
+
+ llama_context * context = llama_new_context_with_model(model, ctx_params);
+
+ if (!context) {
+ LOGe("llama_new_context_with_model() returned null)");
+ env->ThrowNew(env->FindClass("java/lang/IllegalStateException"),
+ "llama_new_context_with_model() returned null)");
+ return 0;
+ }
+
+ return reinterpret_cast(context);
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_nexa_LLamaAndroid_free_1context(JNIEnv *, jobject, jlong context) {
+ llama_free(reinterpret_cast(context));
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_nexa_LLamaAndroid_backend_1free(JNIEnv *, jobject) {
+ llama_backend_free();
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_nexa_LLamaAndroid_log_1to_1android(JNIEnv *, jobject) {
+ llama_log_set(log_callback, NULL);
+}
+
+extern "C"
+JNIEXPORT jstring JNICALL
+Java_com_nexa_LLamaAndroid_bench_1model(
+ JNIEnv *env,
+ jobject,
+ jlong context_pointer,
+ jlong model_pointer,
+ jlong batch_pointer,
+ jint pp,
+ jint tg,
+ jint pl,
+ jint nr
+ ) {
+ auto pp_avg = 0.0;
+ auto tg_avg = 0.0;
+ auto pp_std = 0.0;
+ auto tg_std = 0.0;
+
+ const auto context = reinterpret_cast(context_pointer);
+ const auto model = reinterpret_cast(model_pointer);
+ const auto batch = reinterpret_cast(batch_pointer);
+
+ const int n_ctx = llama_n_ctx(context);
+
+ LOGi("n_ctx = %d", n_ctx);
+
+ int i, j;
+ int nri;
+ for (nri = 0; nri < nr; nri++) {
+ LOGi("Benchmark prompt processing (pp)");
+
+ llama_batch_clear(*batch);
+
+ const int n_tokens = pp;
+ for (i = 0; i < n_tokens; i++) {
+ llama_batch_add(*batch, 0, i, { 0 }, false);
+ }
+
+ batch->logits[batch->n_tokens - 1] = true;
+ llama_kv_cache_clear(context);
+
+ const auto t_pp_start = ggml_time_us();
+ if (llama_decode(context, *batch) != 0) {
+ LOGi("llama_decode() failed during prompt processing");
+ }
+ const auto t_pp_end = ggml_time_us();
+
+ // bench text generation
+
+ LOGi("Benchmark text generation (tg)");
+
+ llama_kv_cache_clear(context);
+ const auto t_tg_start = ggml_time_us();
+ for (i = 0; i < tg; i++) {
+
+ llama_batch_clear(*batch);
+ for (j = 0; j < pl; j++) {
+ llama_batch_add(*batch, 0, i, { j }, true);
+ }
+
+ LOGi("llama_decode() text generation: %d", i);
+ if (llama_decode(context, *batch) != 0) {
+ LOGi("llama_decode() failed during text generation");
+ }
+ }
+
+ const auto t_tg_end = ggml_time_us();
+
+ llama_kv_cache_clear(context);
+
+ const auto t_pp = double(t_pp_end - t_pp_start) / 1000000.0;
+ const auto t_tg = double(t_tg_end - t_tg_start) / 1000000.0;
+
+ const auto speed_pp = double(pp) / t_pp;
+ const auto speed_tg = double(pl * tg) / t_tg;
+
+ pp_avg += speed_pp;
+ tg_avg += speed_tg;
+
+ pp_std += speed_pp * speed_pp;
+ tg_std += speed_tg * speed_tg;
+
+ LOGi("pp %f t/s, tg %f t/s", speed_pp, speed_tg);
+ }
+
+ pp_avg /= double(nr);
+ tg_avg /= double(nr);
+
+ if (nr > 1) {
+ pp_std = sqrt(pp_std / double(nr - 1) - pp_avg * pp_avg * double(nr) / double(nr - 1));
+ tg_std = sqrt(tg_std / double(nr - 1) - tg_avg * tg_avg * double(nr) / double(nr - 1));
+ } else {
+ pp_std = 0;
+ tg_std = 0;
+ }
+
+ char model_desc[128];
+ llama_model_desc(model, model_desc, sizeof(model_desc));
+
+ const auto model_size = double(llama_model_size(model)) / 1024.0 / 1024.0 / 1024.0;
+ const auto model_n_params = double(llama_model_n_params(model)) / 1e9;
+
+ const auto backend = "(Android)"; // TODO: What should this be?
+
+ std::stringstream result;
+ result << std::setprecision(2);
+ result << "| model | size | params | backend | test | t/s |\n";
+ result << "| --- | --- | --- | --- | --- | --- |\n";
+ result << "| " << model_desc << " | " << model_size << "GiB | " << model_n_params << "B | " << backend << " | pp " << pp << " | " << pp_avg << " ± " << pp_std << " |\n";
+ result << "| " << model_desc << " | " << model_size << "GiB | " << model_n_params << "B | " << backend << " | tg " << tg << " | " << tg_avg << " ± " << tg_std << " |\n";
+
+ return env->NewStringUTF(result.str().c_str());
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_nexa_LLamaAndroid_free_1batch(JNIEnv *, jobject, jlong batch_pointer) {
+ llama_batch_free(*reinterpret_cast(batch_pointer));
+}
+
+extern "C"
+JNIEXPORT jlong JNICALL
+Java_com_nexa_LLamaAndroid_new_1batch(JNIEnv *, jobject, jint n_tokens, jint embd, jint n_seq_max) {
+
+ // Source: Copy of llama.cpp:llama_batch_init but heap-allocated.
+
+ llama_batch *batch = new llama_batch {
+ 0,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ 0,
+ 0,
+ 0,
+ };
+
+ if (embd) {
+ batch->embd = (float *) malloc(sizeof(float) * n_tokens * embd);
+ } else {
+ batch->token = (llama_token *) malloc(sizeof(llama_token) * n_tokens);
+ }
+
+ batch->pos = (llama_pos *) malloc(sizeof(llama_pos) * n_tokens);
+ batch->n_seq_id = (int32_t *) malloc(sizeof(int32_t) * n_tokens);
+ batch->seq_id = (llama_seq_id **) malloc(sizeof(llama_seq_id *) * n_tokens);
+ for (int i = 0; i < n_tokens; ++i) {
+ batch->seq_id[i] = (llama_seq_id *) malloc(sizeof(llama_seq_id) * n_seq_max);
+ }
+ batch->logits = (int8_t *) malloc(sizeof(int8_t) * n_tokens);
+
+ return reinterpret_cast(batch);
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_nexa_LLamaAndroid_backend_1init(JNIEnv *, jobject) {
+ llama_backend_init();
+}
+
+extern "C"
+JNIEXPORT jstring JNICALL
+Java_com_nexa_LLamaAndroid_system_1info(JNIEnv *env, jobject) {
+ return env->NewStringUTF(llama_print_system_info());
+}
+
+extern "C"
+JNIEXPORT jint JNICALL
+Java_com_nexa_LLamaAndroid_completion_1init(
+ JNIEnv *env,
+ jobject,
+ jlong context_pointer,
+ jlong batch_pointer,
+ jstring jtext,
+ jint n_len
+ ) {
+
+ cached_token_chars.clear();
+
+ const auto text = env->GetStringUTFChars(jtext, 0);
+ const auto context = reinterpret_cast(context_pointer);
+ const auto batch = reinterpret_cast(batch_pointer);
+
+ const auto tokens_list = llama_tokenize(context, text, 1);
+
+ auto n_ctx = llama_n_ctx(context);
+ auto n_kv_req = tokens_list.size() + (n_len - tokens_list.size());
+
+ LOGi("n_len = %d, n_ctx = %d, n_kv_req = %d", n_len, n_ctx, n_kv_req);
+
+ if (n_kv_req > n_ctx) {
+ LOGe("error: n_kv_req > n_ctx, the required KV cache size is not big enough");
+ }
+
+ for (auto id : tokens_list) {
+ LOGi("%s", llama_token_to_piece(context, id).c_str());
+ }
+
+ llama_batch_clear(*batch);
+
+ // evaluate the initial prompt
+ for (auto i = 0; i < tokens_list.size(); i++) {
+ llama_batch_add(*batch, tokens_list[i], i, { 0 }, false);
+ }
+
+ // llama_decode will output logits only for the last token of the prompt
+ batch->logits[batch->n_tokens - 1] = true;
+
+ if (llama_decode(context, *batch) != 0) {
+ LOGe("llama_decode() failed");
+ }
+
+ env->ReleaseStringUTFChars(jtext, text);
+
+ return batch->n_tokens;
+}
+
+extern "C"
+JNIEXPORT jstring JNICALL
+Java_com_nexa_LLamaAndroid_completion_1loop(
+ JNIEnv * env,
+ jobject,
+ jlong context_pointer,
+ jlong batch_pointer,
+ jint n_len,
+ jobject intvar_ncur
+) {
+ const auto context = reinterpret_cast(context_pointer);
+ const auto batch = reinterpret_cast(batch_pointer);
+ const auto model = llama_get_model(context);
+
+ if (!la_int_var) la_int_var = env->GetObjectClass(intvar_ncur);
+ if (!la_int_var_value) la_int_var_value = env->GetMethodID(la_int_var, "getValue", "()I");
+ if (!la_int_var_inc) la_int_var_inc = env->GetMethodID(la_int_var, "inc", "()V");
+
+ auto n_vocab = llama_n_vocab(model);
+ auto logits = llama_get_logits_ith(context, batch->n_tokens - 1);
+
+ std::vector candidates;
+ candidates.reserve(n_vocab);
+
+ for (llama_token token_id = 0; token_id < n_vocab; token_id++) {
+ candidates.emplace_back(llama_token_data{ token_id, logits[token_id], 0.0f });
+ }
+
+ llama_token_data_array candidates_p = { candidates.data(), candidates.size(), false };
+
+ // sample the most likely token
+ const auto new_token_id = llama_sample_token_greedy(context, &candidates_p);
+
+ const auto n_cur = env->CallIntMethod(intvar_ncur, la_int_var_value);
+ if (llama_token_is_eog(model, new_token_id) || n_cur == n_len) {
+ return nullptr;
+ }
+
+ auto new_token_chars = llama_token_to_piece(context, new_token_id);
+ cached_token_chars += new_token_chars;
+
+ jstring new_token = nullptr;
+ if (is_valid_utf8(cached_token_chars.c_str())) {
+ new_token = env->NewStringUTF(cached_token_chars.c_str());
+ LOGi("cached: %s, new_token_chars: `%s`, id: %d", cached_token_chars.c_str(), new_token_chars.c_str(), new_token_id);
+ cached_token_chars.clear();
+ } else {
+ new_token = env->NewStringUTF("");
+ }
+
+ llama_batch_clear(*batch);
+ llama_batch_add(*batch, new_token_id, n_cur, { 0 }, true);
+
+ env->CallVoidMethod(intvar_ncur, la_int_var_inc);
+
+ if (llama_decode(context, *batch) != 0) {
+ LOGe("llama_decode() returned null");
+ }
+
+ return new_token;
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_nexa_LLamaAndroid_kv_1cache_1clear(JNIEnv *, jobject, jlong context) {
+ llama_kv_cache_clear(reinterpret_cast(context));
+}
diff --git a/android/llama.android/llama/src/main/cpp/llava-android.cpp b/android/llama.android/llama/src/main/cpp/llava-android.cpp
new file mode 100644
index 00000000..f2ce542d
--- /dev/null
+++ b/android/llama.android/llama/src/main/cpp/llava-android.cpp
@@ -0,0 +1,348 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include "llama.h"
+#include "common.h"
+#include "llava-cli.cpp"
+#include
+
+#define TAG "llava-android.cpp"
+#define LOGi(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
+#define LOGe(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
+
+extern bool is_valid_utf8(const char* str);
+
+std::string jstring2str(JNIEnv* env, jstring jstr) {
+ if (!jstr) {
+ return "";
+ }
+ const char* str = env->GetStringUTFChars(jstr, nullptr);
+ if (!str) {
+ return "";
+ }
+ std::string ret(str);
+ env->ReleaseStringUTFChars(jstr, str);
+ return ret;
+}
+
+#include
+#include
+#include
+
+// Helper function to throw a Java exception from JNI
+void throwJavaException(JNIEnv* env, const char* className, const std::string& message) {
+ // Find the exception class
+ jclass exceptionClass = env->FindClass(className);
+ if (exceptionClass != nullptr) {
+ // Throw the exception with the given message
+ env->ThrowNew(exceptionClass, message.c_str());
+ env->DeleteLocalRef(exceptionClass); // Clean up the local reference
+ } else {
+ // If the specified exception class cannot be found, fall back to RuntimeException
+ std::cerr << "Error: Cannot find exception class: " << className << std::endl;
+ jclass runtimeExceptionClass = env->FindClass("java/lang/RuntimeException");
+ if (runtimeExceptionClass != nullptr) {
+ env->ThrowNew(runtimeExceptionClass, ("Fallback: " + message).c_str());
+ env->DeleteLocalRef(runtimeExceptionClass); // Clean up
+ } else {
+ std::cerr << "Critical Error: Cannot find RuntimeException class" << std::endl;
+ }
+ }
+}
+
+
+
+extern "C" JNIEXPORT jlong JNICALL
+Java_com_nexa_NexaVlmInference_init_1params(JNIEnv *env, jobject /* this */, jstring jmodel, jstring jmmproj) {
+ try {
+ // Initialize timers and common components
+ ggml_time_init();
+
+
+ const char* model_chars = env->GetStringUTFChars(jmodel, nullptr);
+ const char* mmproj_chars = env->GetStringUTFChars(jmmproj, nullptr);
+
+ const char* argv = "omni-wrapper-py";
+ char* nc_argv = const_cast(argv);
+ gpt_params* params = new gpt_params();
+ gpt_params_parse(1, &nc_argv, *params);
+
+ params->model = std::string(model_chars);
+ params->mmproj = std::string(mmproj_chars);
+
+ env->ReleaseStringUTFChars(jmodel, model_chars);
+ env->ReleaseStringUTFChars(jmmproj, mmproj_chars);
+
+ return reinterpret_cast(params);
+ } catch (const nlohmann::json::exception& e) {
+ throwJavaException(env, "java/lang/IllegalArgumentException",
+ std::string("JSON parsing error: ") + e.what());
+ } catch (const std::exception& e) {
+ throwJavaException(env, "java/lang/RuntimeException",
+ std::string("Error loading model 1: ") + e.what());
+ } catch (...) {
+ throwJavaException(env, "java/lang/RuntimeException",
+ "Unknown error occurred while loading model");
+ }
+
+ return 0;
+}
+
+extern "C" JNIEXPORT jlong JNICALL
+Java_com_nexa_NexaVlmInference_load_1model(JNIEnv *env, jobject /* this */, jlong jparams) {
+ try {
+ const auto params = reinterpret_cast(jparams);
+
+ auto* model = llava_init(params);
+ if (model == nullptr) {
+ throwJavaException(env, "java/lang/RuntimeException", "Failed to initialize model");
+ return 0;
+ }
+
+ return reinterpret_cast(model);
+ } catch (const nlohmann::json::exception& e) {
+ throwJavaException(env, "java/lang/IllegalArgumentException",
+ std::string("JSON parsing error: ") + e.what());
+ } catch (const std::exception& e) {
+ throwJavaException(env, "java/lang/RuntimeException",
+ std::string("Error loading model 1: ") + e.what());
+ } catch (...) {
+ throwJavaException(env, "java/lang/RuntimeException",
+ "Unknown error occurred while loading model");
+ }
+
+ return 0;
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_com_nexa_NexaVlmInference_update_1params(JNIEnv *env, jobject /* this */, jlong jparams, jfloat jtemp , jint jtopK, jfloat jtopP) {
+ int32_t top_k = (int32_t) jtopK;
+ float top_p = (float) jtopP;
+ float temp = (float) jtemp;
+ const auto params = reinterpret_cast(jparams);
+ params->sparams.top_k = top_k;
+ params->sparams.top_p = top_p;
+ params->sparams.temp = temp;
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_com_nexa_NexaVlmInference_free_1model(JNIEnv *env, jobject /* this */, jlong jmodel) {
+ const auto llava_model = reinterpret_cast(jmodel);
+
+ llama_free_model(llava_model);
+}
+
+
+extern "C" JNIEXPORT jlong JNICALL
+Java_com_nexa_NexaVlmInference_llava_1init_1context(JNIEnv *env, jobject /* this */, jlong jparams, jlong jmodel) {
+ try {
+ const auto params = reinterpret_cast(jparams);
+
+ const auto llava_model = reinterpret_cast(jmodel);
+ auto* ctx_llava = llava_init_context(params, llava_model);
+ if (ctx_llava == nullptr) {
+ throwJavaException(env, "java/lang/RuntimeException", "Failed to initialize llava ctx");
+ return 0;
+ }
+
+ return reinterpret_cast(ctx_llava);
+ } catch (const nlohmann::json::exception& e) {
+ throwJavaException(env, "java/lang/IllegalArgumentException",
+ std::string("JSON parsing error: ") + e.what());
+ } catch (const std::exception& e) {
+ throwJavaException(env, "java/lang/RuntimeException",
+ std::string("Error loading model: ") + e.what());
+ } catch (...) {
+ throwJavaException(env, "java/lang/RuntimeException",
+ "Unknown error occurred while loading model");
+ }
+
+ return 0;
+}
+
+extern "C" JNIEXPORT jlong JNICALL
+Java_com_nexa_NexaVlmInference_llava_1ctx_1free(JNIEnv *env, jobject /* this */, jlong llava_ctx_pointer) {
+ try {
+ auto* llava_ctx = reinterpret_cast(llava_ctx_pointer);
+ if (llava_ctx == nullptr) {
+ throwJavaException(env, "java/lang/RuntimeException", "Null pointer");
+ return 0;
+ }
+
+ llava_ctx->model = NULL;
+ llava_free(llava_ctx);
+ } catch (...) {
+ throwJavaException(env, "java/lang/RuntimeException",
+ "Unknown error occurred while freeing ctx");
+ }
+}
+
+extern "C" JNIEXPORT jint JNICALL
+Java_com_nexa_NexaVlmInference_llava_1image_1embed_1free(JNIEnv *env, jobject /* this */, jlong llava_image_embed_pointer) {
+ try {
+ if (llava_image_embed_pointer == 0) {
+ throwJavaException(env, "java/lang/RuntimeException", "Pointer is null.");
+ return -1;
+ }
+
+ auto* llava_image_embed = reinterpret_cast(llava_image_embed_pointer);
+ if (llava_image_embed == nullptr ) {
+ throwJavaException(env, "java/lang/RuntimeException", "Pointer cast resulted in null.");
+ return -1;
+ }
+ if(llava_image_embed->embed == nullptr ) {
+ throwJavaException(env, "java/lang/RuntimeException", "Pointer cast resulted in null.");
+ return -1;
+ }
+
+ llava_image_embed_free(llava_image_embed);
+ } catch (const std::exception &e) {
+ // 捕获标准异常
+ throwJavaException(env, "java/lang/RuntimeException", e.what());
+ return -1;
+ } catch (...) {
+ // 捕获未知异常
+ throwJavaException(env, "java/lang/RuntimeException",
+ "Unknown error occurred while freeing image");
+ return -1;
+ }
+
+ return 0; // 成功
+}
+
+
+extern "C" JNIEXPORT jlong JNICALL
+Java_com_nexa_NexaVlmInference_load_1image(JNIEnv *env, jobject /* this */, jlong llava_ctx_pointer, jlong jparams, jstring imagePath) {
+ try {
+ auto* params = reinterpret_cast(jparams);
+ auto* ctx_llava = reinterpret_cast(llava_ctx_pointer);
+
+ std::string image_str = jstring2str(env, imagePath);
+ auto * image_embed = load_image(ctx_llava, params, image_str);
+ if (image_embed == nullptr) {
+ throwJavaException(env, "java/lang/RuntimeException", "Failed to initialize llava ctx");
+ return 0;
+ }
+
+ return reinterpret_cast(image_embed);
+ } catch (const nlohmann::json::exception& e) {
+ throwJavaException(env, "java/lang/IllegalArgumentException",
+ std::string("JSON parsing error: ") + e.what());
+ } catch (const std::exception& e) {
+ throwJavaException(env, "java/lang/RuntimeException",
+ std::string("Error loading model: ") + e.what());
+ } catch (...) {
+ throwJavaException(env, "java/lang/RuntimeException",
+ "Unknown error occurred while loading model");
+ }
+}
+
+
+extern "C" JNIEXPORT jlong JNICALL
+Java_com_nexa_NexaVlmInference_llava_1eval(JNIEnv *env, jobject /* this */, jlong llava_ctx_pointer, jlong jparams, jlong llava_image_embed_pointer, jstring jprompt) {
+
+ try {
+ auto* params = reinterpret_cast(jparams);
+ auto* image_embed = reinterpret_cast(llava_image_embed_pointer);
+ auto* ctx_llava = reinterpret_cast(llava_ctx_pointer);
+
+ int* n_past = new int(0);
+
+ const int max_tgt_len = params->n_predict < 0 ? 256 : params->n_predict;
+ std::string prompt = jstring2str(env, jprompt);
+
+ std::string system_prompt, user_prompt;
+ system_prompt = "A chat between a curious human and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the human's questions.\nUSER:";
+ user_prompt = prompt + "\nASSISTANT:";
+
+ eval_string(ctx_llava->ctx_llama, system_prompt.c_str(), params->n_batch, n_past, true);
+ llava_eval_image_embed(ctx_llava->ctx_llama, image_embed, params->n_batch, n_past);
+ eval_string(ctx_llava->ctx_llama, user_prompt.c_str(), params->n_batch, n_past, false);
+
+ return reinterpret_cast(n_past);
+ } catch (const nlohmann::json::exception& e) {
+ throwJavaException(env, "java/lang/IllegalArgumentException",
+ std::string("JSON parsing error: ") + e.what());
+ } catch (const std::exception& e) {
+ throwJavaException(env, "java/lang/RuntimeException",
+ std::string("Error loading model: ") + e.what());
+ } catch (...) {
+ throwJavaException(env, "java/lang/RuntimeException",
+ "Unknown error occurred while loading model");
+ }
+
+ return 0;
+}
+
+
+extern "C" JNIEXPORT jlong JNICALL
+Java_com_nexa_NexaVlmInference_llava_1sampler_1init(JNIEnv *env, jobject /* this */, jlong llava_ctx_pointer, jlong jparams) {
+
+ try {
+ auto* params = reinterpret_cast(jparams);
+ auto* ctx_llava = reinterpret_cast(llava_ctx_pointer);
+ struct llama_sampling_context * smpl = llama_sampling_init(params->sparams);
+
+ if (smpl == nullptr) {
+ throwJavaException(env, "java/lang/RuntimeException", "Failed to initialize llava ctx");
+ return 0;
+ }
+
+ return reinterpret_cast(smpl);
+ } catch (const nlohmann::json::exception& e) {
+ throwJavaException(env, "java/lang/IllegalArgumentException",
+ std::string("JSON parsing error: ") + e.what());
+ } catch (const std::exception& e) {
+ throwJavaException(env, "java/lang/RuntimeException",
+ std::string("Error loading model: ") + e.what());
+ } catch (...) {
+ throwJavaException(env, "java/lang/RuntimeException",
+ "Unknown error occurred while loading model");
+ }
+
+ return 0;
+}
+
+extern "C" JNIEXPORT jstring JNICALL
+Java_com_nexa_NexaVlmInference_llava_1sample(JNIEnv *env, jobject /* this */, jlong llava_ctx_pointer, jlong sampler, jlong jnpast, jlong jcached_tokens) {
+ auto* smpl = reinterpret_cast(sampler);
+ auto* ctx_llava = reinterpret_cast(llava_ctx_pointer);
+ auto* cached_tokens = reinterpret_cast(jcached_tokens);
+ auto* n_past = reinterpret_cast(jnpast);
+ const char* tmp = sample(smpl, ctx_llava->ctx_llama, n_past);
+ *cached_tokens += tmp;
+ jstring new_token = nullptr;
+ if (is_valid_utf8(cached_tokens->c_str())) {
+ new_token = env->NewStringUTF(cached_tokens->c_str());
+ cached_tokens->clear();
+ } else {
+ new_token = env->NewStringUTF("");
+ }
+
+ return new_token;
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_com_nexa_NexaVlmInference_llava_1sample_1free(JNIEnv *env, jobject /* this */, jlong sampler) {
+ auto* smpl = reinterpret_cast(sampler);
+ llama_sampling_free(smpl);
+}
+
+
+extern "C" JNIEXPORT jlong JNICALL
+Java_com_nexa_NexaVlmInference_cached_1token_1init(JNIEnv *env, jobject /* this */) {
+ std::string* strPtr = new std::string("");
+ return reinterpret_cast(strPtr);
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_com_nexa_NexaVlmInference_cached_1token_1free(JNIEnv *env, jobject /* this */, jlong jcached_tokens) {
+ std::string* str = reinterpret_cast(jcached_tokens);
+
+ if (str) {
+ delete str;
+ }
+}
\ No newline at end of file
diff --git a/android/llama.android/llama/src/main/java/com/nexa/LLamaAndroid.kt b/android/llama.android/llama/src/main/java/com/nexa/LLamaAndroid.kt
new file mode 100644
index 00000000..ca11ec58
--- /dev/null
+++ b/android/llama.android/llama/src/main/java/com/nexa/LLamaAndroid.kt
@@ -0,0 +1,179 @@
+package com.nexa
+
+import android.util.Log
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.withContext
+import java.util.concurrent.Executors
+import kotlin.concurrent.thread
+
+class LLamaAndroid {
+ private val tag: String? = this::class.simpleName
+
+ private val threadLocalState: ThreadLocal = ThreadLocal.withInitial { State.Idle }
+
+ private val runLoop: CoroutineDispatcher = Executors.newSingleThreadExecutor {
+ thread(start = false, name = "Llm-RunLoop") {
+ Log.d(tag, "Dedicated thread for native code: ${Thread.currentThread().name}")
+
+ // No-op if called more than once.
+ System.loadLibrary("llama-android")
+
+ // Set llama log handler to Android
+ log_to_android()
+ backend_init(false)
+
+ Log.d(tag, system_info())
+
+ it.run()
+ }.apply {
+ uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { _, exception: Throwable ->
+ Log.e(tag, "Unhandled exception", exception)
+ }
+ }
+ }.asCoroutineDispatcher()
+
+ private val nlen: Int = 64
+
+ private external fun log_to_android()
+ private external fun load_model(filename: String): Long
+ private external fun free_model(model: Long)
+ private external fun new_context(model: Long): Long
+ private external fun free_context(context: Long)
+ private external fun backend_init(numa: Boolean)
+ private external fun backend_free()
+ private external fun new_batch(nTokens: Int, embd: Int, nSeqMax: Int): Long
+ private external fun free_batch(batch: Long)
+ private external fun new_sampler(): Long
+ private external fun free_sampler(sampler: Long)
+ private external fun bench_model(
+ context: Long,
+ model: Long,
+ batch: Long,
+ pp: Int,
+ tg: Int,
+ pl: Int,
+ nr: Int
+ ): String
+
+ private external fun system_info(): String
+
+ private external fun completion_init(
+ context: Long,
+ batch: Long,
+ text: String,
+ nLen: Int
+ ): Int
+
+ private external fun completion_loop(
+ context: Long,
+ batch: Long,
+ sampler: Long,
+ nLen: Int,
+ ncur: IntVar
+ ): String?
+
+ private external fun kv_cache_clear(context: Long)
+
+ suspend fun bench(pp: Int, tg: Int, pl: Int, nr: Int = 1): String {
+ return withContext(runLoop) {
+ when (val state = threadLocalState.get()) {
+ is State.Loaded -> {
+ Log.d(tag, "bench(): $state")
+ bench_model(state.context, state.model, state.batch, pp, tg, pl, nr)
+ }
+
+ else -> throw IllegalStateException("No model loaded")
+ }
+ }
+ }
+
+ suspend fun load(pathToModel: String) {
+ withContext(runLoop) {
+ when (threadLocalState.get()) {
+ is State.Idle -> {
+ val model = load_model(pathToModel)
+ if (model == 0L) throw IllegalStateException("load_model() failed")
+
+ val context = new_context(model)
+ if (context == 0L) throw IllegalStateException("new_context() failed")
+
+ val batch = new_batch(512, 0, 1)
+ if (batch == 0L) throw IllegalStateException("new_batch() failed")
+
+ val sampler = new_sampler()
+ if (sampler == 0L) throw IllegalStateException("new_sampler() failed")
+
+ Log.i(tag, "Loaded model $pathToModel")
+ threadLocalState.set(State.Loaded(model, context, batch, sampler))
+ }
+ else -> throw IllegalStateException("Model already loaded")
+ }
+ }
+ }
+
+ fun send(message: String): Flow = flow {
+ when (val state = threadLocalState.get()) {
+ is State.Loaded -> {
+ val ncur = IntVar(completion_init(state.context, state.batch, message, nlen))
+ while (ncur.value <= nlen) {
+ val str = completion_loop(state.context, state.batch, state.sampler, nlen, ncur)
+ if (str == null) {
+ break
+ }
+ emit(str)
+ }
+ kv_cache_clear(state.context)
+ }
+ else -> {}
+ }
+ }.flowOn(runLoop)
+
+ /**
+ * Unloads the model and frees resources.
+ *
+ * This is a no-op if there's no model loaded.
+ */
+ suspend fun unload() {
+ withContext(runLoop) {
+ when (val state = threadLocalState.get()) {
+ is State.Loaded -> {
+ free_context(state.context)
+ free_model(state.model)
+ free_batch(state.batch)
+ free_sampler(state.sampler);
+
+ threadLocalState.set(State.Idle)
+ }
+ else -> {}
+ }
+ }
+ }
+
+ companion object {
+ private class IntVar(value: Int) {
+ @Volatile
+ var value: Int = value
+ private set
+
+ fun inc() {
+ synchronized(this) {
+ value += 1
+ }
+ }
+ }
+
+ private sealed interface State {
+ data object Idle: State
+ data class Loaded(val model: Long, val context: Long, val batch: Long, val sampler: Long): State
+ }
+
+ // Enforce only one instance of Llm.
+ private val _instance: LLamaAndroid = LLamaAndroid()
+
+ fun instance(): LLamaAndroid = _instance
+ }
+}
diff --git a/android/llama.android/llama/src/main/java/com/nexa/NexaVlmInference.kt b/android/llama.android/llama/src/main/java/com/nexa/NexaVlmInference.kt
new file mode 100644
index 00000000..82a617de
--- /dev/null
+++ b/android/llama.android/llama/src/main/java/com/nexa/NexaVlmInference.kt
@@ -0,0 +1,196 @@
+package com.nexa
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+
+class NexaVlmInference(
+ private val modelPath: String,
+ private val projectorPath: String,
+ private var imagePath: String,
+ private var stopWords: List = emptyList(),
+ private var temperature: Float = 0.8f,
+ private var maxNewTokens: Int = 64,
+ private var topK: Int = 40,
+ private var topP: Float = 0.95f
+) {
+ init {
+ System.loadLibrary("llama-android")
+ }
+
+ private var paramsPointer: Long = 0
+ private var modelPointer: Long = 0
+ private var llavaCtxPointer: Long = 0
+ private var embedImagePointer: Long = 0
+ private var samplerPointer: Long = 0
+ private var nPastPointer: Long = 0
+ private var generatedTokenNum: Int = 0
+ private var generatedText: String = ""
+ private var isModelLoaded: Boolean = false
+ private var cachedTokenPointer: Long = 0
+
+ private external fun init_params(modelPath: String, mmprojPath: String): Long
+
+ private external fun update_params(params: Long, temperature: Float, topK: Int, topP: Float)
+
+ private external fun load_model(params: Long): Long
+
+ private external fun free_model(model: Long)
+
+ private external fun llava_init_context(params: Long, model: Long): Long
+
+ private external fun llava_ctx_free(ctx: Long)
+
+ private external fun load_image(ctx: Long, params: Long, imagepath: String): Long
+
+ private external fun llava_image_embed_free(llava_image_embed: Long)
+
+ private external fun llava_eval(ctx: Long, params: Long, llava_image_embed: Long, prompt: String): Long
+
+ private external fun llava_sampler_init(ctx: Long, params: Long): Long
+
+ private external fun llava_sample(ctx: Long, sampler: Long, n_past: Long, cached_tokens: Long): String
+
+ private external fun cached_token_init(): Long
+
+ private external fun cached_token_free(cached_tokens: Long)
+
+ private external fun llava_sample_free(sampler: Long)
+
+ @Synchronized
+ fun loadModel() {
+ if(isModelLoaded){
+ throw RuntimeException("Model is already loaded.")
+ }
+ try {
+ paramsPointer = init_params(modelPath, mmprojPath = projectorPath)
+ modelPointer = load_model(paramsPointer)
+ isModelLoaded = true
+ } catch (e: Exception) {
+ println(e)
+ } catch (e: UnsatisfiedLinkError) {
+ throw RuntimeException("Native method not found: ${e.message}")
+ }
+ }
+
+ fun dispose() {
+ if(paramsPointer!=0L){
+ paramsPointer = 0;
+ }
+ if (modelPointer != 0L) {
+ free_model(modelPointer)
+ modelPointer = 0;
+ }
+ }
+
+ private fun updateParams(
+ stopWords: List? = null,
+ temperature: Float? = null,
+ maxNewTokens: Int? = null,
+ topK: Int? = null,
+ topP: Float? = null
+ ) {
+ if(stopWords != null){
+ this.stopWords = stopWords
+ }
+ if (temperature != null) {
+ this.temperature = temperature
+ }
+ if (maxNewTokens != null) {
+ this.maxNewTokens = maxNewTokens
+ }
+ if (topK != null) {
+ this.topK = topK;
+ }
+ if (topP != null) {
+ this.topP = topP
+ }
+
+ if(paramsPointer != 0L) {
+ update_params(paramsPointer, this.temperature, this.topK, this.topP)
+ }
+ }
+
+ private fun shouldStop(): Boolean {
+ if(this.generatedTokenNum >= this.maxNewTokens){
+ return true
+ }
+
+ return stopWords.any { generatedText.contains(it, ignoreCase = true) }
+ }
+
+ private fun resetGeneration() {
+ generatedTokenNum = 0
+ generatedText = ""
+ }
+
+ @Synchronized
+ fun createCompletionStream(
+ prompt: String,
+ imagePath: String? = null,
+ stopWords: List? = null,
+ temperature: Float? = null,
+ maxNewTokens: Int? = null,
+ topK: Int? = null,
+ topP: Float? = null
+ ): Flow = flow {
+ if(!isModelLoaded){
+ throw RuntimeException("Model is not loaded.")
+ }
+
+ // Reset generation state at the start
+ resetGeneration()
+ updateParams(stopWords, temperature, maxNewTokens, topK, topP)
+
+ val imagePathToUse = imagePath ?: this@NexaVlmInference.imagePath
+ llavaCtxPointer = llava_init_context(paramsPointer, modelPointer)
+ embedImagePointer = load_image(llavaCtxPointer, paramsPointer, imagePathToUse)
+ nPastPointer = llava_eval(llavaCtxPointer, paramsPointer, embedImagePointer, prompt)
+ samplerPointer = llava_sampler_init(llavaCtxPointer, paramsPointer)
+ cachedTokenPointer = cached_token_init()
+
+ try {
+ while (true) {
+ val sampledText = llava_sample(llavaCtxPointer, samplerPointer, nPastPointer, cachedTokenPointer)
+ generatedTokenNum += 1
+ generatedText += sampledText
+ if(shouldStop()){
+ break
+ }
+ emit(sampledText)
+ }
+ } finally {
+ // Clean up resources and reset generation state
+ cleanupResources()
+ resetGeneration()
+ }
+ }.flowOn(Dispatchers.IO)
+
+ private fun cleanupResources() {
+ if(cachedTokenPointer != 0L){
+ cached_token_free(cachedTokenPointer)
+ cachedTokenPointer = 0
+ }
+
+ if (samplerPointer != 0L) {
+ llava_sample_free(samplerPointer)
+ samplerPointer = 0
+ }
+
+ if (embedImagePointer != 0L) {
+ try {
+ llava_image_embed_free(embedImagePointer)
+ embedImagePointer = 0
+ } catch (e: Exception) {
+ println(e)
+ } catch (e: Error) {
+ throw RuntimeException("Native method not found: ${e.message}")
+ }
+ }
+
+ if (llavaCtxPointer != 0L) {
+ llava_ctx_free(llavaCtxPointer)
+ llavaCtxPointer = 0
+ }
+ }
+}
diff --git a/android/llama.android/llama/src/test/java/android/llama/nexa/ExampleUnitTest.kt b/android/llama.android/llama/src/test/java/android/llama/nexa/ExampleUnitTest.kt
new file mode 100644
index 00000000..cfe765a1
--- /dev/null
+++ b/android/llama.android/llama/src/test/java/android/llama/nexa/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package android.llama.nexa
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
diff --git a/android/llama.android/settings.gradle.kts b/android/llama.android/settings.gradle.kts
new file mode 100644
index 00000000..71dcab93
--- /dev/null
+++ b/android/llama.android/settings.gradle.kts
@@ -0,0 +1,19 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "LlamaAndroid"
+//include(":app")
+include(":llama")
+include(":app-java")