diff --git a/README.md b/README.md index dbf33cb6..e0c035f2 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Here is the list of samples you can find in the `/samples` folder: | Samples | | |:----------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Gemini Hybrid sample | ✨📱☁️ **Hybrid Inference**:
A sample demonstrating a hybrid approach to generative AI, utilizing both on-device (Gemini Nano via ML Kit) and cloud-based (Gemini via Firebase AI SDK) models. It showcases how to fallback to the cloud when on-device capabilities are unavailable.



**[> Browse code](samples/gemini-hybrid)**

| +| Gemini Hybrid sample | ✨📱☁️ **Hybrid Inference**:
A sample demonstrating a hybrid approach to generative AI, utilizing both on-device (Gemini Nano via ML Kit) and cloud-based (Gemini via Firebase AI SDK) models. It showcases how to fallback to the cloud when on-device capabilities are unavailable.



**[> Browse code](samples/gemini-hybrid)**

| | | | | Gemini Image Chat sample | ✨🖼️🍌 **Gemini Image Chat**:
A chatbot app using the new [Gemini 3 Pro Image model](https://deepmind.google/models/gemini-image/pro/) (a.k.a. "Nano Banana Pro") enabling image generation and iterations via conversation with the Gemini model. Ask the model to generate an image and ask for tweaks in the chat.



**[> Browse code](samples/gemini-image-chat)**

| | | | @@ -44,19 +44,17 @@ Here is the list of samples you can find in the `/samples` folder: | | | | Gemini Nano Image description | ✨📱🔍 **On-device Image Description**:
A sample letting you generate image descriptions using Gemini Nano via the [GenAI Image Description API](https://developers.google.com/ml-kit/genai/image-description/android).



**[> Browse code](samples/genai-image-description)**

| | | | -| Gemini Nano Rewrite | ✨📱🖋️ **On-device Writing Assistance**:
A sample letting you proofread and rewrite text using Gemini Nano via the [GenAI Rewriting API](https://developers.google.com/ml-kit/genai/rewriting/android).



**[> Browse code](samples/genai-writing-assistance)**

| +| Gemini Nano Rewrite | ✨📱🖋️ **On-device Writing Assistance**:
A sample letting you proofread and rewrite text using Gemini Nano via the [GenAI Rewriting API](https://developers.google.com/ml-kit/genai/rewriting/android).



**[> Browse code](samples/genai-writing-assistance)**

| | | | -| Imagen sample | 🖼️ **Image Generation with Imagen**:
A sample using [Imagen to generate images](https://developer.android.com/ai/imagen#generate-image) of landscapes, objects and people in various artistic style.



**[> Browse code](samples/imagen)**

| +| Nanobanana sample | 🖼️🍌 **Nanobanana**:
A sample using [Gemini 3.1 Flash Image model](https://developer.android.com/ai/gemini) (a.k.a. \"Nano Banana\") to generate images of landscapes, objects and people in various artistic style.



**[> Browse code](samples/nanobanana)**

| | | | -| Magic Selfie sample | 🖼️📸 **Magic Selfie**:
A sample using [ML Kit subject Segmentation SDK](https://developers.google.com/ml-kit/vision/subject-segmentation/android) to remove the background behind a person, and [Imagen](https://developer.android.com/ai/imagen#generate-image) to generate new background.



**[> Browse code](samples/magic-selfie)**

| +| Magic Selfie sample | 🖼️📸 **Magic Selfie**:
A sample using [ML Kit subject Segmentation SDK](https://developers.google.com/ml-kit/vision/subject-segmentation/android) to remove the background behind a person, and Nano Banana to generate new background.



**[> Browse code](samples/magic-selfie)**

| | | | | Gemini Video Summarization sample | ✨🎥 **Gemini Video Summarization**:
A sample using Gemini Flash to [summarize videos](https://firebase.google.com/docs/ai-logic/analyze-video?api=dev) leveraging the [large file support](https://firebase.google.com/docs/ai-logic/solutions/cloud-storage).



**[> Browse code](samples/gemini-video-summarization)**

| | | | | Gemini Video Metadata sample | ✨🎥 **Gemini Video Metadata Creation**:
A sample using Gemini Flash to generate thumbnails, descriptions, hashtags, account tags, chapters and links from a video. This sample leverages the ability to provide a [Youtube video link](https://firebase.google.com/docs/ai-logic/input-file-requirements?api=dev#provide-file-using-url) to the model context for inference.



**[> Browse code](samples/gemini-video-metadata-creation)**

| | | | -| Gemini Live API to-do sample | ✨🗣️ **Gemini Live API To-do App**:
A to-do list app using the [Gemini Live API](https://developer.android.com/ai/gemini/live) to let the user interact with Gemini via voice to update the todo list.



**[> Browse code](samples/gemini-live-todo)**

| -| | | -| Imagen Editing sample | 🖼️🖌️ **Imagen Editing**:
A sample using Imagen to [generate images](https://developer.android.com/ai/imagen#generate-image) and [editing images](https://developer.android.com/ai/imagen#editing) using the mask based editing capabilities of the model.



**[> Browse code](samples/imagen-editing)**

| +| Gemini Live API to-do sample | ✨🗣️ **Gemini Live API To-do App**:
A to-do list app using the [Gemini Live API](https://developer.android.com/ai/gemini/live) to let the user interact with Gemini via voice to update the todo list.



**[> Browse code](samples/gemini-live-todo)**

| ## Reporting issues diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8cdfcf69..12329c99 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -86,8 +86,7 @@ dependencies { implementation(project(":samples:genai-summarization")) implementation(project(":samples:genai-image-description")) implementation(project(":samples:genai-writing-assistance")) - implementation(project(":samples:imagen")) - implementation(project(":samples:imagen-editing")) + implementation(project(":samples:nanobanana")) implementation(project(":samples:magic-selfie")) implementation(project(":samples:gemini-video-summarization")) implementation(project(":samples:gemini-live-todo")) diff --git a/app/src/main/java/com/android/ai/catalog/domain/SampleCatalog.kt b/app/src/main/java/com/android/ai/catalog/domain/SampleCatalog.kt index 1fadf927..51b15282 100644 --- a/app/src/main/java/com/android/ai/catalog/domain/SampleCatalog.kt +++ b/app/src/main/java/com/android/ai/catalog/domain/SampleCatalog.kt @@ -32,8 +32,7 @@ import com.android.ai.samples.genai_image_description.GenAIImageDescriptionScree import com.android.ai.samples.genai_summarization.GenAISummarizationScreen import com.android.ai.samples.genai_writing_assistance.GenAIWritingAssistanceScreen import com.android.ai.samples.geminihybrid.GeminiHybridScreen -import com.android.ai.samples.imagen.ui.ImagenScreen -import com.android.ai.samples.imagenediting.ui.ImagenEditingScreen +import com.android.ai.samples.nanobanana.ui.NanobananaScreen import com.android.ai.samples.magicselfie.ui.MagicSelfieScreen import com.android.ai.theme.extendedColorScheme import com.google.firebase.ai.type.PublicPreviewAPI @@ -61,15 +60,6 @@ val sampleCatalog = listOf( needsFirebase = true, isFeatured = true, ), - SampleCatalogItem( - title = R.string.imagen_editing_sample_list_title, - description = R.string.imagen_editing_sample_list_description, - route = "ImagenMaskEditing", - sampleEntryScreen = { ImagenEditingScreen() }, - tags = listOf(SampleTags.IMAGEN, SampleTags.FIREBASE), - needsFirebase = true, - keyArt = R.drawable.img_keyart_imagen, - ), SampleCatalogItem( title = R.string.gemini_multimodal_sample_list_title, description = R.string.gemini_multimodal_sample_list_description, @@ -114,11 +104,11 @@ val sampleCatalog = listOf( keyArt = R.drawable.img_keyart_text, ), SampleCatalogItem( - title = R.string.imagen_sample_list_title, - description = R.string.imagen_sample_list_description, - route = "ImagenImageGenerationScreen", - sampleEntryScreen = { ImagenScreen() }, - tags = listOf(SampleTags.IMAGEN, SampleTags.FIREBASE), + title = R.string.nanobanana_sample_list_title, + description = R.string.nanobanana_sample_list_description, + route = "NanobananaImageGenerationScreen", + sampleEntryScreen = { NanobananaScreen() }, + tags = listOf(SampleTags.GEMINI_FLASH, SampleTags.FIREBASE), needsFirebase = true, keyArt = R.drawable.img_keyart_imagen, ), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 19e449e6..f8876099 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,10 +13,8 @@ Android AI Samples Android\nAI Samples Open sample - Image generation with Imagen - Generate images with Imagen, Google image generation model - Image Editing with Imagen - Generate images and edit only specific areas of a generated image with inpainting + Image generation with Nanobanana + Generate images with Nanobanana, Google image generation model Magic Selfie with Gemini Change the background of your selfies with the Gemini Flash model Video Summarization with Gemini and Firebase diff --git a/samples/imagen-editing/.gitignore b/samples/imagen-editing/.gitignore deleted file mode 100644 index 42afabfd..00000000 --- a/samples/imagen-editing/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/samples/imagen-editing/README.md b/samples/imagen-editing/README.md deleted file mode 100644 index 04cf41b2..00000000 --- a/samples/imagen-editing/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Imagen Image Editing Sample - -This sample is part of the [AI Sample Catalog](../../). To build and run this sample, you should clone the entire repository. - -## Description - -This sample demonstrates how to edit images using the Imagen editing model. Users can generate an image, then draw a mask on it and provide a text prompt to inpaint (fill in) the masked area, showcasing advanced image manipulation capabilities with Imagen. - -
-Imagen Image Editing in action -
- -## How it works - -The application uses the Firebase AI SDK (see [How to run](../../#how-to-run)) for Android to interact with Imagen. The core logic is in the [`ImagenEditingDataSource.kt`](https://github.com/android/ai-samples/blob/main/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt) file. It first generates a base image using the generation model. Then, for editing, it takes the source image, a user-drawn mask, and a text prompt, and sends them to the editing model's `editImage` method to perform inpainting. - -Here is the key snippet of code that performs inpainting from [`ImagenEditingDataSource.kt`](./src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt): - -```kotlin -@OptIn(PublicPreviewAPI::class) -suspend fun inpaintImageWithMask(sourceImage: Bitmap, maskImage: Bitmap, prompt: String, editSteps: Int = DEFAULT_EDIT_STEPS): Bitmap { - val imageResponse = editingModel.editImage( - referenceImages = listOf( - ImagenRawImage(sourceImage.toImagenInlineImage()), - ImagenRawMask(maskImage.toImagenInlineImage()), - ), - prompt = prompt, - config = ImagenEditingConfig( - editMode = ImagenEditMode.INPAINT_INSERTION, - editSteps = editSteps, - ), - ) - return imageResponse.images.first().asBitmap() -} -``` - -Read more about [Imagen](https://developer.android.com/ai/imagen) in the Android Documentation. diff --git a/samples/imagen-editing/build.gradle.kts b/samples/imagen-editing/build.gradle.kts deleted file mode 100644 index bf26d7b5..00000000 --- a/samples/imagen-editing/build.gradle.kts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * 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. - */ -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.jetbrains.kotlin.android) - alias(libs.plugins.ksp) - alias(libs.plugins.compose.compiler) -} - -android { - namespace = "com.android.ai.samples.imagenediting" - compileSdk = 36 - - buildFeatures { - compose = true - } - - defaultConfig { - minSdk = 24 - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro", - ) - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = "17" - } - - lint { - warningsAsErrors = true - } -} - -dependencies { - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.androidx.material3) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.material.icons.extended) - implementation(platform(libs.firebase.bom)) - implementation(libs.firebase.ai) - implementation(libs.hilt.android) - implementation(libs.hilt.navigation.compose) - implementation(libs.androidx.runtime.livedata) - implementation(libs.ui.tooling.preview) - implementation(project(":ui-component")) - debugImplementation(libs.ui.tooling) - ksp(libs.hilt.compiler) - - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) -} diff --git a/samples/imagen-editing/imagen_editing.png b/samples/imagen-editing/imagen_editing.png deleted file mode 100644 index d603efa2..00000000 Binary files a/samples/imagen-editing/imagen_editing.png and /dev/null differ diff --git a/samples/imagen-editing/proguard-rules.pro b/samples/imagen-editing/proguard-rules.pro deleted file mode 100644 index 481bb434..00000000 --- a/samples/imagen-editing/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# 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/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt b/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt deleted file mode 100644 index a5b03f39..00000000 --- a/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * 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. - */ -package com.android.ai.samples.imagenediting.data - -import android.graphics.Bitmap -import com.google.firebase.Firebase -import com.google.firebase.ai.ai -import com.google.firebase.ai.type.GenerativeBackend -import com.google.firebase.ai.type.ImagenAspectRatio -import com.google.firebase.ai.type.ImagenEditMode -import com.google.firebase.ai.type.ImagenEditingConfig -import com.google.firebase.ai.type.ImagenGenerationConfig -import com.google.firebase.ai.type.ImagenImageFormat -import com.google.firebase.ai.type.ImagenRawImage -import com.google.firebase.ai.type.ImagenRawMask -import com.google.firebase.ai.type.PublicPreviewAPI -import com.google.firebase.ai.type.toImagenInlineImage -import javax.inject.Inject -import javax.inject.Singleton - -/** - * A data source that provides methods for interacting with the Firebase Imagen API - * for various image generation and editing tasks. - * - * This class encapsulates the logic for initializing Imagen models and calling - * their respective functions for image generation, inpainting, outpainting, and style transfer. - * It leverages the Firebase AI SDK for seamless integration with Vertex AI backends. - * - * Note: This class uses `@OptIn(PublicPreviewAPI::class)` as Imagen features - * are currently in public preview. - */ -@Singleton -class ImagenEditingDataSource @Inject constructor() { - private companion object { - const val IMAGEN_MODEL_NAME = "imagen-4.0-generate-001" - const val IMAGEN_EDITING_MODEL_NAME = "imagen-3.0-capability-001" - const val DEFAULT_EDIT_STEPS = 50 - const val DEFAULT_STYLE_STRENGTH = 1 - } - - @OptIn(PublicPreviewAPI::class) - private val imagenModel = - Firebase.ai(backend = GenerativeBackend.vertexAI()).imagenModel( - IMAGEN_MODEL_NAME, - generationConfig = ImagenGenerationConfig( - numberOfImages = 1, - aspectRatio = ImagenAspectRatio.SQUARE_1x1, - imageFormat = ImagenImageFormat.jpeg(compressionQuality = 75), - ), - ) - - @OptIn(PublicPreviewAPI::class) - private val editingModel = - Firebase.ai(backend = GenerativeBackend.vertexAI()).imagenModel( - IMAGEN_EDITING_MODEL_NAME, - generationConfig = ImagenGenerationConfig( - numberOfImages = 1, - aspectRatio = ImagenAspectRatio.SQUARE_1x1, - imageFormat = ImagenImageFormat.jpeg(compressionQuality = 75), - ), - ) - - /** - * Generates an image based on the provided prompt. - * - * This function uses the Imagen model to generate an image from a textual description. - * It returns the generated image as a Bitmap. - * - * @param prompt The textual description to generate the image from. - * @return The generated image as a [Bitmap]. - * @throws Exception if the image generation fails. - */ - @OptIn(PublicPreviewAPI::class) - suspend fun generateImage(prompt: String): Bitmap { - val imageResponse = imagenModel.generateImages( - prompt = prompt, - ) - val image = imageResponse.images.first() - return image.asBitmap() - } - - /** - * Performs inpainting on a source image using a provided mask and prompt. - * - * This function utilizes the Imagen editing model to fill in the masked areas - * of the source image based on the textual prompt. - * - * @param sourceImage The original image to be inpainted. - * @param maskImage A bitmap representing the mask, where white areas indicate - * regions to be inpainted and black areas indicate regions to be preserved. - * @param prompt A textual description of what should be generated in the masked areas. - * @param editSteps The number of editing steps to perform. Defaults to `DEFAULT_EDIT_STEPS`. - * @return A [Bitmap] representing the inpainted image. - */ - @OptIn(PublicPreviewAPI::class) - suspend fun inpaintImageWithMask(sourceImage: Bitmap, maskImage: Bitmap, prompt: String, editSteps: Int = DEFAULT_EDIT_STEPS): Bitmap { - val imageResponse = editingModel.editImage( - referenceImages = listOf( - ImagenRawImage(sourceImage.toImagenInlineImage()), - ImagenRawMask(maskImage.toImagenInlineImage()), - ), - prompt = prompt, - config = ImagenEditingConfig( - editMode = ImagenEditMode.INPAINT_INSERTION, - editSteps = editSteps, - ), - ) - return imageResponse.images.first().asBitmap() - } -} diff --git a/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingMaskEditor.kt b/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingMaskEditor.kt deleted file mode 100644 index 6d66f321..00000000 --- a/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingMaskEditor.kt +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * 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. - */ -package com.android.ai.samples.imagenediting.ui - -import android.graphics.Bitmap -import android.graphics.Paint -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.detectDragGestures -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Undo -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateListOf -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.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.graphics.StrokeJoin -import androidx.compose.ui.graphics.asAndroidPath -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.graphics.drawscope.withTransform -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.core.graphics.createBitmap -import com.android.ai.samples.imagenediting.R -import kotlin.math.min - -@Composable -fun ImagenEditingMaskEditor(sourceBitmap: Bitmap, onMaskFinalized: (Bitmap) -> Unit, onCancel: () -> Unit, modifier: Modifier = Modifier) { - val paths = remember { mutableStateListOf() } - var currentPath by remember { mutableStateOf(null) } - var scale by remember { mutableFloatStateOf(1f) } - var offsetX by remember { mutableFloatStateOf(0f) } - var offsetY by remember { mutableFloatStateOf(0f) } - - Box(modifier = modifier) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Box( - modifier = Modifier - .weight(1f) - .fillMaxWidth() - .pointerInput(Unit) { - detectDragGestures( - onDragStart = { startOffset -> - val transformedStart = Offset( - (startOffset.x - offsetX) / scale, - (startOffset.y - offsetY) / scale, - ) - currentPath = Path().apply { moveTo(transformedStart.x, transformedStart.y) } - }, - onDrag = { change, _ -> - currentPath?.let { - val transformedChange = Offset( - (change.position.x - offsetX) / scale, - (change.position.y - offsetY) / scale, - ) - it.lineTo(transformedChange.x, transformedChange.y) - currentPath = Path().apply { addPath(it) } - } - change.consume() - }, - onDragEnd = { - currentPath?.let { paths.add(it) } - currentPath = null - }, - ) - }, - ) { - Image( - bitmap = sourceBitmap.asImageBitmap(), - contentDescription = stringResource(R.string.editing_image_to_mask), - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, - ) - Canvas(modifier = Modifier.fillMaxSize()) { - val canvasWidth = size.width - val canvasHeight = size.height - val bitmapWidth = sourceBitmap.width.toFloat() - val bitmapHeight = sourceBitmap.height.toFloat() - scale = min(canvasWidth / bitmapWidth, canvasHeight / bitmapHeight) - offsetX = (canvasWidth - bitmapWidth * scale) / 2 - offsetY = (canvasHeight - bitmapHeight * scale) / 2 - withTransform( - { - translate(left = offsetX, top = offsetY) - scale(scale, scale, pivot = Offset.Zero) - }, - ) { - val strokeWidth = 70f / scale - val stroke = Stroke(width = strokeWidth, cap = StrokeCap.Round, join = StrokeJoin.Round) - val pathColor = Color.White.copy(alpha = 0.5f) - paths.forEach { path -> - drawPath(path = path, color = pathColor, style = stroke) - } - currentPath?.let { path -> - drawPath(path = path, color = pathColor, style = stroke) - } - } - } - - Row( - modifier = Modifier - .padding(16.dp) - .align(Alignment.BottomEnd) - .background(color = MaterialTheme.colorScheme.surfaceContainer, shape = RoundedCornerShape(20.dp)), - ) { - Icon( - Icons.Default.Delete, - contentDescription = stringResource(R.string.cancel_masking), - modifier = Modifier - .padding(10.dp) - .clickable(true) { - onCancel() - }, - ) - Icon( - Icons.AutoMirrored.Filled.Undo, - contentDescription = stringResource(R.string.undo_the_mask), - modifier = Modifier - .padding(10.dp) - .clickable(true) { - if (paths.isNotEmpty()) paths.removeAt(paths.lastIndex) - }, - ) - Icon( - Icons.Default.Check, - contentDescription = stringResource(R.string.save_the_mask), - modifier = Modifier - .padding(10.dp) - .clickable(true) { - val maskBitmap = createBitmap(sourceBitmap.width, sourceBitmap.height) - val canvas = android.graphics.Canvas(maskBitmap) - val paint = Paint().apply { - color = android.graphics.Color.WHITE - strokeWidth = 70f - style = Paint.Style.STROKE - strokeCap = Paint.Cap.ROUND - strokeJoin = Paint.Join.ROUND - isAntiAlias = true - } - paths.forEach { path -> canvas.drawPath(path.asAndroidPath(), paint) } - onMaskFinalized(maskBitmap) - }, - ) - } - } - } - } -} diff --git a/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingScreen.kt b/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingScreen.kt deleted file mode 100644 index 9699e7aa..00000000 --- a/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingScreen.kt +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * 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. - */ -package com.android.ai.samples.imagenediting.ui - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import androidx.activity.compose.LocalOnBackPressedDispatcherOwner -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.foundation.text.input.rememberTextFieldState -import androidx.compose.material3.ContainedLoadingIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.ImageShader -import androidx.compose.ui.graphics.ShaderBrush -import androidx.compose.ui.graphics.TileMode -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.platform.SoftwareKeyboardController -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.android.ai.samples.imagenediting.R -import com.android.ai.uicomponent.GenerateButton -import com.android.ai.uicomponent.SampleDetailTopAppBar -import com.android.ai.uicomponent.TextInput - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ImagenEditingScreen(viewModel: ImagenEditingViewModel = hiltViewModel()) { - val uiState: ImagenEditingUIState by viewModel.uiState.collectAsStateWithLifecycle() - val showMaskEditor: Boolean by viewModel.showMaskEditor.collectAsStateWithLifecycle() - val bitmapForMasking: Bitmap? by viewModel.bitmapForMasking.collectAsStateWithLifecycle() - - ImagenEditingScreenContent( - uiState = uiState, - showMaskEditor = showMaskEditor, - bitmapForMasking = bitmapForMasking, - onGenerateClick = viewModel::generateImage, - onInpaintClick = { source, mask, prompt -> viewModel.inpaintImage(source, mask, prompt) }, - onImageMaskReady = { source, mask -> viewModel.onImageMaskReady(source, mask) }, - onCancelMasking = viewModel::onCancelMasking, - modifier = Modifier.fillMaxSize(), - ) -} - -@Composable -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) -private fun ImagenEditingScreenContent( - uiState: ImagenEditingUIState, - showMaskEditor: Boolean, - bitmapForMasking: Bitmap?, - onGenerateClick: (String) -> Unit, - onInpaintClick: (source: Bitmap, mask: Bitmap, prompt: String) -> Unit, - onImageMaskReady: (source: Bitmap, mask: Bitmap) -> Unit, - onCancelMasking: () -> Unit, - modifier: Modifier = Modifier, -) { - val isGenerating = uiState is ImagenEditingUIState.Loading - val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher - - Scaffold( - containerColor = MaterialTheme.colorScheme.surface, - topBar = { - SampleDetailTopAppBar( - sampleName = stringResource(R.string.editing_title_image_generation_title), - sampleDescription = stringResource(R.string.editing_title_image_generation_subtitle), - sourceCodeUrl = "https://github.com/android/ai-samples/tree/main/samples/imagen-editing", - onBackClick = { backDispatcher?.onBackPressed() }, - ) - }, - modifier = Modifier.fillMaxWidth(), - ) { innerPadding -> - val context = LocalContext.current - val imageBitmap = remember { - val bitmap = BitmapFactory.decodeResource(context.resources, com.android.ai.uicomponent.R.drawable.img_fill) - bitmap.asImageBitmap() - } - val imageShader = remember { - ImageShader( - image = imageBitmap, - tileModeX = TileMode.Repeated, - tileModeY = TileMode.Repeated, - ) - } - - Box( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Box( - Modifier - .padding(16.dp) - .imePadding() - .widthIn(max = 440.dp) - .fillMaxHeight(0.85f) - .border( - 1.dp, - MaterialTheme.colorScheme.outline, - shape = RoundedCornerShape(40.dp), - ) - .clip(RoundedCornerShape(40.dp)) - .background(ShaderBrush(imageShader)), - contentAlignment = Alignment.Center, - ) { - val keyboardController = LocalSoftwareKeyboardController.current - - when (uiState) { - is ImagenEditingUIState.Initial -> { - Text( - text = stringResource(R.string.generate_an_image_to_edit), - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier - .padding(24.dp) - .align(Alignment.Center), - ) - - val textFieldState = rememberTextFieldState() - - TextField( - textFieldState, - isGenerating, - onGenerateClick, - keyboardController, - placeholder = stringResource(R.string.describe_the_image_to_generate), - ) - } - - is ImagenEditingUIState.Loading -> { - Box(modifier.fillMaxSize()) { - ContainedLoadingIndicator( - modifier = Modifier - .size(60.dp) - .align(Alignment.Center), - ) - } - } - - is ImagenEditingUIState.ImageGenerated -> { - if (showMaskEditor && bitmapForMasking != null) { - val textFieldState = rememberTextFieldState() - - ImagenEditingMaskEditor( - sourceBitmap = bitmapForMasking, - onMaskFinalized = { maskBitmap -> - onImageMaskReady(bitmapForMasking, maskBitmap) - }, - onCancel = { onCancelMasking() }, - modifier = Modifier.fillMaxSize(), - ) - - Text( - text = "Draw a mask on the image", - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier - .padding(24.dp) - .align(Alignment.TopCenter) - .background(color = MaterialTheme.colorScheme.surfaceContainer), - ) - } else { - val textFieldState = rememberTextFieldState() - - Image( - bitmap = uiState.bitmap.asImageBitmap(), - contentDescription = uiState.contentDescription, - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize(), - ) - TextField( - textFieldState, - isGenerating, - onGenerateClick, - keyboardController, - placeholder = stringResource(R.string.describe_the_image_to_generate), - ) - } - } - - is ImagenEditingUIState.ImageMasked -> { - Box(modifier = Modifier.fillMaxSize()) { - Image( - bitmap = uiState.originalBitmap.asImageBitmap(), - contentDescription = stringResource(R.string.editing_generated_image), - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, - ) - Image( - bitmap = uiState.maskBitmap.asImageBitmap(), - contentDescription = stringResource(R.string.editing_generated_mask), - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Fit, - colorFilter = ColorFilter.tint(Color.Red.copy(alpha = 0.5f)), - ) - } - val textFieldState = rememberTextFieldState() - - TextField( - textFieldState = textFieldState, - isGenerating = isGenerating, - onGenerateClick = { prompt -> onInpaintClick(uiState.originalBitmap, uiState.maskBitmap, prompt) }, - keyboardController, - placeholder = stringResource(R.string.describe_the_image_to_in_paint), - ) - } - - else -> {} - } - } - } - } -} - -@Composable -private fun BoxScope.TextField( - textFieldState: TextFieldState, - isGenerating: Boolean, - onGenerateClick: (String) -> Unit, - keyboardController: SoftwareKeyboardController?, - placeholder: String = "", -) { - TextInput( - state = textFieldState, - placeholder = placeholder, - primaryButton = { - GenerateButton( - text = "", - icon = painterResource(id = com.android.ai.uicomponent.R.drawable.ic_ai_img), - modifier = Modifier - .width(72.dp) - .height(55.dp) - .padding(4.dp), - enabled = !isGenerating, - onClick = { - onGenerateClick(textFieldState.text.toString()) - keyboardController?.hide() - }, - ) - }, - modifier = Modifier - .widthIn(max = 646.dp) - .padding(start = 10.dp, end = 10.dp, bottom = 10.dp) - .align(Alignment.BottomCenter), - ) -} diff --git a/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingUIState.kt b/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingUIState.kt deleted file mode 100644 index 0a37a73b..00000000 --- a/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingUIState.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * 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. - */ -package com.android.ai.samples.imagenediting.ui - -import android.graphics.Bitmap - -sealed interface ImagenEditingUIState { - data object Initial : ImagenEditingUIState - data object Loading : ImagenEditingUIState - data class ImageGenerated( - val bitmap: Bitmap, - val contentDescription: String, - ) : ImagenEditingUIState - - data class ImageMasked( - val originalBitmap: Bitmap, - val maskBitmap: Bitmap, - val contentDescription: String, - ) : ImagenEditingUIState - - data class Error(val message: String?) : ImagenEditingUIState -} diff --git a/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingViewModel.kt b/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingViewModel.kt deleted file mode 100644 index ce6f8620..00000000 --- a/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingViewModel.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * 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. - */ -package com.android.ai.samples.imagenediting.ui - -import android.graphics.Bitmap -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.android.ai.samples.imagenediting.data.ImagenEditingDataSource -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch - -@HiltViewModel -class ImagenEditingViewModel @Inject constructor(private val imagenDataSource: ImagenEditingDataSource) : ViewModel() { - - private val _uiState: MutableStateFlow = MutableStateFlow(ImagenEditingUIState.Initial) - val uiState: StateFlow = _uiState - - private val _bitmapForMasking = MutableStateFlow(null) - val bitmapForMasking: StateFlow = _bitmapForMasking - - private val _showMaskEditor = MutableStateFlow(false) - val showMaskEditor: StateFlow = _showMaskEditor - - fun generateImage(prompt: String) { - _uiState.value = ImagenEditingUIState.Loading - viewModelScope.launch { - try { - val bitmap = imagenDataSource.generateImage(prompt) - - _bitmapForMasking.value = bitmap - _showMaskEditor.value = true - _uiState.value = ImagenEditingUIState.ImageGenerated(bitmap, contentDescription = prompt) - } catch (e: Exception) { - _uiState.value = ImagenEditingUIState.Error(e.message) - } - } - } - - fun inpaintImage(sourceImage: Bitmap, maskImage: Bitmap, prompt: String, editSteps: Int = 50) { - _uiState.value = ImagenEditingUIState.Loading - viewModelScope.launch { - try { - val inpaintedBitmap = imagenDataSource.inpaintImageWithMask( - sourceImage = sourceImage, - maskImage = maskImage, - prompt = prompt, - editSteps = editSteps, - ) - _uiState.value = ImagenEditingUIState.ImageGenerated( - bitmap = inpaintedBitmap, - contentDescription = "Inpainted image based on prompt: $prompt", - ) - } catch (e: Exception) { - _uiState.value = ImagenEditingUIState.Error(e.localizedMessage ?: "An unknown error occurred during inpainting") - } - } - } - - fun onImageMaskReady(originalBitmap: Bitmap, maskBitmap: Bitmap) { - val originalContentDescription = (_uiState.value as? ImagenEditingUIState.ImageGenerated)?.contentDescription ?: "Edited image" - _uiState.value = ImagenEditingUIState.ImageMasked( - originalBitmap = originalBitmap, - maskBitmap = maskBitmap, - contentDescription = originalContentDescription, - ) - _showMaskEditor.value = false - _bitmapForMasking.value = null - } - - fun onCancelMasking() { - Log.d("ImagenEditingViewModel", "onCancelMasking") - _showMaskEditor.value = false - _bitmapForMasking.value = null - _uiState.value = ImagenEditingUIState.Initial - } -} diff --git a/samples/imagen-editing/src/main/res/values/strings.xml b/samples/imagen-editing/src/main/res/values/strings.xml deleted file mode 100644 index 4f7efff1..00000000 --- a/samples/imagen-editing/src/main/res/values/strings.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - Generate - Generating… - Prompt - Mask edit prompt - Mask Edit - Inpaint - Enter a prompt and tap \"Generate\" to generate an image - Edit Image - Finalize Mask - Generate an image, then tap to draw a mask. - An image of dog working as a chef - An unknown error occurred. - Imagen Editing - Generate images with Imagen, Google\'s image generation model. - Image to be masked - The generated image - The generated mask - Draw a mask - Cancel masking - Undo the mask - Save the mask - describe the image to generate - Generate an image to edit - describe the image to in-paint - \ No newline at end of file diff --git a/samples/imagen/.gitignore b/samples/imagen/.gitignore deleted file mode 100644 index 42afabfd..00000000 --- a/samples/imagen/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/samples/imagen/README.md b/samples/imagen/README.md deleted file mode 100644 index 9adfa02e..00000000 --- a/samples/imagen/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Imagen Image Generation Sample - -This sample is part of the [AI Sample Catalog](../../). To build and run this sample, you should clone the entire repository. - -## Description - -This sample demonstrates how to generate images from text prompts using the Imagen model. Users can input a text description, and the generative model will create an image based on that prompt, showcasing the power of text-to-image generation with Imagen. - -
-Imagen Image Generation in action -
- -## How it works - -The application uses the Firebase AI SDK (see [How to run](../../#how-to-run)) for Android to interact with Imagen. The core logic is in the [`ImagenDataSource.kt`](./src/main/java/com/android/ai/samples/imagen/data/ImagenDataSource.kt) file. An `imagenModel` is initialized with specific generation configurations (e.g., number of images, aspect ratio, image format). When a user provides a text prompt, it's passed to the `generateImages` method, which returns the generated image as a bitmap. - -Here is the key snippet of code that calls the generative model from [`ImagenDataSource.kt`](./src/main/java/com/android/ai/samples/imagen/data/ImagenDataSource.kt): - -```kotlin -@OptIn(PublicPreviewAPI::class) -suspend fun generateImage(prompt: String): Bitmap { - val imageResponse = imagenModel.generateImages( - prompt = prompt, - ) - val image = imageResponse.images.first() - return image.asBitmap() -} -``` - -Read more about [Imagen](https://developer.android.com/ai/imagen) in the Android Documentation. diff --git a/samples/imagen/imagen_image_generation.png b/samples/imagen/imagen_image_generation.png deleted file mode 100644 index af4d3ad2..00000000 Binary files a/samples/imagen/imagen_image_generation.png and /dev/null differ diff --git a/samples/imagen/proguard-rules.pro b/samples/imagen/proguard-rules.pro deleted file mode 100644 index 481bb434..00000000 --- a/samples/imagen/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# 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/samples/imagen/src/main/java/com/android/ai/samples/imagen/data/ImagenDataSource.kt b/samples/imagen/src/main/java/com/android/ai/samples/imagen/data/ImagenDataSource.kt deleted file mode 100644 index 6b42259a..00000000 --- a/samples/imagen/src/main/java/com/android/ai/samples/imagen/data/ImagenDataSource.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * 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. - */ -package com.android.ai.samples.imagen.data - -import android.graphics.Bitmap -import com.google.firebase.Firebase -import com.google.firebase.ai.ai -import com.google.firebase.ai.type.GenerativeBackend -import com.google.firebase.ai.type.ImagenAspectRatio -import com.google.firebase.ai.type.ImagenGenerationConfig -import com.google.firebase.ai.type.ImagenImageFormat -import com.google.firebase.ai.type.PublicPreviewAPI -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ImagenDataSource @Inject constructor() { - @OptIn(PublicPreviewAPI::class) - private val imagenModel = Firebase.ai(backend = GenerativeBackend.vertexAI()).imagenModel( - modelName = "imagen-4.0-generate-001", - generationConfig = ImagenGenerationConfig( - numberOfImages = 1, - aspectRatio = ImagenAspectRatio.SQUARE_1x1, - imageFormat = ImagenImageFormat.jpeg(compressionQuality = 75), - ), - ) - - @OptIn(PublicPreviewAPI::class) - suspend fun generateImage(prompt: String): Bitmap { - val imageResponse = imagenModel.generateImages( - prompt = prompt, - ) - val image = imageResponse.images.first() - return image.asBitmap() - } -} diff --git a/samples/imagen/src/main/java/com/android/ai/samples/imagen/ui/GeneratedContent.kt b/samples/imagen/src/main/java/com/android/ai/samples/imagen/ui/GeneratedContent.kt deleted file mode 100644 index 7248170d..00000000 --- a/samples/imagen/src/main/java/com/android/ai/samples/imagen/ui/GeneratedContent.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * 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. - */ -package com.android.ai.samples.imagen.ui - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.material3.Card -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.asImageBitmap -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import com.android.ai.samples.imagen.R - -@Composable -fun GeneratedContent(uiState: ImagenUIState, modifier: Modifier = Modifier) { - Card( - modifier = modifier, - ) { - when (uiState) { - ImagenUIState.Initial -> { - // - } - - ImagenUIState.Loading -> { - // - } - - is ImagenUIState.ImageGenerated -> { - Image( - bitmap = uiState.bitmap.asImageBitmap(), - contentDescription = uiState.contentDescription, - contentScale = ContentScale.Fit, - modifier = Modifier.fillMaxSize(), - ) - } - - is ImagenUIState.Error -> { - Text( - text = uiState.message ?: stringResource(R.string.error_message_unknown), - modifier = Modifier - .fillMaxSize() - .wrapContentSize(Alignment.Center), - textAlign = TextAlign.Center, - ) - } - } - } -} diff --git a/samples/imagen/src/main/java/com/android/ai/samples/imagen/ui/GenerationInput.kt b/samples/imagen/src/main/java/com/android/ai/samples/imagen/ui/GenerationInput.kt deleted file mode 100644 index f124ed66..00000000 --- a/samples/imagen/src/main/java/com/android/ai/samples/imagen/ui/GenerationInput.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * 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. - */ -package com.android.ai.samples.imagen.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.SmartToy -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.unit.dp -import com.android.ai.samples.imagen.R - -@Composable -fun GenerationInput(onGenerateClick: (String) -> Unit, enabled: Boolean, modifier: Modifier = Modifier) { - val placeholder = stringResource(R.string.placeholder_prompt) - var textFieldValue by rememberSaveable { mutableStateOf(placeholder) } - - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = modifier, - ) { - TextField( - value = textFieldValue, - onValueChange = { textFieldValue = it }, - label = { Text(stringResource(R.string.prompt_label)) }, - modifier = Modifier.fillMaxWidth(), - enabled = enabled, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), - keyboardActions = KeyboardActions( - onSend = { - onGenerateClick(textFieldValue) - }, - ), - ) - Button( - onClick = { - onGenerateClick(textFieldValue) - }, - enabled = enabled, - contentPadding = ButtonDefaults.ButtonWithIconContentPadding, - modifier = Modifier.fillMaxWidth(), - ) { - Icon( - Icons.Default.SmartToy, - contentDescription = null, - modifier = Modifier.size(ButtonDefaults.IconSize), - ) - Spacer(Modifier.size(ButtonDefaults.IconSpacing)) - Text(text = stringResource(R.string.generate_button)) - } - } -} diff --git a/samples/nanobanana/.gitignore b/samples/nanobanana/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/samples/nanobanana/.gitignore @@ -0,0 +1 @@ +/build diff --git a/samples/nanobanana/README.md b/samples/nanobanana/README.md new file mode 100644 index 00000000..dd4a4f20 --- /dev/null +++ b/samples/nanobanana/README.md @@ -0,0 +1,27 @@ +# Nanobanana Image Generation Sample + +This sample is part of the [AI Sample Catalog](../../). To build and run this sample, you should clone the entire repository. + +## Description + +This sample demonstrates how to generate images from text prompts using the Gemini 3.1 Flash Image model (a.k.a. "Nano Banana"). Users can input a text description, and the generative model will create an image based on that prompt, showcasing the power of text-to-image generation with Gemini. + +
+Nanobanana Image Generation in action +
+ +## How it works + +The application uses the Firebase AI SDK (see [How to run](../../#how-to-run)) for Android to interact with Gemini. The core logic is in the [`NanobananaDataSource.kt`](./src/main/java/com/android/ai/samples/nanobanana/data/NanobananaDataSource.kt) file. A `generativeModel` is initialized with specific configurations. When a user provides a text prompt, it's passed to the `generateImage` method, which returns the generated image as a bitmap. + +Here is the key snippet of code that calls the generative model from [`NanobananaDataSource.kt`](./src/main/java/com/android/ai/samples/nanobanana/data/NanobananaDataSource.kt): + +```kotlin +suspend fun generateImage(prompt: String): Bitmap { + val response = generativeModel.generateContent(prompt) + return response.candidates.firstOrNull()?.content?.parts?.firstNotNullOfOrNull { it.asImageOrNull() } + ?: throw Exception("No image generated") +} +``` + +Read more about [Gemini](https://developer.android.com/ai/gemini) in the Android Documentation. diff --git a/samples/imagen/build.gradle.kts b/samples/nanobanana/build.gradle.kts similarity index 97% rename from samples/imagen/build.gradle.kts rename to samples/nanobanana/build.gradle.kts index b2ac122a..9a56b458 100644 --- a/samples/imagen/build.gradle.kts +++ b/samples/nanobanana/build.gradle.kts @@ -21,7 +21,7 @@ plugins { } android { - namespace = "com.android.ai.samples.imagen" + namespace = "com.android.ai.samples.nanobanana" compileSdk = 35 buildFeatures { diff --git a/samples/imagen-editing/consumer-rules.pro b/samples/nanobanana/consumer-rules.pro similarity index 100% rename from samples/imagen-editing/consumer-rules.pro rename to samples/nanobanana/consumer-rules.pro diff --git a/samples/imagen/consumer-rules.pro b/samples/nanobanana/proguard-rules.pro similarity index 100% rename from samples/imagen/consumer-rules.pro rename to samples/nanobanana/proguard-rules.pro diff --git a/samples/nanobanana/src/main/java/com/android/ai/samples/nanobanana/data/NanobananaDataSource.kt b/samples/nanobanana/src/main/java/com/android/ai/samples/nanobanana/data/NanobananaDataSource.kt new file mode 100644 index 00000000..1408c6d3 --- /dev/null +++ b/samples/nanobanana/src/main/java/com/android/ai/samples/nanobanana/data/NanobananaDataSource.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * 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. + */ +package com.android.ai.samples.nanobanana.data + +import android.graphics.Bitmap +import com.google.firebase.Firebase +import com.google.firebase.ai.ai +import com.google.firebase.ai.type.GenerativeBackend +import com.google.firebase.ai.type.ResponseModality +import com.google.firebase.ai.type.asImageOrNull +import com.google.firebase.ai.type.generationConfig +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NanobananaDataSource @Inject constructor() { + private val generativeModel by lazy { + Firebase.ai(backend = GenerativeBackend.googleAI()).generativeModel( + modelName = "gemini-3.1-flash-image-preview", + generationConfig = generationConfig { + responseModalities = listOf(ResponseModality.IMAGE) + } + ) + } + + suspend fun generateImage(prompt: String): Bitmap { + val response = generativeModel.generateContent(prompt) + return response.candidates.firstOrNull()?.content?.parts?.firstNotNullOfOrNull { it.asImageOrNull() } + ?: throw Exception("No image generated") + } +} diff --git a/samples/imagen/src/main/java/com/android/ai/samples/imagen/ui/ImagenScreen.kt b/samples/nanobanana/src/main/java/com/android/ai/samples/nanobanana/ui/NanobananaScreen.kt similarity index 87% rename from samples/imagen/src/main/java/com/android/ai/samples/imagen/ui/ImagenScreen.kt rename to samples/nanobanana/src/main/java/com/android/ai/samples/nanobanana/ui/NanobananaScreen.kt index 10afed0e..89e3814b 100644 --- a/samples/imagen/src/main/java/com/android/ai/samples/imagen/ui/ImagenScreen.kt +++ b/samples/nanobanana/src/main/java/com/android/ai/samples/nanobanana/ui/NanobananaScreen.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.ai.samples.imagen.ui +package com.android.ai.samples.nanobanana.ui import android.graphics.BitmapFactory import android.widget.Toast @@ -57,7 +57,7 @@ import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.android.ai.samples.imagen.R +import com.android.ai.samples.nanobanana.R import com.android.ai.theme.AISampleCatalogTheme import com.android.ai.uicomponent.GenerateButton import com.android.ai.uicomponent.SampleDetailTopAppBar @@ -65,14 +65,14 @@ import com.android.ai.uicomponent.TextInput @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable -fun ImagenScreen(viewModel: ImagenViewModel = hiltViewModel()) { - val uiState: ImagenUIState by viewModel.uiState.collectAsStateWithLifecycle() +fun NanobananaScreen(viewModel: NanobananaViewModel = hiltViewModel()) { + val uiState: NanobananaUIState by viewModel.uiState.collectAsStateWithLifecycle() - if (uiState is ImagenUIState.Error) { - Toast.makeText(LocalContext.current, (uiState as ImagenUIState.Error).message, Toast.LENGTH_SHORT).show() + if (uiState is NanobananaUIState.Error) { + Toast.makeText(LocalContext.current, (uiState as NanobananaUIState.Error).message, Toast.LENGTH_SHORT).show() } - ImagenScreen( + NanobananaScreen( uiState = uiState, onGenerateClick = viewModel::generateImage, ) @@ -80,16 +80,16 @@ fun ImagenScreen(viewModel: ImagenViewModel = hiltViewModel()) { @Composable @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) -private fun ImagenScreen(uiState: ImagenUIState, onGenerateClick: (String) -> Unit) { - val isGenerating = uiState is ImagenUIState.Loading +private fun NanobananaScreen(uiState: NanobananaUIState, onGenerateClick: (String) -> Unit) { + val isGenerating = uiState is NanobananaUIState.Loading val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher Scaffold( containerColor = MaterialTheme.colorScheme.surface, topBar = { SampleDetailTopAppBar( - sampleName = stringResource(R.string.title_image_generation_screen), - sampleDescription = stringResource(R.string.subtitle_image_generation_screen), - sourceCodeUrl = "https://github.com/android/ai-samples/tree/main/samples/imagen", + sampleName = stringResource(R.string.title_nanobanana_screen), + sampleDescription = stringResource(R.string.subtitle_nanobanana_screen), + sourceCodeUrl = "https://github.com/android/ai-samples/tree/main/samples/nanobanana", onBackClick = { backDispatcher?.onBackPressed() }, ) }, @@ -133,13 +133,13 @@ private fun ImagenScreen(uiState: ImagenUIState, onGenerateClick: (String) -> Un ) { when (uiState) { - is ImagenUIState.ImageGenerated -> Image( + is NanobananaUIState.ImageGenerated -> Image( bitmap = uiState.bitmap.asImageBitmap(), contentDescription = uiState.contentDescription, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize(), ) - ImagenUIState.Loading -> { + NanobananaUIState.Loading -> { ContainedLoadingIndicator( modifier = Modifier.size(60.dp) .align(Alignment.Center), @@ -182,10 +182,10 @@ private fun ImagenScreen(uiState: ImagenUIState, onGenerateClick: (String) -> Un @PreviewScreenSizes @Composable @OptIn(ExperimentalMaterial3Api::class) -private fun ImagenScreenPreview() { +private fun NanobananaScreenPreview() { AISampleCatalogTheme { - ImagenScreen( - uiState = ImagenUIState.Initial, + NanobananaScreen( + uiState = NanobananaUIState.Initial, onGenerateClick = {}, ) } diff --git a/samples/imagen/src/main/java/com/android/ai/samples/imagen/ui/ImagenUIState.kt b/samples/nanobanana/src/main/java/com/android/ai/samples/nanobanana/ui/NanobananaUIState.kt similarity index 74% rename from samples/imagen/src/main/java/com/android/ai/samples/imagen/ui/ImagenUIState.kt rename to samples/nanobanana/src/main/java/com/android/ai/samples/nanobanana/ui/NanobananaUIState.kt index 3bca1610..15be384a 100644 --- a/samples/imagen/src/main/java/com/android/ai/samples/imagen/ui/ImagenUIState.kt +++ b/samples/nanobanana/src/main/java/com/android/ai/samples/nanobanana/ui/NanobananaUIState.kt @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.ai.samples.imagen.ui +package com.android.ai.samples.nanobanana.ui import android.graphics.Bitmap -sealed interface ImagenUIState { - data object Initial : ImagenUIState - data object Loading : ImagenUIState +sealed interface NanobananaUIState { + data object Initial : NanobananaUIState + data object Loading : NanobananaUIState data class ImageGenerated( val bitmap: Bitmap, val contentDescription: String, - ) : ImagenUIState - data class Error(val message: String?) : ImagenUIState + ) : NanobananaUIState + data class Error(val message: String?) : NanobananaUIState } diff --git a/samples/imagen/src/main/java/com/android/ai/samples/imagen/ui/ImagenViewModel.kt b/samples/nanobanana/src/main/java/com/android/ai/samples/nanobanana/ui/NanobananaViewModel.kt similarity index 60% rename from samples/imagen/src/main/java/com/android/ai/samples/imagen/ui/ImagenViewModel.kt rename to samples/nanobanana/src/main/java/com/android/ai/samples/nanobanana/ui/NanobananaViewModel.kt index b0f77d08..5e65ff05 100644 --- a/samples/imagen/src/main/java/com/android/ai/samples/imagen/ui/ImagenViewModel.kt +++ b/samples/nanobanana/src/main/java/com/android/ai/samples/nanobanana/ui/NanobananaViewModel.kt @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.ai.samples.imagen.ui +package com.android.ai.samples.nanobanana.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.android.ai.samples.imagen.data.ImagenDataSource +import com.android.ai.samples.nanobanana.data.NanobananaDataSource import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -25,20 +25,20 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @HiltViewModel -class ImagenViewModel @Inject constructor(private val imagenDataSource: ImagenDataSource) : ViewModel() { +class NanobananaViewModel @Inject constructor(private val nanobananaDataSource: NanobananaDataSource) : ViewModel() { - private val _uiState: MutableStateFlow = MutableStateFlow(ImagenUIState.Initial) - val uiState: StateFlow = _uiState + private val _uiState: MutableStateFlow = MutableStateFlow(NanobananaUIState.Initial) + val uiState: StateFlow = _uiState fun generateImage(prompt: String) { - _uiState.value = ImagenUIState.Loading + _uiState.value = NanobananaUIState.Loading viewModelScope.launch { try { - val bitmap = imagenDataSource.generateImage(prompt) - _uiState.value = ImagenUIState.ImageGenerated(bitmap, contentDescription = prompt) + val bitmap = nanobananaDataSource.generateImage(prompt) + _uiState.value = NanobananaUIState.ImageGenerated(bitmap, contentDescription = prompt) } catch (e: Exception) { - _uiState.value = ImagenUIState.Error(e.message) + _uiState.value = NanobananaUIState.Error(e.message) } } } diff --git a/samples/imagen/src/main/res/values/strings.xml b/samples/nanobanana/src/main/res/values/strings.xml similarity index 55% rename from samples/imagen/src/main/res/values/strings.xml rename to samples/nanobanana/src/main/res/values/strings.xml index 9edf57df..5f70ad9a 100644 --- a/samples/imagen/src/main/res/values/strings.xml +++ b/samples/nanobanana/src/main/res/values/strings.xml @@ -16,13 +16,8 @@ --> - See Code - An oil painting of Alcatraz - Imagen image generation - Generate images with Imagen, Google image generation model - Generate - Generating… - Prompt - Enter a prompt and tap \"Generate\" to generate an image + A yellow banana on a blue background + Nanobanana image generation + Generate images with Nanobanana, Google image generation model Unknown error - \ No newline at end of file + diff --git a/settings.gradle.kts b/settings.gradle.kts index 74adde7f..14bffd8c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -43,12 +43,11 @@ include(":samples:gemini-chatbot") include(":samples:genai-summarization") include(":samples:genai-writing-assistance") include(":samples:genai-image-description") -include(":samples:imagen") -include(":samples:imagen-editing") include(":samples:magic-selfie") include(":samples:gemini-video-summarization") include(":samples:gemini-live-todo") include(":samples:gemini-video-metadata-creation") include(":samples:gemini-image-chat") include(":samples:gemini-hybrid") +include(":samples:nanobanana") include(":ui-component")