diff --git a/lite/examples/generative_ai/android/README.md b/lite/examples/generative_ai/android/README.md
new file mode 100644
index 00000000000..116280eef87
--- /dev/null
+++ b/lite/examples/generative_ai/android/README.md
@@ -0,0 +1,131 @@
+# Generative AI
+
+## Introduction
+Large language models (LLMs) are types of machine learning models that are created based on large bodies of text data to generate various outputs for natural language processing (NLP) tasks, including text generation, question answering, and machine translation. They are based on Transformer architecture and are trained on massive amounts of text data, often involving billions of words. Even LLMs of a smaller scale, such as GPT-2, can perform impressively. Converting TensorFlow models to a lighter, faster, and low-power model allows for us to run generative AI models on-device, with benefits of better user security because data will never leave your device.
+
+ This example shows you how to build an Android app with TensorFlow Lite to run a Keras LLM and provides suggestions for model optimization using quantizing techniques, which otherwise would require a much larger amount of memory and greater computational power to run.
+
+This example open sourced an Android app framework that any compatible TFLite LLMs can plug into. Here are two demos:
+* In Figure 1, we used a Keras GPT-2 model to perform text completion tasks on device.
+* In Figure 2, we converted a version of instruction-tuned [PaLM model](https://ai.googleblog.com/2022/04/pathways-language-model-palm-scaling-to.html) (1.5 billion parameters) to TFLite and executed through TFLite runtime.
+
+
+
+
+Figure 1: Example of running the Keras GPT-2 model (converted from this Codelab) on device to perform text completion on Pixel 7. Demo shows the real latency with no speedup.
+
+
+
+Figure 2: Example of running a version of [PaLM model](https://ai.googleblog.com/2022/04/pathways-language-model-palm-scaling-to.html) with 1.5 billion parameters. Demo is recorded on Pixel 7 Pro without playback speedup.
+
+
+## Guides
+### Step 1. Train a language model using Keras
+
+For this demonstration, we will use KerasNLP to get the GPT-2 model. KerasNLP is a library that contains state-of-the-art pretrained models for natural language processing tasks, and can support users through their entire development cycle. You can see the list of models available in the [KerasNLP repository](https://github.com/keras-team/keras-nlp/tree/master/keras_nlp/models). The workflows are built from modular components that have state-of-the-art preset weights and architectures when used out-of-the-box and are easily customizable when more control is needed. Creating the GPT-2 model can be done with the following steps:
+
+```python
+gpt2_tokenizer = keras_nlp.models.GPT2Tokenizer.from_preset("gpt2_base_en")
+
+gpt2_preprocessor = keras_nlp.models.GPT2CausalLMPreprocessor.from_preset(
+ "gpt2_base_en",
+ sequence_length=256,
+ add_end_token=True,
+)
+
+gpt2_lm = keras_nlp.models.GPT2CausalLM.from_preset(
+ "gpt2_base_en",
+ preprocessor=gpt2_preprocessor,
+)
+```
+
+You can check out the full GPT-2 model implementation [on GitHub](https://github.com/keras-team/keras-nlp/tree/master/keras_nlp/models/gpt2).
+
+
+### Step 2. Convert a Keras model to a TFLite model
+
+Start with the `generate()` function from GPT2CausalLM that performs the conversion. Wrap the `generate()` function to create a concrete TensorFlow function:
+
+```python
+@tf.function
+def generate(prompt, max_length):
+ # prompt: input prompt to the LLM in string format
+ # max_length: the max length of the generated tokens
+ return gpt2_lm.generate(prompt, max_length)
+concrete_func = generate.get_concrete_function(tf.TensorSpec([], tf.string), 100)
+```
+
+Now define a helper function that will run inference with an input and a TFLite model. TensorFlow text ops are not built-in ops in the TFLite runtime, so you will need to add these custom ops in order for the interpreter to make inference on this model. This helper function accepts an input and a function that performs the conversion, namely the `generator()` function defined above.
+
+```python
+def run_inference(input, generate_tflite):
+ interp = interpreter.InterpreterWithCustomOps(
+ model_content=generate_tflite,
+ custom_op_registerers=tf_text.tflite_registrar.SELECT_TFTEXT_OPS)
+ interp.get_signature_list()
+
+ generator = interp.get_signature_runner('serving_default')
+ output = generator(prompt=np.array([input]))
+```
+
+You can convert the model now:
+
+```python
+gpt2_lm.jit_compile = False
+converter = tf.lite.TFLiteConverter.from_concrete_functions(
+ [concrete_func],
+ gpt2_lm)
+
+converter.target_spec.supported_ops = [
+ tf.lite.OpsSet.TFLITE_BUILTINS, # enable TFLite ops
+ tf.lite.OpsSet.SELECT_TF_OPS, # enable TF ops
+]
+converter.allow_custom_ops = True
+converter.target_spec.experimental_select_user_tf_ops = [
+ "UnsortedSegmentJoin",
+ "UpperBound"
+]
+converter._experimental_guarantee_all_funcs_one_use = True
+generate_tflite = converter.convert()
+run_inference("I'm enjoying a", generate_tflite)
+```
+
+### Step 3. Quantization
+TensorFlow Lite has implemented an optimization technique called quantization which can reduce model size and accelerate inference. Through the quantization process, 32-bit floats are mapped to smaller 8-bit integers, therefore reducing the model size by a factor of 4 for more efficient execution on modern hardwares. There are several ways to do quantization in TensorFlow. You can visit the [TFLite Model optimization](https://www.tensorflow.org/lite/performance/model_optimization) and [TensorFlow Model Optimization Toolkit](https://www.tensorflow.org/model_optimization) pages for more information. The types of quantizations are explained briefly below.
+
+Here, you will use the post-training dynamic range quantization on the GPT-2 model by setting the converter optimization flag to tf.lite.Optimize.DEFAULT, and the rest of the conversion process is the same as detailed before. We tested that with this quantization technique the latency is around 6.7 seconds on Pixel 7 with max output length set to 100.
+
+```python
+gpt2_lm.jit_compile = False
+converter = tf.lite.TFLiteConverter.from_concrete_functions(
+ [concrete_func],
+ gpt2_lm)
+
+converter.target_spec.supported_ops = [
+ tf.lite.OpsSet.TFLITE_BUILTINS, # enable TFLite ops
+ tf.lite.OpsSet.SELECT_TF_OPS, # enable TF ops
+]
+converter.allow_custom_ops = True
+converter.optimizations = [tf.lite.Optimize.DEFAULT]
+converter.target_spec.experimental_select_user_tf_ops = [
+ "UnsortedSegmentJoin",
+ "UpperBound"
+]
+converter._experimental_guarantee_all_funcs_one_use = True
+quant_generate_tflite = converter.convert()
+run_inference("I'm enjoying a", quant_generate_tflite)
+
+with open('quantized_gpt2.tflite', 'wb') as f:
+ f.write(quant_generate_tflite)
+```
+
+
+
+### Step 4. Android App integration
+
+You can clone this repo and substitute `android/app/src/main/assets/autocomplete.tflite` with your converted `quant_generate_tflite` file. Please refer to [how-to-build.md](https://github.com/tensorflow/examples/blob/master/lite/examples/generative_ai/android/how-to-build.md) to build this Android App.
+
+## Safety and Responsible AI
+As noted in the original [OpenAI GPT-2 announcement](https://openai.com/research/better-language-models), there are [notable caveats and limitations](https://github.com/openai/gpt-2#some-caveats) with the GPT-2 model. In fact, LLMs today generally have some well-known challenges such as hallucinations, fairness, and bias; this is because these models are trained on real-world data, which make them reflect real world issues.
+This codelab is created only to demonstrate how to create an app powered by LLMs with TensorFlow tooling. The model produced in this codelab is for educational purposes only and not intended for production usage.
+LLM production usage requires thoughtful selection of training datasets and comprehensive safety mitigations. One such functionality offered in this Android app is the profanity filter, which rejects bad user inputs or model outputs. If any inappropriate language is detected, the app will in return reject that action. To learn more about Responsible AI in the context of LLMs, make sure to watch the Safe and Responsible Development with Generative Language Models technical session at Google I/O 2023 and check out the [Responsible AI Toolkit](https://www.tensorflow.org/responsible_ai).
diff --git a/lite/examples/generative_ai/android/app/build.gradle.kts b/lite/examples/generative_ai/android/app/build.gradle.kts
new file mode 100644
index 00000000000..ed1a8880253
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/build.gradle.kts
@@ -0,0 +1,102 @@
+@file:Suppress("UnstableApiUsage")
+
+plugins {
+ kotlin("android")
+ id("com.android.application")
+ id("de.undercouch.download")
+}
+
+ext {
+ set("AAR_URL", "https://storage.googleapis.com/download.tensorflow.org/models/tflite/generativeai/tensorflow-lite-select-tf-ops.aar")
+ set("AAR_PATH", "$projectDir/libs/tensorflow-lite-select-tf-ops.aar")
+}
+
+apply {
+ from("download.gradle")
+}
+
+android {
+ namespace = "com.google.tensorflowdemo"
+ compileSdk = 33
+
+ defaultConfig {
+ applicationId = "com.google.tensorflowdemo"
+ minSdk = 24
+ targetSdk = 33
+ versionCode = 1
+ versionName = "1.0"
+ }
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ viewBinding = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.3.2"
+ }
+ packagingOptions {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = true
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ isDebuggable = false
+ }
+ getByName("debug") {
+ applicationIdSuffix = ".debug"
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ freeCompilerArgs = listOf(
+ "-P",
+ "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=1.8.10"
+ )
+ }
+}
+
+dependencies {
+ implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
+
+ // Compose
+ implementation(libraries.compose.ui)
+ implementation(libraries.compose.ui.tooling)
+ implementation(libraries.compose.ui.tooling.preview)
+ implementation(libraries.compose.foundation)
+ implementation(libraries.compose.material)
+ implementation(libraries.compose.material.icons)
+ implementation(libraries.compose.activity)
+
+ // Accompanist for Compose
+ implementation(libraries.accompanist.systemuicontroller)
+
+ // Koin
+ implementation(libraries.koin.core)
+ implementation(libraries.koin.android)
+ implementation(libraries.koin.compose)
+
+ // Lifecycle
+ implementation(libraries.lifecycle.viewmodel)
+ implementation(libraries.lifecycle.viewmodel.compose)
+ implementation(libraries.lifecycle.viewmodel.ktx)
+ implementation(libraries.lifecycle.runtime.compose)
+
+ // Logging
+ implementation(libraries.napier)
+
+ // Profanity filter
+ implementation(libraries.wordfilter)
+
+ // TensorFlow Lite
+ implementation(libraries.tflite)
+
+ // Unit tests
+ testImplementation(libraries.junit)
+}
diff --git a/lite/examples/generative_ai/android/app/download.gradle b/lite/examples/generative_ai/android/app/download.gradle
new file mode 100644
index 00000000000..8552a349054
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/download.gradle
@@ -0,0 +1,7 @@
+task downloadAAR {
+ download {
+ src project.ext.AAR_URL
+ dest project.ext.AAR_PATH
+ overwrite false
+ }
+}
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/libs/.gitignore b/lite/examples/generative_ai/android/app/libs/.gitignore
new file mode 100644
index 00000000000..c5db4891b20
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/libs/.gitignore
@@ -0,0 +1 @@
+tensorflow-lite-select-tf-ops.aar
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/libs/build_aar/README.md b/lite/examples/generative_ai/android/app/libs/build_aar/README.md
new file mode 100644
index 00000000000..199f1d392d9
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/libs/build_aar/README.md
@@ -0,0 +1,16 @@
+# Build your own aar
+
+By default the app automatically downloads the needed aar files. But if you want
+to build your own, just go ahead and run `./build_aar.sh`. This script will pull
+in the necessary ops from [TensorFlow Text](https://www.tensorflow.org/text) and
+build the aar for [Select TF operators](https://www.tensorflow.org/lite/guide/ops_select).
+
+After compilation, a new file `tftext_tflite_flex.aar` is generated. Replace the
+one in app/libs/ folder and re-build the app.
+
+By default, the script builds only for `android_x86_64`. You can change it to
+`android_x86`, `android_arm` or `android_arm64`.
+
+Note that you still need to include the standard `tensorflow-lite` aar in your
+gradle file.
+
diff --git a/lite/examples/generative_ai/android/app/libs/build_aar/build_aar.sh b/lite/examples/generative_ai/android/app/libs/build_aar/build_aar.sh
new file mode 100755
index 00000000000..53c08d571da
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/libs/build_aar/build_aar.sh
@@ -0,0 +1,28 @@
+#! /bin/bash
+
+set -e
+
+# Clone TensorFlow Text repo
+git clone https://github.com/tensorflow/text.git tensorflow_text
+
+cd tensorflow_text/
+echo 'exports_files(["LICENSE"])' > BUILD
+
+# Checkout 2.12 branch
+git checkout 2.12
+
+# Apply tftext-2.12.patch
+git apply ../tftext-2.12.patch
+
+# Run config
+./oss_scripts/configure.sh
+
+# Run bazel build
+bazel build -c opt --cxxopt='--std=c++14' --config=monolithic --config=android_x86_64 --experimental_repo_remote_exec //tensorflow_text:tftext_tflite_flex
+
+if [ $? -eq 0 ]; then
+ # Print a message
+ echo "Please find the aar file: tensorflow_text/bazel-bin/tensorflow_text/tftext_tflite_flex.aar"
+else
+ echo "build_aar.sh has failed. Please find the error message above and address it before proceeding."
+fi
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/libs/build_aar/tftext-2.12.patch b/lite/examples/generative_ai/android/app/libs/build_aar/tftext-2.12.patch
new file mode 100644
index 00000000000..e3edc1c2e47
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/libs/build_aar/tftext-2.12.patch
@@ -0,0 +1,61 @@
+diff --git a/WORKSPACE b/WORKSPACE
+index 28b7ee5..5ad0b55 100644
+--- a/WORKSPACE
++++ b/WORKSPACE
+@@ -116,3 +116,10 @@ load("@org_tensorflow//third_party/android:android_configure.bzl", "android_conf
+ android_configure(name="local_config_android")
+ load("@local_config_android//:android.bzl", "android_workspace")
+ android_workspace()
++
++android_sdk_repository(name = "androidsdk")
++
++android_ndk_repository(
++ name = "androidndk",
++ api_level = 21,
++)
+diff --git a/tensorflow_text/BUILD b/tensorflow_text/BUILD
+index 9b5ee5b..880c7c5 100644
+--- a/tensorflow_text/BUILD
++++ b/tensorflow_text/BUILD
+@@ -2,6 +2,8 @@ load("//tensorflow_text:tftext.bzl", "py_tf_text_library")
+
+ # [internal] load build_test.bzl
+ load("@org_tensorflow//tensorflow/lite:build_def.bzl", "tflite_cc_shared_object")
++load("@org_tensorflow//tensorflow/lite/delegates/flex:build_def.bzl", "tflite_flex_android_library")
++load("@org_tensorflow//tensorflow/lite/java:aar_with_jni.bzl", "aar_with_jni")
+
+ # Visibility rules
+ package(
+@@ -61,6 +63,20 @@ tflite_cc_shared_object(
+ deps = [":ops_lib"],
+ )
+
++tflite_flex_android_library(
++ name = "tftext_ops",
++ additional_deps = [
++ "@org_tensorflow//tensorflow/lite/delegates/flex:delegate",
++ ":ops_lib",
++ ],
++ visibility = ["//visibility:public"],
++)
++
++aar_with_jni(
++ name = "tftext_tflite_flex",
++ android_library = ":tftext_ops",
++)
++
+ py_library(
+ name = "ops",
+ srcs = [
+diff --git a/tensorflow_text/tftext.bzl b/tensorflow_text/tftext.bzl
+index 65430ca..04f68d8 100644
+--- a/tensorflow_text/tftext.bzl
++++ b/tensorflow_text/tftext.bzl
+@@ -140,6 +140,7 @@ def tf_cc_library(
+ deps += select({
+ "@org_tensorflow//tensorflow:mobile": [
+ "@org_tensorflow//tensorflow/core:portable_tensorflow_lib_lite",
++ "@org_tensorflow//tensorflow/lite/kernels/shim:tf_op_shim",
+ ],
+ "//conditions:default": [
+ "@local_config_tf//:libtensorflow_framework",
diff --git a/lite/examples/generative_ai/android/app/proguard-rules.pro b/lite/examples/generative_ai/android/app/proguard-rules.pro
new file mode 100644
index 00000000000..481bb434814
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/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/lite/examples/generative_ai/android/app/src/main/AndroidManifest.xml b/lite/examples/generative_ai/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..397bae6ecb1
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/ic_launcher-playstore.png b/lite/examples/generative_ai/android/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 00000000000..3a716721143
Binary files /dev/null and b/lite/examples/generative_ai/android/app/src/main/ic_launcher-playstore.png differ
diff --git a/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/DemoApplication.kt b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/DemoApplication.kt
new file mode 100644
index 00000000000..3be20d41944
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/DemoApplication.kt
@@ -0,0 +1,30 @@
+package com.google.tensorflowdemo
+
+import android.app.Application
+import com.google.tensorflowdemo.di.appModule
+import com.google.tensorflowdemo.di.viewmodelModule
+import io.github.aakira.napier.DebugAntilog
+import io.github.aakira.napier.Napier
+import org.koin.android.ext.koin.androidContext
+import org.koin.android.ext.koin.androidLogger
+import org.koin.core.context.startKoin
+
+class DemoApplication : Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+
+ if (BuildConfig.DEBUG) {
+ Napier.base(DebugAntilog())
+ }
+
+ startKoin {
+ androidLogger()
+ androidContext(this@DemoApplication)
+ modules(
+ appModule,
+ viewmodelModule
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/data/autocomplete/AutoCompleteService.kt b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/data/autocomplete/AutoCompleteService.kt
new file mode 100644
index 00000000000..68fec5c5c7d
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/data/autocomplete/AutoCompleteService.kt
@@ -0,0 +1,285 @@
+package com.google.tensorflowdemo.data.autocomplete
+
+import android.content.Context
+import androidx.annotation.WorkerThread
+import com.google.tensorflowdemo.data.autocomplete.AutoCompleteService.AutoCompleteInputConfiguration
+import com.google.tensorflowdemo.data.autocomplete.AutoCompleteService.AutoCompleteResult
+import com.google.tensorflowdemo.data.autocomplete.AutoCompleteService.AutoCompleteServiceError
+import com.google.tensorflowdemo.data.autocomplete.AutoCompleteService.InitModelResult
+import com.google.tensorflowdemo.util.splitToWords
+import com.google.tensorflowdemo.util.trimToMaxWordCount
+import com.mediamonks.wordfilter.LanguageChecker
+import io.github.aakira.napier.Napier
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.tensorflow.lite.Interpreter
+import java.io.FileInputStream
+import java.io.FileNotFoundException
+import java.nio.ByteBuffer
+import java.nio.MappedByteBuffer
+import java.nio.channels.FileChannel
+import kotlin.math.min
+
+interface AutoCompleteService {
+
+ /**
+ * Configuration of input for model
+ */
+ val inputConfiguration: AutoCompleteInputConfiguration
+
+ /**
+ * Boolean indicating whether the model has been initialized successfully
+ */
+ val isInitialized: Boolean
+
+ /**
+ * Initialize TensorFlow-Lite with app provided model
+ * @return [InitModelResult.Success] if TFLite was initialized properly, otherwise [InitModelResult.Error]
+ */
+ suspend fun initModel(): InitModelResult
+
+ /**
+ * Get autocomplete suggestion split into words for the provided [input].
+ * If [applyWindow] is true, the last [windowSize] words are taken from [input] and fed into the interpreter.
+ * @return an instance of [AutoCompleteResult.Error] if something went wrong, or
+ * an instance of [AutoCompleteResult.Success] with the suggested text, split into words
+ */
+ suspend fun getSuggestion(input: String, applyWindow: Boolean = false, windowSize: Int = 50): AutoCompleteResult
+
+ /**
+ * Possible errors from [AutoCompleteService]
+ */
+ enum class AutoCompleteServiceError {
+ MODEL_FILE_NOT_FOUND,
+ MODEL_NOT_INITIALIZED,
+ NO_SUGGESTIONS,
+ BAD_LANGUAGE,
+ }
+
+ /**
+ * Result from [AutoCompleteService.getSuggestion] method call
+ */
+ sealed interface AutoCompleteResult {
+ data class Success(val words: List) : AutoCompleteResult
+ data class Error(val error: AutoCompleteServiceError) : AutoCompleteResult
+ }
+
+ /**
+ * Result from [AutoCompleteService.initModel] method call
+ */
+ sealed interface InitModelResult {
+ object Success : InitModelResult
+ data class Error(val error: AutoCompleteServiceError) : InitModelResult
+ }
+
+ data class AutoCompleteInputConfiguration(
+ // Minimum number of words to be taken from the end of the input text
+ val minWordCount: Int = 5,
+ // Maximum number of words to be taken from the end of the input text
+ val maxWordCount: Int = 50,
+ // Initially selected value for number of words to be taken from the end of the input text
+ val initialWordCount: Int = 20,
+ )
+}
+
+class AutoCompleteServiceImpl(
+ private val context: Context,
+ private val languageChecker: LanguageChecker,
+ private val dispatcher: CoroutineDispatcher = Dispatchers.IO
+) : AutoCompleteService, AutoCloseable {
+
+ /**
+ * The values below are used to configure the slider that allows a user to select the number of words to take from the current text
+ * and use as input for the model to generate new text from.
+ */
+ override val inputConfiguration = AutoCompleteInputConfiguration(
+ // Minimum number of words to be taken from the end of the input text
+ minWordCount = 5,
+ // Maximum number of words to be taken from the end of the input text, limited by what the model allows
+ maxWordCount = min(50, MAX_INPUT_WORD_COUNT),
+ // Initially selected value for number of words to be taken from the end of the input text
+ initialWordCount = 20
+ )
+
+ override var isInitialized: Boolean = false
+ private set
+
+ private lateinit var interpreter: Interpreter
+ private val outputBuffer = ByteBuffer.allocateDirect(OUTPUT_BUFFER_SIZE)
+
+
+ /**
+ * Initialize TensorFlow Lite with app provided model
+ * @return [InitModelResult.Success] if TFLite was initialized properly, otherwise [InitModelResult.Error]
+ */
+ override suspend fun initModel(): InitModelResult {
+ return withContext(dispatcher) {
+ // Load model file
+ val loadResult = loadModelFile(context)
+
+ // Determine if load was successful
+ if (loadResult.isFailure) {
+ val exc = loadResult.exceptionOrNull()
+ return@withContext if (exc is FileNotFoundException) {
+ InitModelResult.Error(AutoCompleteServiceError.MODEL_FILE_NOT_FOUND)
+ } else {
+ InitModelResult.Error(AutoCompleteServiceError.MODEL_NOT_INITIALIZED)
+ }
+ }
+
+ // Instantiate interpreter with loaded model
+ val model = loadResult.getOrNull()
+ isInitialized = model?.let {
+ interpreter = Interpreter(it)
+ true
+ } ?: false
+
+ if (isInitialized) InitModelResult.Success
+ else InitModelResult.Error(AutoCompleteServiceError.MODEL_NOT_INITIALIZED)
+ }
+ }
+
+ /**
+ * Get autocomplete suggestion split into words for the provided [input].
+ * If [applyWindow] is true, the last [windowSize] words are taken from [input] and fed into the interpreter.
+ * @return an instance of [AutoCompleteResult.Error] if something went wrong, or
+ * an instance of [AutoCompleteResult.Success] with the suggested text, split into words
+ */
+ override suspend fun getSuggestion(input: String, applyWindow: Boolean, windowSize: Int) = withContext(dispatcher) {
+ Napier.d { "[0] Start interpretation" }
+ Napier.d { "[1] Input text: (${input.length} chars) '$input'" }
+
+ // Check input for bad language
+ if (languageChecker.containsBadLanguage(input)) {
+ Napier.w { "[2] Input contains bad language, refused!" }
+ return@withContext AutoCompleteResult.Error(AutoCompleteServiceError.BAD_LANGUAGE)
+ }
+
+ // Initialize interpreter if necessary
+ if (!::interpreter.isInitialized) {
+ val result = initModel()
+ if (result is InitModelResult.Error) {
+ return@withContext AutoCompleteResult.Error(result.error)
+ }
+ }
+
+ // Determine maximum number of words to take from input as model input
+ val maxInputWordCount = if (applyWindow) windowSize else MAX_INPUT_WORD_COUNT
+ Napier.d { "[2] Trimming input to max $maxInputWordCount words" }
+
+ // Trim input text to maximum number of words
+ val trimmedInput = input.trimToMaxWordCount(maxInputWordCount)
+ Napier.d { "[3] Model input: (${trimmedInput.length} chars) '$trimmedInput'" }
+
+ var retryCount = 0
+ var containsBadLanguage: Boolean
+ lateinit var output: String
+
+ // Run generation until it no longer contains bad language or a max number of tries has been exceeded
+ do {
+ // Let model generate new text based on windowed input
+ output = runInterpreterOn(trimmedInput)
+
+ // Check output for bad language
+ containsBadLanguage = languageChecker.containsBadLanguage(output)
+
+ retryCount++
+ } while (containsBadLanguage && retryCount < RETRY_COUNT_ON_BAD_LANGUAGE)
+
+ // Return error if output still contains bad language
+ if (containsBadLanguage) {
+ Napier.w { "[4] Output still contains bad language after 3 attempts, refused!" }
+
+ return@withContext AutoCompleteResult.Error(AutoCompleteServiceError.NO_SUGGESTIONS)
+ }
+
+ Napier.d { "[4] Model output: (${output.length} chars) '$output'" }
+
+ // Check if output size is actually longer than original input text, if not that's an error
+ if (output.length < trimmedInput.length) {
+ Napier.w { "[5] NO SUGGESTION: Output length is shorter than trimmed input length, so there was no new text suggested" }
+ AutoCompleteResult.Error(AutoCompleteServiceError.NO_SUGGESTIONS)
+ } else {
+ // Output = input + new text, determine new text by subtracting input
+ val newText = output.substring(output.indexOf(trimmedInput) + trimmedInput.length)
+ Napier.d { "[5] New text from interpreter: (${newText.length} chars) '$newText'" }
+
+ // Split new text into words. If there are no words, that's an error, otherwise return the words
+ val words = newText.splitToWords()
+ if (words.isEmpty()) {
+ Napier.w { "[6] NO SUGGESTION: No words found after splitting new text into words" }
+ AutoCompleteResult.Error(AutoCompleteServiceError.NO_SUGGESTIONS)
+ } else {
+ Napier.d { "[6] New text split in words: (${words.size} words) [${words.joinToString()}]" }
+ AutoCompleteResult.Success(words)
+ }
+ }
+ }
+
+ /**
+ * Run the previously created [interpreter] on the provided input, which will return with appended generated text
+ * Note that this method may take quite some time to finish, so call this from a background thread
+ */
+ @WorkerThread
+ private fun runInterpreterOn(input: String): String {
+ outputBuffer.clear()
+
+ // Run interpreter, which will generate text into outputBuffer
+ interpreter.run(input, outputBuffer)
+
+ // Set output buffer limit to current position & position to 0
+ outputBuffer.flip()
+
+ // Get bytes from output buffer
+ val bytes = ByteArray(outputBuffer.remaining())
+ outputBuffer.get(bytes)
+
+ outputBuffer.clear()
+
+ // Return bytes converted to String
+ return String(bytes, Charsets.UTF_8)
+ }
+
+ /**
+ * Load TF Lite model file into memory.
+ * The model file is expected in the `src/main/assets` folder, with name configured in [TFLITE_MODEL]
+ */
+ private fun loadModelFile(context: Context): Result {
+ try {
+ val descriptor = context.assets.openFd(TFLITE_MODEL)
+
+ FileInputStream(descriptor.fileDescriptor).use { stream ->
+ return Result.success(
+ stream.channel.map(
+ /* mode = */ FileChannel.MapMode.READ_ONLY,
+ /* position = */ descriptor.startOffset,
+ /* size = */ descriptor.declaredLength
+ )
+ )
+ }
+ } catch (e: Exception) {
+ Napier.e { "Failed to load model: ${e.localizedMessage}" }
+
+ return Result.failure(e)
+ }
+ }
+
+ override fun close() {
+ interpreter.close()
+ }
+
+ companion object {
+ // File name of TF Lite model as expected in the assets folder
+ private const val TFLITE_MODEL = "autocomplete.tflite"
+
+ // Size of output buffer for the model to generate text into
+ private const val OUTPUT_BUFFER_SIZE = 800
+
+ // Maximum number of words that can be fed into the model
+ private const val MAX_INPUT_WORD_COUNT = 1024
+
+ // Maximum number of attempts to generate text that does not contain bad language
+ private const val RETRY_COUNT_ON_BAD_LANGUAGE = 3
+ }
+}
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/di/appModule.kt b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/di/appModule.kt
new file mode 100644
index 00000000000..a5e662f5a89
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/di/appModule.kt
@@ -0,0 +1,20 @@
+package com.google.tensorflowdemo.di
+
+import com.google.tensorflowdemo.data.autocomplete.AutoCompleteService
+import com.google.tensorflowdemo.data.autocomplete.AutoCompleteServiceImpl
+import com.mediamonks.wordfilter.LanguageChecker
+import com.mediamonks.wordfilter.LanguageCheckerImpl
+import org.koin.android.ext.koin.androidContext
+import org.koin.core.module.dsl.singleOf
+import org.koin.dsl.module
+
+val appModule = module {
+ single {
+ AutoCompleteServiceImpl(
+ context = androidContext(),
+ languageChecker = get()
+ )
+ }
+
+ singleOf(::LanguageCheckerImpl)
+}
diff --git a/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/di/viewmodelModule.kt b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/di/viewmodelModule.kt
new file mode 100644
index 00000000000..b6ddfd87304
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/di/viewmodelModule.kt
@@ -0,0 +1,9 @@
+package com.google.tensorflowdemo.di
+
+import com.google.tensorflowdemo.ui.screens.autocomplete.AutoCompleteViewModel
+import org.koin.androidx.viewmodel.dsl.viewModelOf
+import org.koin.dsl.module
+
+val viewmodelModule = module {
+ viewModelOf(::AutoCompleteViewModel)
+}
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/MainActivity.kt b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/MainActivity.kt
new file mode 100644
index 00000000000..f633009b4ab
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/MainActivity.kt
@@ -0,0 +1,89 @@
+package com.google.tensorflowdemo.ui
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.core.view.WindowCompat
+import com.google.accompanist.systemuicontroller.rememberSystemUiController
+import com.google.tensorflowdemo.R
+import com.google.tensorflowdemo.ui.components.HeaderBar
+import com.google.tensorflowdemo.ui.screens.autocomplete.AutoCompleteScreen
+import com.google.tensorflowdemo.ui.theme.TensorFlowDemoTheme
+import com.google.tensorflowdemo.ui.theme.VeryLightGrey
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
+class MainActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ setContent {
+
+ val systemUiController = rememberSystemUiController()
+ DisposableEffect(systemUiController) {
+ // Update all of the system bar colors to be transparent, and use
+ // dark icons if we're in light theme
+ systemUiController.setSystemBarsColor(
+ color = Color.Transparent,
+ darkIcons = true
+ )
+ systemUiController.setNavigationBarColor(Color.White)
+ onDispose {}
+ }
+
+ TensorFlowDemoTheme {
+ val insets = WindowInsets.statusBars.asPaddingValues()
+ val barHeight = 66.dp
+
+ Scaffold(
+ topBar = {
+ HeaderBar(
+ label = stringResource(R.string.header_autocomplete),
+ textOffset = (insets.calculateTopPadding() / 4),
+ modifier = Modifier.height(barHeight + insets.calculateTopPadding() / 2)
+ )
+ }
+ ) { paddings ->
+ AutoCompleteScreen(
+ onShowToast = { id -> Toast.makeText(this, id, Toast.LENGTH_SHORT).show() },
+ modifier = Modifier.padding(
+ top = barHeight - 20.dp,
+ bottom = paddings.calculateBottomPadding()
+ )
+ )
+ }
+ }
+ }
+ }
+}
+
+@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Composable
+fun PreviewMain() {
+ TensorFlowDemoTheme {
+ Scaffold {
+ AutoCompleteScreen(onShowToast = {}, modifier = Modifier.padding(top = 50.dp))
+ }
+ }
+}
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/components/HeaderBar.kt b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/components/HeaderBar.kt
new file mode 100644
index 00000000000..4128e271dc3
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/components/HeaderBar.kt
@@ -0,0 +1,68 @@
+package com.google.tensorflowdemo.ui.components
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.material3.Divider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.FixedScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.google.tensorflowdemo.R
+import com.google.tensorflowdemo.ui.theme.TensorFlowDemoTheme
+
+@Composable
+fun HeaderBar(
+ label: String,
+ textOffset: Dp,
+ modifier:Modifier = Modifier
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(Color.White)
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.header_background),
+ contentDescription = stringResource(R.string.header_background_desc),
+ contentScale = FixedScale(.47f),
+ )
+ Text(
+ text = label,
+ color = MaterialTheme.colorScheme.secondary,
+ style = MaterialTheme.typography.displayLarge,
+ modifier = Modifier
+ .align(Alignment.Center)
+ .offset(y = textOffset)
+ )
+ Divider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(1.dp)
+ .align(Alignment.BottomStart)
+ )
+
+ }
+}
+
+@Preview
+@Composable
+fun PreviewHeaderBar() {
+ TensorFlowDemoTheme {
+ HeaderBar(
+ label = "Autocomplete",
+ textOffset = 20.dp
+ )
+ }
+}
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/AutoCompleteScreen.kt b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/AutoCompleteScreen.kt
new file mode 100644
index 00000000000..29baecadf80
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/AutoCompleteScreen.kt
@@ -0,0 +1,269 @@
+package com.google.tensorflowdemo.ui.screens.autocomplete
+
+import android.content.res.Configuration
+import androidx.annotation.StringRes
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.ClipboardManager
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.google.tensorflowdemo.R
+import com.google.tensorflowdemo.data.autocomplete.AutoCompleteService.AutoCompleteInputConfiguration
+import com.google.tensorflowdemo.data.autocomplete.AutoCompleteService.AutoCompleteServiceError
+import com.google.tensorflowdemo.ui.screens.autocomplete.components.AutoCompleteInfo
+import com.google.tensorflowdemo.ui.screens.autocomplete.components.AutoCompleteTextField
+import com.google.tensorflowdemo.ui.screens.autocomplete.components.TextControlBar
+import com.google.tensorflowdemo.ui.screens.autocomplete.components.WindowSizeSelection
+import com.google.tensorflowdemo.ui.theme.TensorFlowDemoTheme
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import org.koin.androidx.compose.getViewModel
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun AutoCompleteScreen(
+ onShowToast: (Int) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val viewmodel = getViewModel()
+
+ val textValue = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue(annotatedString = AnnotatedString(""))) }
+ val barState by viewmodel.textBarState.collectAsState()
+ val inputFieldEnabled by viewmodel.inputFieldEnabled.collectAsStateWithLifecycle()
+ val windowSizeConfiguration by remember { mutableStateOf(viewmodel.windowSizeConfiguration) }
+ val clipboardManager: ClipboardManager = LocalClipboardManager.current
+ val keyboardController = LocalSoftwareKeyboardController.current
+ val uriHandler = LocalUriHandler.current
+
+ fun showToast(@StringRes id: Int) {
+ keyboardController?.hide()
+
+ onShowToast(id)
+ }
+
+ AutoCompleteScreenContent(
+ inputValue = textValue.value,
+ inputEnabled = inputFieldEnabled,
+ onInputValueChange = { value ->
+ textValue.value = value
+ viewmodel.isTextEmpty = value.text.isEmpty()
+ },
+ barState = barState,
+ inputConfiguration = windowSizeConfiguration,
+ previousSuggestions = viewmodel.previousSuggestions,
+ onClear = {
+ textValue.value = TextFieldValue(AnnotatedString(""))
+ viewmodel.onClearInput()
+ },
+ onCopy = {
+ clipboardManager.setText(textValue.value.annotatedString)
+
+ onShowToast(R.string.text_copied)
+ },
+ onGenerate = { viewmodel.onGenerateAutoComplete(textValue.value.text) },
+ onRetry = viewmodel::onRetryGenerateAutoComplete,
+ onAccept = {
+ viewmodel.onAcceptSuggestion()
+ textValue.value = TextFieldValue(
+ text = textValue.value.text,
+ selection = TextRange(textValue.value.text.length)
+ )
+ },
+ onWindowSizeChange = viewmodel::onWindowSizeChange,
+ onSuggestionsRemoved = viewmodel::removeMissingSuggestions,
+ onLinkoutSelect = { uriHandler.openUri("https://github.com/keras-team/keras-nlp") },
+ modifier = modifier
+ )
+
+ val lifecycle = LocalLifecycleOwner.current.lifecycle
+ val colorScheme = MaterialTheme.colorScheme
+
+ LaunchedEffect(key1 = Unit) {
+ lifecycle.repeatOnLifecycle(state = Lifecycle.State.STARTED) {
+ launch {
+ viewmodel.suggestion.collectLatest { words ->
+ words?.let {
+ animateSuggestion(textValue, words, colorScheme) {
+ viewmodel.onSuggestionReceived()
+ }
+ }
+ }
+ }
+ launch {
+ viewmodel.resetInputText.collectLatest { resetText ->
+ resetText?.let { text ->
+ textValue.value = TextFieldValue(
+ annotatedString = AnnotatedString(text),
+ selection = TextRange(text.length)
+ )
+
+ viewmodel.onResetReceived()
+ }
+ }
+ }
+ launch {
+ viewmodel.error.collectLatest { error ->
+ showToast(getErrorMessage(error))
+ }
+ }
+ }
+ }
+}
+
+suspend fun animateSuggestion(
+ textValueState: MutableState,
+ words: List,
+ colorScheme: ColorScheme,
+ onAnimationComplete: () -> Unit
+) {
+ val builder = AnnotatedString.Builder(textValueState.value.annotatedString)
+
+ val stylePos = builder.pushStyle(
+ SpanStyle(
+ color = colorScheme.primary,
+ fontWeight = FontWeight.Bold
+ )
+ )
+
+ for (word in words) {
+ builder.append(word)
+
+ val annotatedString = builder.toAnnotatedString()
+ textValueState.value = TextFieldValue(
+ annotatedString = annotatedString,
+ selection = TextRange(annotatedString.length)
+ )
+ delay(100)
+ }
+
+ builder.pop(stylePos)
+
+ onAnimationComplete()
+}
+
+@Composable
+fun AutoCompleteScreenContent(
+ inputValue: TextFieldValue,
+ inputEnabled: Boolean,
+ onInputValueChange: (TextFieldValue) -> Unit,
+ barState: TextEditBarState,
+ previousSuggestions: List,
+ inputConfiguration: AutoCompleteInputConfiguration,
+ onClear: () -> Unit,
+ onCopy: () -> Unit,
+ onGenerate: () -> Unit,
+ onRetry: () -> Unit,
+ onAccept: () -> Unit,
+ onWindowSizeChange: (Int) -> Unit,
+ onSuggestionsRemoved: (List) -> Unit,
+ onLinkoutSelect: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier
+ .background(Color.White)
+ .fillMaxHeight()
+ ) {
+ Column(modifier = Modifier.fillMaxHeight()) {
+ Column(
+ modifier = modifier
+ .background(Color.White)
+ .padding(start = 16.dp, end = 16.dp, top = 20.dp)
+ ) {
+ AutoCompleteTextField(
+ inputValue = inputValue,
+ inputEnabled = inputEnabled,
+ previousSuggestions = previousSuggestions,
+ onInputValueChange = onInputValueChange,
+ onSuggestionsRemoved = onSuggestionsRemoved,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(250.dp)
+ .padding(bottom = 16.dp),
+ )
+ TextControlBar(
+ state = barState,
+ onClearClick = onClear,
+ onGenerateClick = onGenerate,
+ onCopyClick = onCopy,
+ onAccept = onAccept,
+ onRetry = onRetry
+ )
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ WindowSizeSelection(
+ inputConfiguration = inputConfiguration,
+ onWindowValueChange = onWindowSizeChange,
+ modifier = Modifier.padding(bottom = 32.dp, start = 16.dp, end = 16.dp)
+ )
+ AutoCompleteInfo(
+ onLinkoutSelect = onLinkoutSelect,
+ modifier = Modifier
+ .padding(horizontal = 8.dp, vertical = 8.dp)
+ )
+ }
+ }
+}
+
+private fun getErrorMessage(error: AutoCompleteServiceError) = when (error) {
+ AutoCompleteServiceError.MODEL_NOT_INITIALIZED -> R.string.error_model_not_initialized
+ AutoCompleteServiceError.NO_SUGGESTIONS -> R.string.error_no_suggestion_found
+ AutoCompleteServiceError.MODEL_FILE_NOT_FOUND -> R.string.error_model_not_found
+ AutoCompleteServiceError.BAD_LANGUAGE -> R.string.error_input_contains_bad_language
+}
+
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true)
+@Composable
+fun PreviewAutoCompleteScreen() {
+ TensorFlowDemoTheme {
+ val inputValue by remember { mutableStateOf(TextFieldValue()) }
+ AutoCompleteScreenContent(
+ inputValue = inputValue,
+ onInputValueChange = {},
+ inputEnabled = true,
+ inputConfiguration = AutoCompleteInputConfiguration(),
+ previousSuggestions = listOf(),
+ onClear = {},
+ onCopy = {},
+ onGenerate = {},
+ onRetry = {},
+ onAccept = {},
+ onWindowSizeChange = {},
+ onSuggestionsRemoved = {},
+ onLinkoutSelect = {},
+ barState = initialControlBarState,
+ )
+ }
+}
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/AutoCompleteViewModel.kt b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/AutoCompleteViewModel.kt
new file mode 100644
index 00000000000..654b306abd5
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/AutoCompleteViewModel.kt
@@ -0,0 +1,249 @@
+package com.google.tensorflowdemo.ui.screens.autocomplete
+
+import androidx.compose.runtime.mutableStateListOf
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.tensorflowdemo.data.autocomplete.AutoCompleteService
+import com.google.tensorflowdemo.data.autocomplete.AutoCompleteService.AutoCompleteResult
+import com.google.tensorflowdemo.data.autocomplete.AutoCompleteService.AutoCompleteServiceError
+import com.google.tensorflowdemo.data.autocomplete.AutoCompleteService.InitModelResult
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+class AutoCompleteViewModel(
+ private val autoCompleteService: AutoCompleteService
+) : ViewModel() {
+
+ private var currentSuggestionInputText: String = ""
+ private var currentSuggestionText: String? = null
+ private var windowSize = 0
+ private var initModelError: InitModelResult.Error? = null
+ private var previousSuggestionsIndex = 0
+ private val isGenerating = MutableStateFlow(false)
+ private val hasGenerated = MutableStateFlow(false)
+ private val isSuggesting = MutableStateFlow(false)
+ private val isModelInitialized = MutableStateFlow(false)
+
+ init {
+ if (!autoCompleteService.isInitialized) {
+ viewModelScope.launch {
+ val result = autoCompleteService.initModel()
+ if (result is InitModelResult.Error) {
+ initModelError = result
+ }
+ isModelInitialized.value = true
+ }
+ } else {
+ isModelInitialized.value = true
+ }
+ }
+
+ private val _isTextEmpty = MutableStateFlow(true)
+ var isTextEmpty: Boolean = true
+ set(value) {
+ field = value
+ _isTextEmpty.value = value
+
+ // Clear previous suggestions if the input text is empty
+ if (value) {
+ _previousSuggestions.clear()
+ }
+ }
+
+ val windowSizeConfiguration = autoCompleteService.inputConfiguration
+
+ /**
+ * State flow to reset text to previous value.
+ * Needs to be acknowledged by calling [onResetReceived] since the previous value can be the same
+ */
+ private val _resetInputText = MutableStateFlow(null)
+ val resetInputText: StateFlow
+ get() = _resetInputText
+
+ fun onResetReceived() {
+ _resetInputText.value = null
+ }
+
+ /**
+ * State flow containing most recent suggestion from model, as list of words
+ * Needs to be acknowledged by calling [onSuggestionReceived]
+ */
+ private val _suggestion = MutableStateFlow?>(null)
+ val suggestion: StateFlow?>
+ get() = _suggestion
+
+ /**
+ * Shared flow exposing errors from autocomplete service
+ */
+ private val _error = MutableSharedFlow()
+ val error: SharedFlow
+ get() = _error
+
+ /**
+ * State flow exposing previously made suggestions
+ */
+ private val _previousSuggestions = mutableStateListOf()
+ val previousSuggestions: List
+ get() = _previousSuggestions
+
+ /**
+ * State flow exposing whether Clear CTA should be enabled
+ */
+ private val clearEnabled = combine(isGenerating, _isTextEmpty) { isGenerating, isEmpty ->
+ !isGenerating && !isEmpty
+ }
+
+ /**
+ * State flow exposing whether Generate CTA should be enabled
+ */
+ private val generateEnabled = combine(isGenerating, _isTextEmpty) { isGenerating, isEmpty ->
+ !isGenerating && !isEmpty
+ }
+
+ /**
+ * State flow exposing whether Copy CTA should be enabled
+ */
+ private val copyEnabled = combine(isGenerating, hasGenerated) { isGenerating, hasGenerated ->
+ !isGenerating && hasGenerated
+ }
+
+ /**
+ * State flow exposing edit bar state for Clear, Generate & Copy CTAs & generation process state
+ */
+ private val editingBarState = combine(clearEnabled, generateEnabled, copyEnabled, isGenerating) { clear, generate, copy, generating ->
+ TextEditBarState.Editing(
+ clearEnabled = clear,
+ generateEnabled = generate,
+ copyEnabled = copy,
+ generating = generating,
+ )
+ }
+
+ /**
+ * State flow exposing edit bar state & whether a suggestion is active
+ */
+ val textBarState = combine(editingBarState, isSuggesting) { editState, suggesting ->
+ if (suggesting) TextEditBarState.Suggesting
+ else editState
+ }.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.Lazily,
+ initialValue = initialControlBarState
+ )
+
+ /**
+ * State flow exposing whether input by user should be possible
+ */
+ val inputFieldEnabled = combine(isModelInitialized, isGenerating, isSuggesting) { initialized, generating, suggesting ->
+ initialized && !generating && !suggesting
+ }.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.Lazily,
+ initialValue = true
+ )
+
+ fun onClearInput() {
+ isGenerating.value = false
+ hasGenerated.value = false
+ _isTextEmpty.value = true
+ currentSuggestionInputText = ""
+
+ _previousSuggestions.clear()
+ }
+
+ fun onWindowSizeChange(size: Int) {
+ windowSize = size
+ }
+
+ fun onRetryGenerateAutoComplete() {
+ _resetInputText.value = currentSuggestionInputText
+
+ isSuggesting.value = false
+ currentSuggestionText = null
+
+ onGenerateAutoComplete(currentSuggestionInputText)
+ }
+
+ fun onAcceptSuggestion() {
+ currentSuggestionText?.let { text ->
+ _previousSuggestions += Suggestion(
+ text = text,
+ id = previousSuggestionsIndex++
+ )
+ }
+
+ isSuggesting.value = false
+ currentSuggestionText = null
+ }
+
+ fun removeMissingSuggestions(ids: List) {
+ for (id in ids) {
+ _previousSuggestions.removeIf { suggestion -> suggestion.id == id }
+ }
+ }
+
+ fun onGenerateAutoComplete(text: String) {
+ initModelError?.let { error ->
+ viewModelScope.launch {
+ _error.emit(error.error)
+ }
+ return
+ }
+
+ currentSuggestionInputText = text
+
+ isGenerating.value = true
+
+ viewModelScope.launch {
+ when (val result = autoCompleteService.getSuggestion(text, applyWindow = true, windowSize = windowSize)) {
+ is AutoCompleteResult.Success -> {
+ _suggestion.value = result.words
+ currentSuggestionText = result.words.joinToString(separator = "")
+ }
+
+ is AutoCompleteResult.Error -> {
+ _error.emit(result.error)
+
+ isGenerating.value = false
+ }
+ }
+ }
+ }
+
+ fun onSuggestionReceived() {
+ _suggestion.value = null
+
+ isGenerating.value = false
+ hasGenerated.value = true
+ isSuggesting.value = true
+ }
+}
+
+sealed class TextEditBarState {
+ data class Editing(
+ val clearEnabled: Boolean,
+ val generateEnabled: Boolean,
+ val copyEnabled: Boolean,
+ val generating: Boolean,
+ ) : TextEditBarState()
+
+ object Suggesting : TextEditBarState()
+}
+
+val initialControlBarState: TextEditBarState = TextEditBarState.Editing(
+ clearEnabled = false,
+ generateEnabled = false,
+ copyEnabled = false,
+ generating = false
+)
+
+data class Suggestion(
+ val text: String,
+ val id: Int
+)
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/components/AutoCompleteInfo.kt b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/components/AutoCompleteInfo.kt
new file mode 100644
index 00000000000..be8016fe644
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/components/AutoCompleteInfo.kt
@@ -0,0 +1,41 @@
+package com.google.tensorflowdemo.ui.screens.autocomplete.components
+
+import android.content.res.Configuration
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.google.tensorflowdemo.R
+import com.google.tensorflowdemo.ui.theme.TensorFlowDemoTheme
+
+@Composable
+fun AutoCompleteInfo(
+ onLinkoutSelect: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ TextButton(
+ onClick = onLinkoutSelect,
+ modifier = modifier.fillMaxWidth()
+ ) {
+ Spacer(modifier = Modifier.weight(1f))
+ Text(
+ text = stringResource(R.string.about_title),
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.tertiary,
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ }
+}
+
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true)
+@Composable
+fun PreviewAutCompleteInfo() {
+ TensorFlowDemoTheme {
+ AutoCompleteInfo({})
+ }
+}
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/components/AutoCompleteTextField.kt b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/components/AutoCompleteTextField.kt
new file mode 100644
index 00000000000..d0241b61fe5
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/components/AutoCompleteTextField.kt
@@ -0,0 +1,116 @@
+package com.google.tensorflowdemo.ui.screens.autocomplete.components
+
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.TextFieldValue
+import com.google.tensorflowdemo.R
+import com.google.tensorflowdemo.ui.screens.autocomplete.Suggestion
+import com.google.tensorflowdemo.ui.theme.InactiveOutlinedTextFieldBorder
+import com.google.tensorflowdemo.ui.theme.DarkBlue
+import com.google.tensorflowdemo.ui.theme.ActiveOutlinedTextFieldBackground
+import com.google.tensorflowdemo.ui.theme.InactiveOutlinedTextFieldBackground
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AutoCompleteTextField(
+ inputValue: TextFieldValue,
+ inputEnabled: Boolean,
+ previousSuggestions: List,
+ onInputValueChange: (TextFieldValue) -> Unit,
+ onSuggestionsRemoved: (List) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val focusRequester = remember { FocusRequester() }
+ val scope = rememberCoroutineScope()
+ val colorScheme = MaterialTheme.colorScheme
+
+ SideEffect {
+ if (inputEnabled) {
+ scope.launch {
+ focusRequester.requestFocus()
+ }
+ }
+ }
+
+ OutlinedTextField(
+ value = inputValue,
+ onValueChange = onInputValueChange,
+ enabled = inputEnabled,
+ textStyle = MaterialTheme.typography.bodySmall,
+ shape = MaterialTheme.shapes.medium,
+ placeholder = {
+ Text(
+ text = stringResource(R.string.input_hint),
+ color = MaterialTheme.colorScheme.tertiary,
+ modifier = Modifier.alpha(.7f)
+ )
+ },
+ colors = TextFieldDefaults.outlinedTextFieldColors(
+ disabledTextColor = MaterialTheme.colorScheme.onSurface,
+ unfocusedBorderColor = MaterialTheme.colorScheme.tertiary.copy(alpha = .7f),
+ disabledBorderColor = InactiveOutlinedTextFieldBorder,
+ focusedBorderColor = DarkBlue,
+ containerColor = when {
+ inputValue.text.isEmpty() -> InactiveOutlinedTextFieldBackground
+ inputEnabled -> ActiveOutlinedTextFieldBackground
+ else -> InactiveOutlinedTextFieldBackground
+ }
+ ),
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.Sentences,
+ ),
+ modifier = modifier
+ .focusRequester(focusRequester)
+ )
+}
+
+private fun annotatePreviousSuggestions(
+ text: AnnotatedString,
+ suggestions: List,
+ colorScheme: ColorScheme,
+ onSuggestionsRemoved: (List) -> Unit
+): AnnotatedString {
+ val removedSuggestionIds = mutableListOf()
+
+ val string = buildAnnotatedString {
+ append(text)
+
+ for (suggestion in suggestions) {
+ val index = text.indexOf(suggestion.text)
+ if (index == -1) {
+ removedSuggestionIds += suggestion.id
+ } else {
+ addStyle(
+ style = SpanStyle(color = colorScheme.primary),
+ start = index,
+ end = index + suggestion.text.length
+ )
+ }
+ }
+ }
+
+ if (removedSuggestionIds.isNotEmpty()) {
+ onSuggestionsRemoved(removedSuggestionIds)
+ }
+
+ return string
+}
diff --git a/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/components/TextControlBar.kt b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/components/TextControlBar.kt
new file mode 100644
index 00000000000..13e387a559a
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/components/TextControlBar.kt
@@ -0,0 +1,209 @@
+package com.google.tensorflowdemo.ui.screens.autocomplete.components
+
+import android.content.res.Configuration.UI_MODE_NIGHT_NO
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.outlined.ContentCopy
+import androidx.compose.material.icons.outlined.RestartAlt
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.google.tensorflowdemo.R
+import com.google.tensorflowdemo.ui.screens.autocomplete.TextEditBarState
+import com.google.tensorflowdemo.ui.theme.TensorFlowDemoTheme
+
+@Composable
+fun TextControlBar(
+ state: TextEditBarState,
+ onClearClick: () -> Unit,
+ onGenerateClick: () -> Unit,
+ onCopyClick: () -> Unit,
+ onRetry: () -> Unit,
+ onAccept: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ when (state) {
+ is TextEditBarState.Editing ->
+ TextEditBar(
+ onClearClick = onClearClick,
+ onGenerateClick = onGenerateClick,
+ onCopyClick = onCopyClick,
+ barState = state,
+ modifier = modifier
+ )
+
+ is TextEditBarState.Suggesting ->
+ SuggestionControlBar(
+ onRetry = onRetry,
+ onAccept = onAccept,
+ modifier = modifier
+ )
+ }
+}
+
+@Composable
+fun TextEditBar(
+ onClearClick: () -> Unit,
+ onGenerateClick: () -> Unit,
+ onCopyClick: () -> Unit,
+ barState: TextEditBarState.Editing,
+ modifier: Modifier = Modifier
+) {
+ Row(
+ modifier = modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ IconButton(
+ onClick = onClearClick,
+ enabled = barState.clearEnabled,
+ colors = IconButtonDefaults.iconButtonColors(
+ contentColor = MaterialTheme.colorScheme.tertiary
+ ),
+ modifier = Modifier.padding(start = 12.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.RestartAlt,
+ contentDescription = stringResource(R.string.clear_cta)
+ )
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ OutlinedButton(
+ onClick = onGenerateClick,
+ enabled = barState.generateEnabled,
+ border = if (barState.generateEnabled) {
+ BorderStroke(1.dp, MaterialTheme.colorScheme.primary)
+ } else {
+ BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = .38f))
+ },
+ colors = ButtonDefaults.outlinedButtonColors(
+ contentColor = MaterialTheme.colorScheme.tertiary
+ ),
+ modifier = Modifier
+ .height(40.dp)
+ .width(180.dp)
+ ) {
+ if (barState.generating) {
+ CircularProgressIndicator(
+ strokeWidth = 2.dp,
+ modifier = Modifier
+ .scale(.5f)
+ .offset(0.dp, (-8).dp)
+ )
+ } else {
+ Text(
+ text = stringResource(R.string.generate_cta),
+ modifier = Modifier.padding(horizontal = 32.dp)
+ )
+ }
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ IconButton(
+ onClick = onCopyClick,
+ enabled = barState.copyEnabled,
+ colors = IconButtonDefaults.iconButtonColors(
+ contentColor = MaterialTheme.colorScheme.tertiary
+ ),
+ modifier = Modifier.padding(end = 12.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.ContentCopy,
+ contentDescription = stringResource(R.string.copy_cta)
+ )
+ }
+ }
+}
+
+@Composable
+fun SuggestionControlBar(
+ onRetry: () -> Unit,
+ onAccept: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Row(modifier = modifier.fillMaxWidth()) {
+ TextButton(
+ onClick = onRetry,
+ modifier = Modifier.padding(start = 16.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.reject_suggestion_cta),
+ color = MaterialTheme.colorScheme.tertiary
+ )
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ OutlinedButton(
+ onClick = onAccept,
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary),
+ modifier = Modifier.padding(end = 12.dp)
+ ) {
+ Row {
+ Icon(
+ imageVector = Icons.Default.Check,
+ tint = MaterialTheme.colorScheme.tertiary,
+ contentDescription = null,
+ modifier = Modifier
+ .scale(.8f)
+ .padding(start = 32.dp)
+ )
+ Text(
+ text = stringResource(R.string.accept_suggestion_cta),
+ color = MaterialTheme.colorScheme.tertiary,
+ modifier = Modifier.padding(start = 8.dp, end = 32.dp, top = 2.dp)
+ )
+ }
+
+ }
+ }
+}
+
+@Preview(uiMode = UI_MODE_NIGHT_NO, showBackground = true)
+@Composable
+fun PreviewTextControlBar() {
+ TensorFlowDemoTheme {
+ Column {
+ TextControlBar(
+ state = TextEditBarState.Editing(
+ clearEnabled = false,
+ generateEnabled = true,
+ copyEnabled = false,
+ generating = false
+ ),
+ onClearClick = {},
+ onGenerateClick = {},
+ onCopyClick = {},
+ onRetry = {},
+ onAccept = {}
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ TextControlBar(
+ state = TextEditBarState.Suggesting,
+ onClearClick = { },
+ onGenerateClick = { },
+ onCopyClick = {},
+ onRetry = {},
+ onAccept = {}
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/components/WindowSizeSelection.kt b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/components/WindowSizeSelection.kt
new file mode 100644
index 00000000000..46f6748c2f9
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/screens/autocomplete/components/WindowSizeSelection.kt
@@ -0,0 +1,89 @@
+package com.google.tensorflowdemo.ui.screens.autocomplete.components
+
+import android.content.res.Configuration
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.google.tensorflowdemo.R
+import com.google.tensorflowdemo.data.autocomplete.AutoCompleteService.AutoCompleteInputConfiguration
+import com.google.tensorflowdemo.ui.theme.LightGrey
+import com.google.tensorflowdemo.ui.theme.TensorFlowDemoTheme
+
+@Composable
+fun WindowSizeSelection(
+ inputConfiguration: AutoCompleteInputConfiguration,
+ onWindowValueChange: (Int) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ var sliderValue by remember { mutableStateOf(inputConfiguration.initialWordCount) }
+
+ LaunchedEffect(key1 = Unit) {
+ onWindowValueChange(inputConfiguration.initialWordCount)
+ }
+
+ Column(modifier = modifier.fillMaxWidth()) {
+ Text(
+ text = stringResource(R.string.window_size_slider_label),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.tertiary,
+ modifier = Modifier
+ )
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp)
+ ) {
+ Slider(
+ value = sliderValue.toFloat(),
+ onValueChange = { value ->
+ sliderValue = value.toInt()
+
+ onWindowValueChange(sliderValue)
+ },
+ valueRange = inputConfiguration.minWordCount.toFloat()..inputConfiguration.maxWordCount.toFloat(),
+ steps = 45,
+ colors = SliderDefaults.colors(
+ activeTickColor = MaterialTheme.colorScheme.primary,
+ inactiveTrackColor = LightGrey,
+ inactiveTickColor = LightGrey
+ ),
+ modifier = Modifier.weight(1f)
+ )
+ Text(
+ text = stringResource(R.string.window_size_wordcount).replace("{count}", sliderValue.toString()),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.tertiary,
+ modifier = Modifier.padding(start = 16.dp)
+ )
+ }
+ }
+}
+
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true)
+@Composable
+fun PreviewWindowSizeSelection() {
+ TensorFlowDemoTheme {
+ WindowSizeSelection(
+ inputConfiguration = AutoCompleteInputConfiguration(),
+ onWindowValueChange = {}
+ )
+ }
+}
diff --git a/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/theme/Color.kt b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/theme/Color.kt
new file mode 100644
index 00000000000..cee15668321
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/theme/Color.kt
@@ -0,0 +1,15 @@
+package com.google.tensorflowdemo.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Orange = Color(0xFFFF6F00)
+val DarkBlue = Color(0xFF425066)
+val DarkGrey = Color(0xFF616161)
+val MediumGrey = Color(0xFFCCCCCC)
+val LightGrey = Color(0xFFE6E6E6)
+val VeryLightGrey = Color(0xFFF8F8F8)
+val DarkRed = Color(0xFFBF281B)
+
+val InactiveOutlinedTextFieldBorder = Color(0xFFD4D7DC)
+val InactiveOutlinedTextFieldBackground = Color(0xFFF9F9F9)
+val ActiveOutlinedTextFieldBackground = Color(0xFFFFFFFF)
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/theme/Shape.kt b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/theme/Shape.kt
new file mode 100644
index 00000000000..4b5f356d087
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/theme/Shape.kt
@@ -0,0 +1,10 @@
+package com.google.tensorflowdemo.ui.theme
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Shapes
+import androidx.compose.ui.unit.dp
+
+val TfDemoShapes = Shapes(
+ small = RoundedCornerShape(size = 12.dp),
+ medium = RoundedCornerShape(size = 12.dp)
+)
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/theme/Theme.kt b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/theme/Theme.kt
new file mode 100644
index 00000000000..7ff431672e8
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/theme/Theme.kt
@@ -0,0 +1,27 @@
+package com.google.tensorflowdemo.ui.theme
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+
+private val TfDemoColorScheme = lightColorScheme(
+ primary = Orange,
+ secondary = DarkBlue,
+ tertiary = DarkGrey,
+ surface = VeryLightGrey,
+ surfaceVariant = LightGrey,
+ outline = MediumGrey,
+ error = DarkRed
+)
+
+@Composable
+fun TensorFlowDemoTheme(
+ content: @Composable () -> Unit
+) {
+ MaterialTheme(
+ colorScheme = TfDemoColorScheme,
+ typography = TfDemoTypography,
+ shapes = TfDemoShapes,
+ content = content
+ )
+}
diff --git a/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/theme/Type.kt b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/theme/Type.kt
new file mode 100644
index 00000000000..e867fd7b397
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/ui/theme/Type.kt
@@ -0,0 +1,35 @@
+package com.google.tensorflowdemo.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.TextUnitType
+import androidx.compose.ui.unit.sp
+import com.google.tensorflowdemo.R
+
+val TfDemoTypography = Typography(
+ displayLarge = TextStyle(
+ fontFamily = FontFamily(Font(R.font.roboto_regular)),
+ fontSize = 20.sp,
+ fontWeight = FontWeight.W800,
+ letterSpacing = TextUnit(.05f, TextUnitType.Em),
+ background = Color.White
+ ),
+ titleSmall = TextStyle(
+ fontFamily = FontFamily(Font(R.font.roboto_regular)),
+ fontSize = 14.sp,
+ fontWeight = FontWeight.W500,
+ letterSpacing = TextUnit(.1f, TextUnitType.Em),
+ ),
+ bodySmall = TextStyle(
+ fontFamily = FontFamily(Font(R.font.roboto_regular)),
+ fontSize = 14.sp,
+ fontWeight = FontWeight.W200,
+ letterSpacing = TextUnit(.1f, TextUnitType.Em),
+ lineHeight = 20.sp
+ )
+)
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/util/StringExt.kt b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/util/StringExt.kt
new file mode 100644
index 00000000000..240e1cf9a38
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/java/com/google/tensorflowdemo/util/StringExt.kt
@@ -0,0 +1,48 @@
+package com.google.tensorflowdemo.util
+
+fun String.trimToMaxWordCount(count: Int): String {
+ val allWords = allWords()
+ val wordCount = allWords.size
+ if (wordCount < count) return this
+
+ val lastWords = allWords.toMutableList().subList(allWords.size - count, allWords.size)
+ val lastText = lastWords.joinToString(separator = "")
+
+ var inputIndex = this.length
+ for (trimmedTextIndex in lastText.length - 1 downTo 0) {
+ inputIndex--
+ val trimmedChar = lastText[trimmedTextIndex]
+ while (inputIndex >= 0 && this[inputIndex] != trimmedChar) {
+ inputIndex--
+ }
+ }
+ return this.substring(inputIndex)
+}
+
+fun String.splitToWords(): List {
+ val allWords = allWords()
+
+ var index = 0
+ val indexList = allWords.mapIndexed { wordIndex, word ->
+ if (wordIndex == 0) {
+ index += word.length
+ 0
+ } else {
+ val ch = word[0]
+ while (index < length && this[index] != ch) index++
+ val outputIndex = index
+ index += word.length
+ outputIndex
+ }
+ }
+
+ return indexList.mapIndexed { i, wordStartIndex ->
+ if (i < indexList.size - 1) {
+ val wordEndIndex = indexList[i + 1]
+ this.substring(wordStartIndex, wordEndIndex)
+ } else this.substring(wordStartIndex)
+ }
+}
+
+private val wordsRegex = """(\b\S+\b)""".toRegex()
+fun String.allWords() = wordsRegex.findAll(this).toList().map { it.groupValues.first() }
diff --git a/lite/examples/generative_ai/android/app/src/main/res/drawable/header_background.xml b/lite/examples/generative_ai/android/app/src/main/res/drawable/header_background.xml
new file mode 100644
index 00000000000..dfb636bd0ca
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/res/drawable/header_background.xml
@@ -0,0 +1,812 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lite/examples/generative_ai/android/app/src/main/res/drawable/ic_launcher_background.xml b/lite/examples/generative_ai/android/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000000..7fa8da209db
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lite/examples/generative_ai/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/lite/examples/generative_ai/android/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 00000000000..f0cc8a153ee
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lite/examples/generative_ai/android/app/src/main/res/font/roboto_bold.ttf b/lite/examples/generative_ai/android/app/src/main/res/font/roboto_bold.ttf
new file mode 100644
index 00000000000..43da14d84ec
Binary files /dev/null and b/lite/examples/generative_ai/android/app/src/main/res/font/roboto_bold.ttf differ
diff --git a/lite/examples/generative_ai/android/app/src/main/res/font/roboto_regular.ttf b/lite/examples/generative_ai/android/app/src/main/res/font/roboto_regular.ttf
new file mode 100644
index 00000000000..ddf4bfacb39
Binary files /dev/null and b/lite/examples/generative_ai/android/app/src/main/res/font/roboto_regular.ttf differ
diff --git a/lite/examples/generative_ai/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/lite/examples/generative_ai/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000000..7353dbd1fd8
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/lite/examples/generative_ai/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000000..7353dbd1fd8
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/lite/examples/generative_ai/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000000..5d738dade88
Binary files /dev/null and b/lite/examples/generative_ai/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/lite/examples/generative_ai/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/lite/examples/generative_ai/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000000..2c5920de0cd
Binary files /dev/null and b/lite/examples/generative_ai/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/lite/examples/generative_ai/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/lite/examples/generative_ai/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000000..4fd3bc52f81
Binary files /dev/null and b/lite/examples/generative_ai/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/lite/examples/generative_ai/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/lite/examples/generative_ai/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000000..5f1d5dc93c5
Binary files /dev/null and b/lite/examples/generative_ai/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/lite/examples/generative_ai/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/lite/examples/generative_ai/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000000..bd9c563a110
Binary files /dev/null and b/lite/examples/generative_ai/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/lite/examples/generative_ai/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/lite/examples/generative_ai/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000000..381fb7d10bd
Binary files /dev/null and b/lite/examples/generative_ai/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/lite/examples/generative_ai/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/lite/examples/generative_ai/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000000..fa430a9caca
Binary files /dev/null and b/lite/examples/generative_ai/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/lite/examples/generative_ai/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/lite/examples/generative_ai/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000000..9a2ac3be316
Binary files /dev/null and b/lite/examples/generative_ai/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/lite/examples/generative_ai/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/lite/examples/generative_ai/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000000..357d0c4f4a1
Binary files /dev/null and b/lite/examples/generative_ai/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/lite/examples/generative_ai/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/lite/examples/generative_ai/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000000..c93b4f3b360
Binary files /dev/null and b/lite/examples/generative_ai/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/lite/examples/generative_ai/android/app/src/main/res/values/ic_launcher_background.xml b/lite/examples/generative_ai/android/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 00000000000..f42ada656ee
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
diff --git a/lite/examples/generative_ai/android/app/src/main/res/values/strings.xml b/lite/examples/generative_ai/android/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000000..8d159d33e49
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,25 @@
+
+ Keras to TFL
+
+ Header background
+ Autocomplete
+
+ Text copied
+ Tap to begin typing your story…
+ Clear
+ Generate
+ Copy
+ New suggestion
+ Accept
+
+ Apply context window
+ {count} words
+
+ Powered by KerasNLP and TensorFlow Lite
+
+
+ Model not initialized
+ No suggestions found
+ The autocomplete.tflite model is missing
+ Mind your language!
+
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/res/values/themes.xml b/lite/examples/generative_ai/android/app/src/main/res/values/themes.xml
new file mode 100644
index 00000000000..a7aa887e359
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/res/xml/backup_rules.xml b/lite/examples/generative_ai/android/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 00000000000..fa0f996d2c2
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/main/res/xml/data_extraction_rules.xml b/lite/examples/generative_ai/android/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 00000000000..9ee9997b0b4
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/app/src/test/java/com/google/tensorflowdemo/util/StringExtKtTest.kt b/lite/examples/generative_ai/android/app/src/test/java/com/google/tensorflowdemo/util/StringExtKtTest.kt
new file mode 100644
index 00000000000..e07ec4a30ce
--- /dev/null
+++ b/lite/examples/generative_ai/android/app/src/test/java/com/google/tensorflowdemo/util/StringExtKtTest.kt
@@ -0,0 +1,43 @@
+package com.google.tensorflowdemo.util
+
+import org.junit.Assert.*
+import org.junit.Test
+
+class StringExtKtTest {
+ @Test fun TestThatShortTextIsLeftUnharmed() {
+ val input = "Mary had a little lamb!"
+ val output = input.trimToMaxWordCount(20)
+ assertEquals(input, output)
+ }
+
+ @Test fun TestThatATextWithMoreWordsThanAllowedIsTrimmedProperly() {
+ val input = "Mary had a little, lamb"
+ val output = input.trimToMaxWordCount(2)
+ assertTrue(output.length < input.length)
+ }
+
+ @Test fun TestThatAnEmptyTextIsTrimmedProperly() {
+ val input = ""
+ val output = input.trimToMaxWordCount(5)
+ assertTrue(output.isEmpty())
+ }
+
+ @Test fun TestThatASentenceIsSplitIntoWordsProperly() {
+ val input = "Mary had a little lamb!"
+ val output = input.splitToWords()
+ assertEquals(5, output.size)
+ }
+
+ @Test fun TestThatAnEmptyStringIsSplitIntoWordsProperly() {
+ val input = ""
+ val output = input.splitToWords()
+ assertEquals(0, output.size)
+ }
+
+ @Test fun TestThatNonWordsAtStartAreKeptAsIs() {
+ val input = ". A few of us, a couple of guys"
+ val output = input.splitToWords()
+ assertEquals(8, output.size)
+ assertTrue(output.first().startsWith(input.substring(0, 2)))
+ }
+}
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/build.gradle.kts b/lite/examples/generative_ai/android/build.gradle.kts
new file mode 100644
index 00000000000..4791219b37f
--- /dev/null
+++ b/lite/examples/generative_ai/android/build.gradle.kts
@@ -0,0 +1,15 @@
+val libs = libraries
+val versionCatalog = extensions.getByType().named("libs")
+
+plugins {
+ //trick: for the same plugin versions in all sub-modules
+ id("com.android.application").version("7.4.2").apply(false)
+ id("com.android.library").version("7.4.2").apply(false)
+ kotlin("android").version("1.8.10").apply(false)
+ id("com.android.test").version("7.4.0").apply(false)
+ id("de.undercouch.download").version("4.0.2").apply(false)
+}
+
+tasks.register("clean", Delete::class) {
+ delete(rootProject.buildDir)
+}
diff --git a/lite/examples/generative_ai/android/figures/fig1.gif b/lite/examples/generative_ai/android/figures/fig1.gif
new file mode 100644
index 00000000000..6f1b2b9a2be
Binary files /dev/null and b/lite/examples/generative_ai/android/figures/fig1.gif differ
diff --git a/lite/examples/generative_ai/android/figures/fig2.gif b/lite/examples/generative_ai/android/figures/fig2.gif
new file mode 100644
index 00000000000..ae8acdcf77b
Binary files /dev/null and b/lite/examples/generative_ai/android/figures/fig2.gif differ
diff --git a/lite/examples/generative_ai/android/gradle.properties b/lite/examples/generative_ai/android/gradle.properties
new file mode 100644
index 00000000000..3e927b11efb
--- /dev/null
+++ b/lite/examples/generative_ai/android/gradle.properties
@@ -0,0 +1,21 @@
+# 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
+# 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
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/gradle/libs.versions.toml b/lite/examples/generative_ai/android/gradle/libs.versions.toml
new file mode 100644
index 00000000000..a13356e56e2
--- /dev/null
+++ b/lite/examples/generative_ai/android/gradle/libs.versions.toml
@@ -0,0 +1,32 @@
+[versions]
+compose = "1.3.2"
+lifecycle = "2.6.0-beta01"
+koin = "3.4.0"
+
+[libraries]
+compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
+compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
+compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
+compose-foundation = { module = "androidx.compose.foundation:foundation", version = "1.3.1" }
+compose-material = { module = "androidx.compose.material3:material3", version = "1.0.1" }
+compose-material-icons = { module = "androidx.compose.material:material-icons-extended", version = "1.4.0" }
+compose-activity = { module = "androidx.activity:activity-compose", version = "1.6.1" }
+
+accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version = "0.30.0" }
+
+koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
+koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
+koin-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
+
+lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycle" }
+lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
+lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
+lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
+
+napier = { module = "io.github.aakira:napier", version = "2.6.1" }
+
+wordfilter = { module = "com.github.mediamonks:AndroidWordFilter", version = "1.0.3" }
+
+tflite = { module = "org.tensorflow:tensorflow-lite", version = "2.12.0" }
+
+junit = { module = "junit:junit", version = "4.13.2" }
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/gradle/wrapper/gradle-wrapper.properties b/lite/examples/generative_ai/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000000..8842cd60a46
--- /dev/null
+++ b/lite/examples/generative_ai/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sun Mar 12 16:16:18 CST 2023
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/lite/examples/generative_ai/android/gradlew b/lite/examples/generative_ai/android/gradlew
new file mode 100755
index 00000000000..4f906e0c811
--- /dev/null
+++ b/lite/examples/generative_ai/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/lite/examples/generative_ai/android/gradlew.bat b/lite/examples/generative_ai/android/gradlew.bat
new file mode 100644
index 00000000000..107acd32c4e
--- /dev/null
+++ b/lite/examples/generative_ai/android/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/lite/examples/generative_ai/android/how-to-build.md b/lite/examples/generative_ai/android/how-to-build.md
new file mode 100644
index 00000000000..38752fd8e92
--- /dev/null
+++ b/lite/examples/generative_ai/android/how-to-build.md
@@ -0,0 +1,22 @@
+## Prerequisites
+If you haven’t already, install [Android Studio](https://developer.android.com/studio/index.html), following the instructions on the website.
+
+* Android Studio 2022.2.1 or above
+* An Android device or Android emulator with more than 4G memory
+
+## Building and Running with Android Studio
+* Open Android Studio, and from the Welcome screen, select Open an existing Android Studio project.
+* From the `Open File or Project` window that appears, navigate to and select the `lite/examples/generative_ai/android` directory from wherever you cloned the TensorFlow Lite sample GitHub repo.
+* You may also need to install various platforms and tools according to error messages.
+* Rename the converted `.tflite model` to `autocomplete.tflite` and copy it into `app/src/main/assets/` folder.
+* Select menu `Build -> Make Project` to build the app (Ctrl+F9, depending on your version).
+* Click menu `Run -> Run 'app'` (Shift+F10, depending on your version).
+
+Alternatively, you can also use the [gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html#gradle_wrapper) to build it in the command line. Please refer to the [Gradle documentation](https://docs.gradle.org/current/userguide/command_line_interface.html) for more information.
+
+## (Optional) Building the .aar file
+By default the app automatically downloads the needed .aar files. But if you want to build your own, switch to `app/libs/build_aar/` folder run `./build_aar.sh`. This script will pull in the necessary ops from [TensorFlow Text](https://www.tensorflow.org/text) and build the aar for [Select TF operators](https://www.tensorflow.org/lite/guide/ops_select).
+
+After compilation, a new file `tftext_tflite_flex.aar` is generated. Replace the `.aar` file in `app/libs/` folder and re-build the app.
+
+Note that you still need to include the standard `tensorflow-lite` aar in your gradle file.
\ No newline at end of file
diff --git a/lite/examples/generative_ai/android/ml/README.md b/lite/examples/generative_ai/android/ml/README.md
new file mode 100644
index 00000000000..00a71f504fc
--- /dev/null
+++ b/lite/examples/generative_ai/android/ml/README.md
@@ -0,0 +1 @@
+To generate the TFLite model, please follow this [notebook](https://github.com/tensorflow/codelabs/blob/main/KerasNLP/io2023_workshop.ipynb).
diff --git a/lite/examples/generative_ai/android/settings.gradle.kts b/lite/examples/generative_ai/android/settings.gradle.kts
new file mode 100644
index 00000000000..3a31ddd15fc
--- /dev/null
+++ b/lite/examples/generative_ai/android/settings.gradle.kts
@@ -0,0 +1,27 @@
+@file:Suppress("UnstableApiUsage")
+pluginManagement {
+ repositories {
+ google()
+ gradlePluginPortal()
+ mavenCentral()
+ }
+}
+
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
+ repositories {
+ google()
+ mavenCentral()
+ maven {
+ url = uri("https://jitpack.io")
+ }
+ }
+ versionCatalogs {
+ create("libraries") {
+ from(files("gradle/libs.versions.toml"))
+ }
+ }
+}
+
+rootProject.name = "Google TensorFlow Demo"
+include(":app")
\ No newline at end of file