diff --git a/mobile/examples/phi-3-vision/android/.gradle/8.0/checksums/checksums.lock b/mobile/examples/phi-3-vision/android/.gradle/8.0/checksums/checksums.lock new file mode 100644 index 000000000..a38d9f87c Binary files /dev/null and b/mobile/examples/phi-3-vision/android/.gradle/8.0/checksums/checksums.lock differ diff --git a/mobile/examples/phi-3-vision/android/.gradle/8.0/checksums/md5-checksums.bin b/mobile/examples/phi-3-vision/android/.gradle/8.0/checksums/md5-checksums.bin new file mode 100644 index 000000000..558d51a57 Binary files /dev/null and b/mobile/examples/phi-3-vision/android/.gradle/8.0/checksums/md5-checksums.bin differ diff --git a/mobile/examples/phi-3-vision/android/.gradle/8.0/checksums/sha1-checksums.bin b/mobile/examples/phi-3-vision/android/.gradle/8.0/checksums/sha1-checksums.bin new file mode 100644 index 000000000..bfab45525 Binary files /dev/null and b/mobile/examples/phi-3-vision/android/.gradle/8.0/checksums/sha1-checksums.bin differ diff --git a/mobile/examples/phi-3-vision/android/.gradle/8.0/dependencies-accessors/dependencies-accessors.lock b/mobile/examples/phi-3-vision/android/.gradle/8.0/dependencies-accessors/dependencies-accessors.lock new file mode 100644 index 000000000..a64d81212 Binary files /dev/null and b/mobile/examples/phi-3-vision/android/.gradle/8.0/dependencies-accessors/dependencies-accessors.lock differ diff --git a/mobile/examples/phi-3-vision/android/.gradle/8.0/dependencies-accessors/gc.properties b/mobile/examples/phi-3-vision/android/.gradle/8.0/dependencies-accessors/gc.properties new file mode 100644 index 000000000..e69de29bb diff --git a/mobile/examples/phi-3-vision/android/.gradle/8.0/executionHistory/executionHistory.bin b/mobile/examples/phi-3-vision/android/.gradle/8.0/executionHistory/executionHistory.bin new file mode 100644 index 000000000..febb3e0cc Binary files /dev/null and b/mobile/examples/phi-3-vision/android/.gradle/8.0/executionHistory/executionHistory.bin differ diff --git a/mobile/examples/phi-3-vision/android/.gradle/8.0/executionHistory/executionHistory.lock b/mobile/examples/phi-3-vision/android/.gradle/8.0/executionHistory/executionHistory.lock new file mode 100644 index 000000000..354a7e884 Binary files /dev/null and b/mobile/examples/phi-3-vision/android/.gradle/8.0/executionHistory/executionHistory.lock differ diff --git a/mobile/examples/phi-3-vision/android/.gradle/8.0/fileChanges/last-build.bin b/mobile/examples/phi-3-vision/android/.gradle/8.0/fileChanges/last-build.bin new file mode 100644 index 000000000..f76dd238a Binary files /dev/null and b/mobile/examples/phi-3-vision/android/.gradle/8.0/fileChanges/last-build.bin differ diff --git a/mobile/examples/phi-3-vision/android/.gradle/8.0/fileHashes/fileHashes.bin b/mobile/examples/phi-3-vision/android/.gradle/8.0/fileHashes/fileHashes.bin new file mode 100644 index 000000000..3de5c8074 Binary files /dev/null and b/mobile/examples/phi-3-vision/android/.gradle/8.0/fileHashes/fileHashes.bin differ diff --git a/mobile/examples/phi-3-vision/android/.gradle/8.0/fileHashes/fileHashes.lock b/mobile/examples/phi-3-vision/android/.gradle/8.0/fileHashes/fileHashes.lock new file mode 100644 index 000000000..38927b526 Binary files /dev/null and b/mobile/examples/phi-3-vision/android/.gradle/8.0/fileHashes/fileHashes.lock differ diff --git a/mobile/examples/phi-3-vision/android/.gradle/8.0/fileHashes/resourceHashesCache.bin b/mobile/examples/phi-3-vision/android/.gradle/8.0/fileHashes/resourceHashesCache.bin new file mode 100644 index 000000000..e4e1e9df5 Binary files /dev/null and b/mobile/examples/phi-3-vision/android/.gradle/8.0/fileHashes/resourceHashesCache.bin differ diff --git a/mobile/examples/phi-3-vision/android/.gradle/8.0/gc.properties b/mobile/examples/phi-3-vision/android/.gradle/8.0/gc.properties new file mode 100644 index 000000000..e69de29bb diff --git a/mobile/examples/phi-3-vision/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/mobile/examples/phi-3-vision/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 000000000..6f95a5dd1 Binary files /dev/null and b/mobile/examples/phi-3-vision/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/mobile/examples/phi-3-vision/android/.gradle/buildOutputCleanup/cache.properties b/mobile/examples/phi-3-vision/android/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 000000000..a7ad1f7f0 --- /dev/null +++ b/mobile/examples/phi-3-vision/android/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Fri Jul 19 11:10:34 PDT 2024 +gradle.version=8.0 diff --git a/mobile/examples/phi-3-vision/android/.gradle/buildOutputCleanup/outputFiles.bin b/mobile/examples/phi-3-vision/android/.gradle/buildOutputCleanup/outputFiles.bin new file mode 100644 index 000000000..eb51c52b1 Binary files /dev/null and b/mobile/examples/phi-3-vision/android/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/mobile/examples/phi-3-vision/android/.gradle/config.properties b/mobile/examples/phi-3-vision/android/.gradle/config.properties new file mode 100644 index 000000000..0852258b7 --- /dev/null +++ b/mobile/examples/phi-3-vision/android/.gradle/config.properties @@ -0,0 +1,2 @@ +#Fri Jul 19 11:10:23 PDT 2024 +java.home=C\:\\Program Files\\Android\\Android Studio\\jbr diff --git a/mobile/examples/phi-3-vision/android/.gradle/file-system.probe b/mobile/examples/phi-3-vision/android/.gradle/file-system.probe new file mode 100644 index 000000000..cd68689a0 Binary files /dev/null and b/mobile/examples/phi-3-vision/android/.gradle/file-system.probe differ diff --git a/mobile/examples/phi-3-vision/android/.gradle/vcs-1/gc.properties b/mobile/examples/phi-3-vision/android/.gradle/vcs-1/gc.properties new file mode 100644 index 000000000..e69de29bb diff --git a/mobile/examples/phi-3-vision/android/app/libs/onnxruntime-genai-android-0.4.1-dev-old.aar b/mobile/examples/phi-3-vision/android/app/libs/onnxruntime-genai-android-0.4.1-dev-old.aar new file mode 100644 index 000000000..6c80a24e8 Binary files /dev/null and b/mobile/examples/phi-3-vision/android/app/libs/onnxruntime-genai-android-0.4.1-dev-old.aar differ diff --git a/mobile/examples/phi-3-vision/android/app/libs/onnxruntime-genai-android-0.4.1-dev.aar b/mobile/examples/phi-3-vision/android/app/libs/onnxruntime-genai-android-0.4.1-dev.aar new file mode 100644 index 000000000..f28ebfeed Binary files /dev/null and b/mobile/examples/phi-3-vision/android/app/libs/onnxruntime-genai-android-0.4.1-dev.aar differ diff --git a/mobile/examples/phi-3-vision/android/app/src/androidTest/java/ai/onnxruntime/genai/vision/demo/ExampleInstrumentedTest.java b/mobile/examples/phi-3-vision/android/app/src/androidTest/java/ai/onnxruntime/genai/vision/demo/ExampleInstrumentedTest.java new file mode 100644 index 000000000..0e9868637 --- /dev/null +++ b/mobile/examples/phi-3-vision/android/app/src/androidTest/java/ai/onnxruntime/genai/vision/demo/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package ai.onnxruntime.genai.vision.demo; + +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.onnxruntime.genai.vision.demo", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/mobile/examples/phi-3-vision/android/app/src/main/java/ai/onnxruntime/genai/vision/demo/GenAIImage.java b/mobile/examples/phi-3-vision/android/app/src/main/java/ai/onnxruntime/genai/vision/demo/GenAIImage.java new file mode 100644 index 000000000..0a6f5d7c8 --- /dev/null +++ b/mobile/examples/phi-3-vision/android/app/src/main/java/ai/onnxruntime/genai/vision/demo/GenAIImage.java @@ -0,0 +1,60 @@ +package ai.onnxruntime.genai.vision.demo; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +import ai.onnxruntime.genai.GenAIException; +import ai.onnxruntime.genai.Images; + +public class GenAIImage { + Images images = null; + Bitmap bitmap = null; + + GenAIImage(Context context, Uri uri, final int maxWidth, final int maxHeight) throws IOException, GenAIException { + Bitmap bmp = decodeUri(context, uri, maxWidth, maxHeight); + String filename = context.getFilesDir() + "/multimodalinput.png"; + FileOutputStream out = new FileOutputStream(filename); + bmp.compress(Bitmap.CompressFormat.PNG, 100, out); // bmp is your Bitmap instance + // PNG is a lossless format, the compression factor (100) is ignored + images = new Images(filename); + images = new Images(filename); + bitmap = BitmapFactory.decodeFile(filename); + } + + GenAIImage(Context context, Uri uri) throws IOException, GenAIException { + this(context, uri, 100000, 100000); + } + + public Images getImages() { + return images; + } + + public Bitmap getBitmap() { return bitmap; } + + private static Bitmap decodeUri(Context c, Uri uri, final int maxWidth, final int maxHeight) + throws FileNotFoundException { + BitmapFactory.Options o = new BitmapFactory.Options(); + o.inJustDecodeBounds = true; + BitmapFactory.decodeStream(c.getContentResolver().openInputStream(uri), null, o); + + int width_tmp = o.outWidth + , height_tmp = o.outHeight; + int scale = 1; + + while(width_tmp / 2 > maxWidth || height_tmp / 2 > maxHeight) { + width_tmp /= 2; + height_tmp /= 2; + scale *= 2; + } + + BitmapFactory.Options o2 = new BitmapFactory.Options(); + o2.inSampleSize = scale; + return BitmapFactory.decodeStream(c.getContentResolver().openInputStream(uri), null, o2); + } +} diff --git a/mobile/examples/phi-3-vision/android/app/src/main/java/ai/onnxruntime/genai/vision/demo/MainActivity.java b/mobile/examples/phi-3-vision/android/app/src/main/java/ai/onnxruntime/genai/vision/demo/MainActivity.java new file mode 100644 index 000000000..c71b35072 --- /dev/null +++ b/mobile/examples/phi-3-vision/android/app/src/main/java/ai/onnxruntime/genai/vision/demo/MainActivity.java @@ -0,0 +1,354 @@ +package ai.onnxruntime.genai.vision.demo; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.PickVisualMediaRequest; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AppCompatActivity; + +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.text.method.ScrollingMovementMethod; +import android.util.Log; +import android.util.Pair; +import android.view.View; +import android.view.WindowManager; +import android.webkit.MimeTypeMap; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.TextView; +import android.widget.Toast; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +import ai.onnxruntime.genai.GenAIException; +import ai.onnxruntime.genai.Generator; +import ai.onnxruntime.genai.GeneratorParams; +import ai.onnxruntime.genai.Images; +import ai.onnxruntime.genai.Model; +import ai.onnxruntime.genai.MultiModalProcessor; +import ai.onnxruntime.genai.NamedTensors; +import ai.onnxruntime.genai.Sequences; +import ai.onnxruntime.genai.SimpleGenAI; +import ai.onnxruntime.genai.Tokenizer; +import ai.onnxruntime.genai.TokenizerStream; +import ai.onnxruntime.genai.vision.demo.databinding.ActivityMainBinding; + +public class MainActivity extends AppCompatActivity implements Consumer { + + private ActivityMainBinding binding; + private EditText userMsgEdt; + private Model model; + //private Tokenizer tokenizer; + private MultiModalProcessor multiModalProcessor; + private ImageButton sendMsgIB; + private ImageButton selectPhotoIB; + private TextView generatedTV; + private TextView promptTV; + private TextView progressText; + private static final String TAG = "genai.demo.MainActivity"; + + private final int PICK_IMAGE_FILE = 2; + private GenAIImage inputImage = null; + + @Override + public void onActivityResult(int requestCode, int resultCode, + Intent resultData) { + if (requestCode == PICK_IMAGE_FILE) { + if (resultCode == RESULT_OK) { + // The result data contains a URI for the document or directory that + // the user selected. + inputImage = null; + if (resultData != null && resultData.getData() != null) { + Uri uri = resultData.getData(); + try { + inputImage = new GenAIImage(this, uri); + if (inputImage.getBitmap() != null) { + runOnUiThread(() -> { + selectPhotoIB.setImageBitmap(inputImage.getBitmap()); + }); + } + } catch (IOException | GenAIException e) { + throw new RuntimeException(e); + } + } + } + } + super.onActivityResult(requestCode, resultCode, resultData); + } + private static boolean fileExists(Context context, String fileName) { + File file = new File(context.getFilesDir(), fileName); + return file.exists(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + binding = ActivityMainBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + sendMsgIB = findViewById(R.id.idIBSend); + selectPhotoIB = findViewById(R.id.idIBPhoto); + userMsgEdt = findViewById(R.id.idEdtMessage); + generatedTV = findViewById(R.id.sample_text); + promptTV = findViewById(R.id.user_text); + progressText = findViewById(R.id.idProgressStatus); + + // Trigger the download operation when the application is created + try { + downloadModels( + getApplicationContext()); + } catch (GenAIException e) { + throw new RuntimeException(e); + } + + Consumer tokenListener = this; + + //enable scrolling and resizing of text boxes + generatedTV.setMovementMethod(new ScrollingMovementMethod()); + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + + selectPhotoIB.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + +// final int androidVersion = Build.VERSION.SDK_INT; +// if (androidVersion >= Build.VERSION_CODES.TIRAMISU) { +// ActivityResultLauncher pickMedia = +// registerForActivityResult(new ActivityResultContracts.PickVisualMedia(), uri -> { +// // Callback is invoked after the user selects a media item or closes the +// // photo picker. +// if (uri != null) { +// Log.d("PhotoPicker", "Selected URI: " + uri); +// } else { +// Log.d("PhotoPicker", "No media selected"); +// } +// }); +// pickMedia.launch(new PickVisualMediaRequest.Builder() +// .setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE) +// .build()); +// } else { + Intent chooseFile = new Intent(Intent.ACTION_GET_CONTENT); + chooseFile.addCategory(Intent.CATEGORY_OPENABLE); + chooseFile.setType("image/*"); + startActivityForResult( + Intent.createChooser(chooseFile, "Choose an image"), + PICK_IMAGE_FILE + ); + return; +// } + }}); + + // adding on click listener for send message button. + sendMsgIB.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (model == null) { + // if the edit text is empty display a toast message. + Toast.makeText(MainActivity.this, "Model not loaded yet, please wait...", Toast.LENGTH_SHORT).show(); + return; + } + + // Checking if the message entered + // by user is empty or not. + if (userMsgEdt.getText().toString().isEmpty()) { + // if the edit text is empty display a toast message. + Toast.makeText(MainActivity.this, "Please enter your message..", Toast.LENGTH_SHORT).show(); + return; + } + + String promptQuestion = "<|user|>\n"; + if (inputImage != null) { + promptQuestion += "<|image_1|>\n"; + } + promptQuestion += userMsgEdt.getText().toString() + "You are a helpful AI assistant. Answer in two paragraphs or less<|end|>\n<|assistant|>\n"; + final String promptQuestion_formatted = promptQuestion; + + Log.i("GenAI: prompt question", promptQuestion_formatted); + setVisibility(); + + // Disable send button while responding to prompt. + sendMsgIB.setEnabled(false); + + promptTV.setText(userMsgEdt.getText().toString()); + // Clear Edit Text or prompt question. + userMsgEdt.setText(""); + if (inputImage != null) { + generatedTV.setText("[analyzing image...]\n"); + } + else { + generatedTV.setText(""); + } + + new Thread(new Runnable() { + @Override + public void run() { + try { + TokenizerStream stream = multiModalProcessor.createStream(); + + GeneratorParams generatorParams = model.createGeneratorParams(); + //generatorParams.setSearchOption("length_penalty", 1000); + //generatorParams.setSearchOption("max_length", 500); + + Images images = null; + if (inputImage != null) { + images = inputImage.getImages(); + } + + + NamedTensors inputTensors = multiModalProcessor.processImages(promptQuestion_formatted, images); + generatorParams.setInput(inputTensors); + + Generator generator = new Generator(model, generatorParams); + + while (!generator.isDone()) { + generator.computeLogits(); + generator.generateNextToken(); + + int token = generator.getLastTokenInSequence(0); + + tokenListener.accept(stream.decode(token)); + } + + generator.close(); + } + catch (GenAIException e) { + throw new RuntimeException(e); + } + + runOnUiThread(() -> { + sendMsgIB.setEnabled(true); + }); + } + }).start(); + } + }); + } + + @Override + protected void onDestroy() { + multiModalProcessor.close(); + multiModalProcessor = null; + model.close(); + model = null; + super.onDestroy(); + } + + + private void downloadModels(Context context) throws GenAIException { + + final String baseUrl = "https://huggingface.co/microsoft/Phi-3-vision-128k-instruct-onnx-cpu/resolve/main/cpu-int4-rtn-block-32-acc-level-4/"; + List files = Arrays.asList( + "genai_config.json", + "phi-3-v-128k-instruct-text-embedding.onnx", + "phi-3-v-128k-instruct-text-embedding.onnx.data", + "phi-3-v-128k-instruct-text.onnx", + "phi-3-v-128k-instruct-text.onnx.data", + "phi-3-v-128k-instruct-vision.onnx", + "phi-3-v-128k-instruct-vision.onnx.data", + "processor_config.json", + "special_tokens_map.json", + "tokenizer.json", + "tokenizer_config.json"); + + + List> urlFilePairs = new ArrayList<>(); + for (String file : files) { + if (/*file.endsWith(".data") ||*/ !fileExists(context, file)) { + urlFilePairs.add(new Pair<>( + baseUrl + file,// + "?download=true", + file)); + } + } + if (urlFilePairs.isEmpty()) { + // Display a message using Toast + Toast.makeText(this, "All files already exist. Skipping download.", Toast.LENGTH_SHORT).show(); + Log.d(TAG, "All files already exist. Skipping download."); + model = new Model(getFilesDir().getPath()); + multiModalProcessor = new MultiModalProcessor(model); + return; + } + + progressText.setText("Downloading..."); + progressText.setVisibility(View.VISIBLE); + + Toast.makeText(this, + "Downloading model for the app... Model Size greater than 2GB, please allow a few minutes to download.", + Toast.LENGTH_SHORT).show(); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.execute(() -> { + ModelDownloader.downloadModel(context, urlFilePairs, new ModelDownloader.DownloadCallback() { + @Override + public void onProgress(long lastBytesRead, long bytesRead, long bytesTotal) { + long lastPctDone = 100 * lastBytesRead / bytesTotal; + long pctDone = 100 * bytesRead / bytesTotal; + if (pctDone > lastPctDone) { + Log.d(TAG, "Downloading files: " + pctDone + "%"); + //if (pctDone / 10 > lastPctDone / 10) { + runOnUiThread(() -> { + progressText.setText("Downloading: " + pctDone + "%"); + //Toast.makeText(context, "Downloading: " + pctDone + "%", Toast.LENGTH_SHORT).show(); + }); + //} + } + } + @Override + public void onDownloadComplete() { + Log.d(TAG, "All downloads completed."); + + // Last download completed, create SimpleGenAI + try { + model = new Model(getFilesDir().getPath()); + multiModalProcessor = new MultiModalProcessor(model); + runOnUiThread(() -> { + Toast.makeText(context, "All downloads completed", Toast.LENGTH_SHORT).show(); + progressText.setVisibility(View.INVISIBLE); + }); + } catch (GenAIException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + + } + }); + }); + executor.shutdown(); + } + + @Override + public void accept(String token) { + runOnUiThread(() -> { + // Update and aggregate the generated text and write to text box. + CharSequence generated = generatedTV.getText(); + generatedTV.setText(generated + token); + generatedTV.invalidate(); + final int scrollAmount = generatedTV.getLayout().getLineTop(generatedTV.getLineCount()) - generatedTV.getHeight(); + generatedTV.scrollTo(0, Math.max(scrollAmount, 0)); + }); + } + + public void setVisibility() { + TextView view = (TextView) findViewById(R.id.user_text); + view.setVisibility(View.VISIBLE); + TextView botView = (TextView) findViewById(R.id.sample_text); + botView.setVisibility(View.VISIBLE); + } +} diff --git a/mobile/examples/phi-3-vision/android/app/src/main/java/ai/onnxruntime/genai/vision/demo/ModelDownloader.java b/mobile/examples/phi-3-vision/android/app/src/main/java/ai/onnxruntime/genai/vision/demo/ModelDownloader.java new file mode 100644 index 000000000..3d544a69d --- /dev/null +++ b/mobile/examples/phi-3-vision/android/app/src/main/java/ai/onnxruntime/genai/vision/demo/ModelDownloader.java @@ -0,0 +1,104 @@ +package ai.onnxruntime.genai.vision.demo; + +import static androidx.constraintlayout.helper.widget.MotionEffect.TAG; + +import android.content.Context; +import android.util.Log; +import android.util.Pair; +import android.widget.Toast; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import ai.onnxruntime.genai.GenAIException; + +public class ModelDownloader { + interface DownloadCallback { + void onProgress(long lastBytesRead, long bytesRead, long bytesTotal); + void onDownloadComplete() throws GenAIException; + } + + public static void downloadModel(Context context, List> urlFilePairs, DownloadCallback callback) { + try { + + List connections = new ArrayList<>(); + long totalDownloadBytes = 0; + for (int i = 0; i < urlFilePairs.size(); i++) { + String url = urlFilePairs.get(i).first; + URL modelUrl = new URL(url); + HttpURLConnection connection = (HttpURLConnection) modelUrl.openConnection(); + connections.add(connection); + long totalFileSize = connection.getHeaderFieldLong("Content-Length",-1); + totalDownloadBytes += totalFileSize; + } + + long totalBytesRead = 0; + for (int i = 0; i < urlFilePairs.size(); i++) { + String fileName = urlFilePairs.get(i).second; + HttpURLConnection connection = connections.get(i); + + File file = new File(context.getFilesDir(), fileName); + File tempFile = new File(context.getFilesDir(), fileName + ".tmp"); + Log.d(TAG, "Downloading file: " + fileName); + connection.connect(); + + // Check if response code is OK + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + InputStream inputStream = connection.getInputStream(); + FileOutputStream outputStream = new FileOutputStream(tempFile); + + long begin = System.currentTimeMillis(); + + byte[] buffer = new byte[4096]; + int bytesRead; + + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + if (callback != null) { + callback.onProgress(totalBytesRead, totalBytesRead + bytesRead, totalDownloadBytes); + } + totalBytesRead += bytesRead; + } + + outputStream.flush(); + outputStream.close(); + inputStream.close(); + connection.disconnect(); + + long duration = System.currentTimeMillis() - begin; + + // File downloaded successfully + if (tempFile.renameTo(file)) { + if (duration > 0) { + Log.d(TAG, "File downloaded successfully: " + fileName + "(" + totalBytesRead + " bytes, " + (totalBytesRead / duration) + "KBps)"); + } else { + Log.d(TAG, "File downloaded successfully: " + fileName + "(" + totalBytesRead + " bytes, " + (duration / 1000.0) + "s)"); + } + } else { + Log.e(TAG, "Failed to rename temp file to original file"); + } + } else { + Log.e(TAG, "Failed to download model. HTTP response code: " + connection.getResponseCode()); + } + } + if (callback != null) { + callback.onDownloadComplete(); + } + } catch (IOException e) { + e.printStackTrace(); + Log.e(TAG, "Exception occurred during model download: " + e.getMessage()); + } catch (GenAIException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/mobile/examples/phi-3-vision/android/app/src/test/java/ai/onnxruntime/genai/vision/demo/README.md b/mobile/examples/phi-3-vision/android/app/src/test/java/ai/onnxruntime/genai/vision/demo/README.md new file mode 100644 index 000000000..2d75f43a3 --- /dev/null +++ b/mobile/examples/phi-3-vision/android/app/src/test/java/ai/onnxruntime/genai/vision/demo/README.md @@ -0,0 +1,5 @@ +**Note**: + +Note that we are not implementing any unit tests here as there's no simple unit tests that can be done without having a model locally. + +Debugging/Testing should be done via running the app on the simulator or actual android device. \ No newline at end of file diff --git a/mobile/examples/phi-3-vision/android/local.properties b/mobile/examples/phi-3-vision/android/local.properties new file mode 100644 index 000000000..50b82dc35 --- /dev/null +++ b/mobile/examples/phi-3-vision/android/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Fri Jul 26 10:25:51 PDT 2024 +sdk.dir=C\:\\Users\\t-boskovicf\\AppData\\Local\\Android\\Sdk diff --git a/mobile/examples/phi-3/android/README.md b/mobile/examples/phi-3/android/README.md index 129a6d961..e6145bb81 100644 --- a/mobile/examples/phi-3/android/README.md +++ b/mobile/examples/phi-3/android/README.md @@ -27,7 +27,7 @@ Clone this repository to get the sample application. The current set up supports downloading Phi-3-mini model directly from Huggingface repo to the android device folder. However, it takes time since the model data is >2.5G. -You can also follow this link to download **Phi-3-mini**: https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/tree/main/cpu_and_mobile/cpu-int4-rtn-block-32-acc-level-4 +You can also download [**Phi-3-mini**](https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/tree/main/cpu_and_mobile/cpu-int4-rtn-block-32-acc-level-4) and manually copy to the android device file directory following the below instructions: #### Steps for manual copying models to android device directory: @@ -40,7 +40,7 @@ From Android Studio: - Open Device Explorer in Android Studio - Navigate to `/data/data/ai.onnxruntime.genai.demo/files` - adjust as needed if the value returned by getFilesDir() differs for your emulator or device - - copy the whole [phi-3](https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/tree/main/cpu_and_mobile/cpu-int4-rtn-block-32-acc-level-4) model folder to the `files` directory + - copy the whole [Phi-3](https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/tree/main/cpu_and_mobile/cpu-int4-rtn-block-32-acc-level-4) model folder to the `files` directory ### Step 3: Connect Android Device and Run the app Connect your Android Device to your computer or select the Android Emulator in Android Studio Device manager. diff --git a/mobile/examples/phi-3/android/app/build.gradle.kts b/mobile/examples/phi-3/android/app/build.gradle.kts index e8d3478a5..10e53b580 100644 --- a/mobile/examples/phi-3/android/app/build.gradle.kts +++ b/mobile/examples/phi-3/android/app/build.gradle.kts @@ -17,7 +17,7 @@ android { ndk { //noinspection ChromeOsAbiSupport - abiFilters += listOf("arm64-v8a") + abiFilters += listOf("arm64-v8a", "x86_64") } } diff --git a/mobile/examples/phi-3/android/app/src/main/java/ai/onnxruntime/genai/demo/MainActivity.java b/mobile/examples/phi-3/android/app/src/main/java/ai/onnxruntime/genai/demo/MainActivity.java index 49bb93e7f..c43722149 100644 --- a/mobile/examples/phi-3/android/app/src/main/java/ai/onnxruntime/genai/demo/MainActivity.java +++ b/mobile/examples/phi-3/android/app/src/main/java/ai/onnxruntime/genai/demo/MainActivity.java @@ -40,6 +40,7 @@ public class MainActivity extends AppCompatActivity implements Consumer private ImageButton sendMsgIB; private TextView generatedTV; private TextView promptTV; + private TextView progressText; private static final String TAG = "genai.demo.MainActivity"; private static boolean fileExists(Context context, String fileName) { @@ -92,7 +93,7 @@ public void onClick(View v) { } String promptQuestion = userMsgEdt.getText().toString(); - String promptQuestion_formatted = "You are a helpful AI assistant. Answer in one paragraph or less<|end|><|user|>"+promptQuestion+"<|end|>\n"; + String promptQuestion_formatted = "You are a helpful AI assistant. Answer in two paragraphs or less<|end|><|user|>"+promptQuestion+"<|end|>\n"; Log.i("GenAI: prompt question", promptQuestion_formatted); setVisibility(); @@ -107,12 +108,15 @@ public void onClick(View v) { new Thread(new Runnable() { @Override public void run() { - try { + try (Outstream stream = new stream; + Outsteam generatorParams; + Outstream generator; + ) { TokenizerStream stream = tokenizer.createStream(); GeneratorParams generatorParams = model.createGeneratorParams(); //generatorParams.setSearchOption("length_penalty", 1000); - generatorParams.setSearchOption("max_length", 500); + //generatorParams.setSearchOption("max_length", 500); Sequences encodedPrompt = tokenizer.encode(promptQuestion_formatted); generatorParams.setInput(encodedPrompt); @@ -128,7 +132,9 @@ public void run() { tokenListener.accept(stream.decode(token)); } - //generator.close(); + generator.close(); + generatorParams.close(); + } catch (GenAIException e) { throw new RuntimeException(e); @@ -153,89 +159,81 @@ protected void onDestroy() { } private void downloadModels(Context context) throws GenAIException { - List> urlFilePairs = Arrays.asList( - new Pair<>( - "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/resolve/main/cpu_and_mobile/cpu-int4-rtn-block-32-acc-level-4/added_tokens.json?download=true", - "added_tokens.json"), - new Pair<>( - "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/resolve/main/cpu_and_mobile/cpu-int4-rtn-block-32-acc-level-4/config.json?download=true", - "config.json"), - new Pair<>( - "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/resolve/main/cpu_and_mobile/cpu-int4-rtn-block-32-acc-level-4/configuration_phi3.py?download=true", - "configuration_phi3.py"), - new Pair<>( - "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/resolve/main/cpu_and_mobile/cpu-int4-rtn-block-32-acc-level-4/genai_config.json?download=true", - "genai_config.json"), - new Pair<>( - "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/resolve/main/cpu_and_mobile/cpu-int4-rtn-block-32-acc-level-4/phi3-mini-4k-instruct-cpu-int4-rtn-block-32-acc-level-4.onnx?download=true", - "phi3-mini-4k-instruct-cpu-int4-rtn-block-32-acc-level-4.onnx"), - new Pair<>( - "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/resolve/main/cpu_and_mobile/cpu-int4-rtn-block-32-acc-level-4/phi3-mini-4k-instruct-cpu-int4-rtn-block-32-acc-level-4.onnx.data?download=true", - "phi3-mini-4k-instruct-cpu-int4-rtn-block-32-acc-level-4.onnx.data"), - new Pair<>( - "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/resolve/main/cpu_and_mobile/cpu-int4-rtn-block-32-acc-level-4/special_tokens_map.json?download=true", - "special_tokens_map.json"), - new Pair<>( - "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/resolve/main/cpu_and_mobile/cpu-int4-rtn-block-32-acc-level-4/tokenizer.json?download=true", - "tokenizer.json"), - new Pair<>( - "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/resolve/main/cpu_and_mobile/cpu-int4-rtn-block-32-acc-level-4/tokenizer.model?download=true", - "tokenizer.model"), - new Pair<>( - "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/resolve/main/cpu_and_mobile/cpu-int4-rtn-block-32-acc-level-4/tokenizer_config.json?download=true", - "tokenizer_config.json")); + + final String baseUrl = "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/resolve/main/cpu_and_mobile/cpu-int4-rtn-block-32-acc-level-4/"; + List files = Arrays.asList( + "added_tokens.json", + "config.json", + "configuration_phi3.py", + "genai_config.json", + "phi3-mini-4k-instruct-cpu-int4-rtn-block-32-acc-level-4.onnx", + "phi3-mini-4k-instruct-cpu-int4-rtn-block-32-acc-level-4.onnx.data", + "special_tokens_map.json", + "tokenizer.json", + "tokenizer.model", + "tokenizer_config.json"); + + List> urlFilePairs = new ArrayList<>(); + for (String file : files) { + if (/*file.endsWith(".data") ||*/ !fileExists(context, file)) { + urlFilePairs.add(new Pair<>( + baseUrl + file,// + "?download=true", + file)); + } + } + if (urlFilePairs.isEmpty()) { + // Display a message using Toast + Toast.makeText(this, "All files already exist. Skipping download.", Toast.LENGTH_SHORT).show(); + Log.d(TAG, "All files already exist. Skipping download."); + model = new Model(getFilesDir().getPath()); + tokenizer = model.createTokenizer(); + return; + } + + progressText.setText("Downloading..."); + progressText.setVisibility(View.VISIBLE); + Toast.makeText(this, "Downloading model for the app... Model Size greater than 2GB, please allow a few minutes to download.", Toast.LENGTH_SHORT).show(); ExecutorService executor = Executors.newSingleThreadExecutor(); - for (int i = 0; i < urlFilePairs.size(); i++) { - final int index = i; - String url = urlFilePairs.get(index).first; - String fileName = urlFilePairs.get(index).second; - if (fileExists(context, fileName)) { - // Display a message using Toast - Toast.makeText(this, "File already exists. Skipping Download.", Toast.LENGTH_SHORT).show(); - - Log.d(TAG, "File " + fileName + " already exists. Skipping download."); - // note: since we always download the files lists together for once, - // so assuming if one filename exists, then the download model step has already - // be - // done. - if (index == urlFilePairs.size() - 1) { - model = new Model(getFilesDir().getPath()); - tokenizer = model.createTokenizer(); - break; - } - continue; - } - executor.execute(() -> { - ModelDownloader.downloadModel(context, url, fileName, new ModelDownloader.DownloadCallback() { - private long pctDone = 0; - @Override - public void onDownloadProgress(long bytesDone, long bytesTotal) { - if (bytesTotal > 0) { - long newPctDone = bytesDone * 100 / bytesTotal; - if (newPctDone > pctDone) { - pctDone = newPctDone; - Log.d(TAG, "Download" + fileName + ": " + pctDone - + "% of " + (bytesTotal/1024) + " KB"); - } - } + executor.execute(() -> { + ModelDownloader.downloadModel(context, urlFilePairs, new ModelDownloader.DownloadCallback() { + @Override + public void onProgress(long lastBytesRead, long bytesRead, long bytesTotal) { + long lastPctDone = 100 * lastBytesRead / bytesTotal; + long pctDone = 100 * bytesRead / bytesTotal; + if (pctDone > lastPctDone) { + Log.d(TAG, "Downloading files: " + pctDone + "%"); + //if (pctDone / 10 > lastPctDone / 10) { + runOnUiThread(() -> { + progressText.setText("Downloading: " + pctDone + "%"); + //Toast.makeText(context, "Downloading: " + pctDone + "%", Toast.LENGTH_SHORT).show(); + }); + //} } - @Override - public void onDownloadComplete() throws GenAIException { - Log.d(TAG, "Download complete for " + fileName); - if (index == urlFilePairs.size() - 1) { - // Last download completed, create GenAIWrapper - model = new Model(getFilesDir().getPath()); - tokenizer = model.createTokenizer(); - Log.d(TAG, "All downloads completed"); - } + } + @Override + public void onDownloadComplete() { + Log.d(TAG, "All downloads completed."); + + // Last download completed, create SimpleGenAI + try { + model = new Model(getFilesDir().getPath()); + tokenizer = model.createTokenizer(); + runOnUiThread(() -> { + Toast.makeText(context, "All downloads completed", Toast.LENGTH_SHORT).show(); + progressText.setVisibility(View.INVISIBLE); + }); + } catch (GenAIException e) { + e.printStackTrace(); + throw new RuntimeException(e); } - }); + + } }); - } + }); executor.shutdown(); } diff --git a/mobile/examples/phi-3/android/app/src/main/java/ai/onnxruntime/genai/demo/ModelDownloader.java b/mobile/examples/phi-3/android/app/src/main/java/ai/onnxruntime/genai/demo/ModelDownloader.java index c41f7eb57..120560eb6 100644 --- a/mobile/examples/phi-3/android/app/src/main/java/ai/onnxruntime/genai/demo/ModelDownloader.java +++ b/mobile/examples/phi-3/android/app/src/main/java/ai/onnxruntime/genai/demo/ModelDownloader.java @@ -4,6 +4,9 @@ import android.content.Context; import android.util.Log; +import android.util.Pair; +import android.widget.Toast; + import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; @@ -14,61 +17,88 @@ import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; +import java.util.ArrayList; +import java.util.List; import ai.onnxruntime.genai.GenAIException; public class ModelDownloader { interface DownloadCallback { - void onDownloadProgress(long doneBytes, long totalBytes); + void onProgress(long lastBytesRead, long bytesRead, long bytesTotal); void onDownloadComplete() throws GenAIException; } - public static void downloadModel(Context context, String url, String fileName, DownloadCallback callback) { + public static void downloadModel(Context context, List> urlFilePairs, DownloadCallback callback) { try { - File file = new File(context.getFilesDir(), fileName); - File tempFile = new File(context.getFilesDir(), fileName + ".tmp"); - URL modelUrl = new URL(url); - HttpURLConnection connection = (HttpURLConnection) modelUrl.openConnection(); - connection.connect(); - - // Check if response code is OK - if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { - InputStream inputStream = connection.getInputStream(); - FileOutputStream outputStream = new FileOutputStream(tempFile); - long totalFileSize = connection.getHeaderFieldLong("Content-Length", -1); - - byte[] buffer = new byte[4096]; - long totalBytesRead = 0; - int bytesRead; - while ((bytesRead = inputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, bytesRead); - if (callback != null) { + + List connections = new ArrayList<>(); + long totalDownloadBytes = 0; + for (int i = 0; i < urlFilePairs.size(); i++) { + String url = urlFilePairs.get(i).first; + URL modelUrl = new URL(url); + HttpURLConnection connection = (HttpURLConnection) modelUrl.openConnection(); + connections.add(connection); + long totalFileSize = connection.getHeaderFieldLong("Content-Length",-1); + totalDownloadBytes += totalFileSize; + } + + long totalBytesRead = 0; + for (int i = 0; i < urlFilePairs.size(); i++) { + String fileName = urlFilePairs.get(i).second; + HttpURLConnection connection = connections.get(i); + + File file = new File(context.getFilesDir(), fileName); + File tempFile = new File(context.getFilesDir(), fileName + ".tmp"); + Log.d(TAG, "Downloading file: " + fileName); + connection.connect(); + + // Check if response code is OK + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + InputStream inputStream = connection.getInputStream(); + FileOutputStream outputStream = new FileOutputStream(tempFile); + + long begin = System.currentTimeMillis(); + + byte[] buffer = new byte[4096]; + int bytesRead; + + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + if (callback != null) { + callback.onProgress(totalBytesRead, totalBytesRead + bytesRead, totalDownloadBytes); + } totalBytesRead += bytesRead; - callback.onDownloadProgress(totalBytesRead, totalFileSize); } - } - outputStream.flush(); - outputStream.close(); - inputStream.close(); + outputStream.flush(); + outputStream.close(); + inputStream.close(); + connection.disconnect(); + + long duration = System.currentTimeMillis() - begin; - // File downloaded successfully - if (tempFile.renameTo(file)) { - Log.d(TAG, "File downloaded successfully"); - if (callback != null) { - callback.onDownloadComplete(); + // File downloaded successfully + if (tempFile.renameTo(file)) { + if (duration > 0) { + Log.d(TAG, "File downloaded successfully: " + fileName + "(" + totalBytesRead + " bytes, " + (totalBytesRead / duration) + "KBps)"); + } else { + Log.d(TAG, "File downloaded successfully: " + fileName + "(" + totalBytesRead + " bytes, " + (duration / 1000.0) + "s)"); + } + } else { + Log.e(TAG, "Failed to rename temp file to original file"); } } else { - Log.e(TAG, "Failed to rename temp file to original file"); + Log.e(TAG, "Failed to download model. HTTP response code: " + connection.getResponseCode()); } - } else { - Log.e(TAG, "Failed to download model. HTTP response code: " + connection.getResponseCode()); + } + if (callback != null) { + callback.onDownloadComplete(); } } catch (IOException e) { e.printStackTrace(); Log.e(TAG, "Exception occurred during model download: " + e.getMessage()); } catch (GenAIException e) { - throw new RuntimeException(e); + throw new RuntimeException(e); } } } \ No newline at end of file diff --git a/mobile/examples/phi-3/android/app/src/main/jniLibs/arm64-v8a/libonnxruntime-genai.so b/mobile/examples/phi-3/android/app/src/main/jniLibs/arm64-v8a/libonnxruntime-genai.so new file mode 100644 index 000000000..f5e6e6e05 Binary files /dev/null and b/mobile/examples/phi-3/android/app/src/main/jniLibs/arm64-v8a/libonnxruntime-genai.so differ diff --git a/mobile/examples/phi-3/android/app/src/main/jniLibs/arm64-v8a/libonnxruntime.so b/mobile/examples/phi-3/android/app/src/main/jniLibs/arm64-v8a/libonnxruntime.so new file mode 100644 index 000000000..c14976892 Binary files /dev/null and b/mobile/examples/phi-3/android/app/src/main/jniLibs/arm64-v8a/libonnxruntime.so differ diff --git a/mobile/examples/phi-3/android/app/src/main/jniLibs/arm64-v8a/libonnxruntime4j_jni.so b/mobile/examples/phi-3/android/app/src/main/jniLibs/arm64-v8a/libonnxruntime4j_jni.so new file mode 100644 index 000000000..f75540a19 Binary files /dev/null and b/mobile/examples/phi-3/android/app/src/main/jniLibs/arm64-v8a/libonnxruntime4j_jni.so differ diff --git a/mobile/examples/phi-3/android/app/src/main/jniLibs/x86_64/libonnxruntime-genai.so b/mobile/examples/phi-3/android/app/src/main/jniLibs/x86_64/libonnxruntime-genai.so new file mode 100644 index 000000000..f986355ab Binary files /dev/null and b/mobile/examples/phi-3/android/app/src/main/jniLibs/x86_64/libonnxruntime-genai.so differ diff --git a/mobile/examples/phi-3/android/app/src/main/jniLibs/x86_64/libonnxruntime.so b/mobile/examples/phi-3/android/app/src/main/jniLibs/x86_64/libonnxruntime.so new file mode 100644 index 000000000..c14976892 Binary files /dev/null and b/mobile/examples/phi-3/android/app/src/main/jniLibs/x86_64/libonnxruntime.so differ diff --git a/mobile/examples/phi-3/android/app/src/main/jniLibs/x86_64/libonnxruntime4j_jni.so b/mobile/examples/phi-3/android/app/src/main/jniLibs/x86_64/libonnxruntime4j_jni.so new file mode 100644 index 000000000..f75540a19 Binary files /dev/null and b/mobile/examples/phi-3/android/app/src/main/jniLibs/x86_64/libonnxruntime4j_jni.so differ diff --git a/mobile/examples/phi-3/android/app/src/main/res/drawable/rounded_corner2.xml b/mobile/examples/phi-3/android/app/src/main/res/drawable/rounded_corner2.xml index de9870ee9..913738d1d 100644 --- a/mobile/examples/phi-3/android/app/src/main/res/drawable/rounded_corner2.xml +++ b/mobile/examples/phi-3/android/app/src/main/res/drawable/rounded_corner2.xml @@ -1,7 +1,7 @@ - + @color/blue_700 @color/black - @color/teal_200 - @color/teal_200 + @color/purple_200 + @color/purple_200 @color/black ?attr/colorPrimaryVariant diff --git a/mobile/examples/phi-3/android/app/src/main/res/values/colors.xml b/mobile/examples/phi-3/android/app/src/main/res/values/colors.xml index 8a660986c..31b1630c0 100644 --- a/mobile/examples/phi-3/android/app/src/main/res/values/colors.xml +++ b/mobile/examples/phi-3/android/app/src/main/res/values/colors.xml @@ -3,8 +3,8 @@ #CC03A9F4 #CC03A9F4 #CC03A9F4 - #FF03DAC5 - #FF018786 + #a26cf3 + #7626ef #FF000000 #FFFFFFFF \ No newline at end of file diff --git a/mobile/examples/phi-3/android/app/src/main/res/values/themes.xml b/mobile/examples/phi-3/android/app/src/main/res/values/themes.xml index 53ebc75d6..5e976ac5e 100644 --- a/mobile/examples/phi-3/android/app/src/main/res/values/themes.xml +++ b/mobile/examples/phi-3/android/app/src/main/res/values/themes.xml @@ -6,8 +6,8 @@ @color/blue_700 @color/white - @color/teal_200 - @color/teal_700 + @color/purple_200 + @color/purple_700 @color/black ?attr/colorPrimaryVariant