Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
8f853dc
Create a new video-metatdata-creation-sample with Gemini, Firebase an…
MayuriKhinvasara Aug 11, 2025
5a3d33a
Create a new video-metatdata-creation-sample with Gemini, Firebase an…
MayuriKhinvasara Aug 11, 2025
6331d80
Create a new video-metatdata-creation-sample with Gemini, Firebase an…
MayuriKhinvasara Aug 11, 2025
688c239
Create a new video-metatdata-creation-sample with Gemini, Firebase an…
MayuriKhinvasara Aug 11, 2025
58941a1
Create a new video-metatdata-creation-sample with Gemini, Firebase an…
MayuriKhinvasara Aug 11, 2025
91ca1d4
Create a new video-metatdata-creation-sample with Gemini, Firebase an…
MayuriKhinvasara Aug 11, 2025
7995179
Merge remote-tracking branch 'origin/video-metadata-creation' into vi…
MayuriKhinvasara Aug 11, 2025
64e58d2
Merge remote-tracking branch 'origin/video-metadata-creation' into vi…
MayuriKhinvasara Aug 11, 2025
f7d8d84
Merge remote-tracking branch 'origin/video-metadata-creation' into vi…
MayuriKhinvasara Aug 11, 2025
8aadc1c
🤖 Apply Spotless formatting
MayuriKhinvasara Aug 11, 2025
8eb2692
Upgrade video metadata sample to use Gemini 2.5 Flash.
MayuriKhinvasara Aug 13, 2025
b820bff
Migrate VideoPlayer from Android Views to use new Media3 Compose UI
MayuriKhinvasara Aug 13, 2025
5d74ebf
Reset metadata on video change and update description prompt
MayuriKhinvasara Aug 14, 2025
b1ca497
Merge branch 'main' into video-metadata-creation
MayuriKhinvasara Aug 14, 2025
b1ec586
🤖 Apply Spotless formatting
MayuriKhinvasara Aug 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ai-catalog/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ dependencies {
implementation(project(":samples:magic-selfie"))
implementation(project(":samples:gemini-video-summarization"))
implementation(project(":samples:gemini-live-todo"))
implementation(project(":samples:gemini-video-metadata-creation"))

testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.android.ai.catalog.R
import com.android.ai.samples.geminichatbot.GeminiChatbotScreen
import com.android.ai.samples.geminilivetodo.ui.TodoScreen
import com.android.ai.samples.geminimultimodal.ui.GeminiMultimodalScreen
import com.android.ai.samples.geminivideometadatacreation.VideoMetadataCreationScreen
import com.android.ai.samples.geminivideosummary.VideoSummarizationScreen
import com.android.ai.samples.genai_image_description.GenAIImageDescriptionScreen
import com.android.ai.samples.genai_summarization.GenAISummarizationScreen
Expand Down Expand Up @@ -92,6 +93,14 @@ val sampleCatalog = listOf(
tags = listOf(SampleTags.GEMINI_2_0_FLASH, SampleTags.FIREBASE, SampleTags.MEDIA3),
needsFirebase = true,
),
SampleCatalogItem(
title = R.string.gemini_video_metadata_creation_sample_title,
description = R.string.gemini_video_metadata_creation_sample_description,
route = "VideoMetadataCreationScreen",
sampleEntryScreen = { VideoMetadataCreationScreen() },
tags = listOf(SampleTags.GEMINI_2_5_FLASH, SampleTags.FIREBASE, SampleTags.MEDIA3),
needsFirebase = true,
),
SampleCatalogItem(
title = R.string.gemini_live_todo_title,
description = R.string.gemini_live_todo_description,
Expand Down Expand Up @@ -120,6 +129,7 @@ enum class SampleTags(
) {
FIREBASE("Firebase", Color(0xFFFF9100), Color.White),
GEMINI_2_0_FLASH("Gemini 2.0 Flash", Color(0xFF4285F4), Color.White),
GEMINI_2_5_FLASH("Gemini 2.5 Flash", Color(0xFF4285F4), Color.White),
GEMINI_NANO("Gemini Nano", Color(0xFF7abafe), Color.White),
IMAGEN("Imagen", Color(0xFF7CB342), Color.White),
MEDIA3("Media3", Color(0xFF7CB584), Color.White),
Expand Down
4 changes: 3 additions & 1 deletion ai-catalog/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<string name="genai_image_description_sample_title">Image Description with Gemini Nano</string>
<string name="genai_image_description_sample_description">Generate short descriptions of images on-device with GenAI API powered by Gemini Nano</string>
<string name="genai_writing_assistance_sample_title">Polish text with Gemini Nano</string>
<string name="genai_writing_assistance_sample_description">Proofread and rewrite short content on-device with GenAI API powered by Gemini Nano</string>"
<string name="genai_writing_assistance_sample_description">Proofread and rewrite short content on-device with GenAI API powered by Gemini Nano</string>
<string name="top_bar_title">Android AI Samples</string>
<string name="open_sample_button">Open sample</string>
<string name="imagen_sample_title">Image generation with Imagen</string>
Expand All @@ -18,6 +18,8 @@
<string name="magic_selfie_sample_description">Change the background of your selfies with Imagen and the ML Kit Segmentation API</string>
<string name="gemini_video_summarization_sample_title">Video Summarization with Gemini and Firebase</string>
<string name="gemini_video_summarization_sample_description">"Generate a summary of a video (from a cloud URL or Youtube) with Gemini API powered by Firebase"</string>
<string name="gemini_video_metadata_creation_sample_title">Video Metadata Creation with Gemini and Firebase</string>
<string name="gemini_video_metadata_creation_sample_description">"Generate metadata of a video (from a cloud URL or Youtube) with Gemini API powered by Firebase"</string>
<string name="gemini_live_todo_title">Gemini Live Todo</string>
<string name="gemini_live_todo_description">"Simple Todo app using the Gemini Live API to interact with the items in the list"</string>
<string name="firebase_required">Firebase Required</string>
Expand Down
4 changes: 3 additions & 1 deletion ai-catalog/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ hiltNavigationCompose = "1.2.0"
ksp = "2.1.0-1.0.29"
runtimeLivedata = "1.7.6"
material3Android = "1.3.1"
media3 = "1.6.1"
media3 = "1.8.0"
firebaseCommonKtx = "21.0.0"
uiToolingPreviewAndroid = "1.8.1"
spotless = "7.0.4"
Expand Down Expand Up @@ -70,6 +70,8 @@ hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-com
androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" }
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3" }
androidx-media3-ui-compose = { module = "androidx.media3:media3-ui-compose", version.ref = "media3"}
androidx-media3-transformer = { module = "androidx.media3:media3-transformer", version.ref = "media3" }
androidx-ui-tooling-preview-android = { group = "androidx.compose.ui", name = "ui-tooling-preview-android", version.ref = "uiToolingPreviewAndroid" }
ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "uiToolingPreview" }
ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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)
alias(libs.plugins.hilt.plugin)
}

android {
namespace = "com.android.ai.samples.geminivideometadatacreation"
compileSdk = 36

defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

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"
}
buildFeatures {
compose = true
}
}

dependencies {

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.material.icons.extended)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.hilt.android)
implementation(libs.hilt.navigation.compose)
implementation(libs.androidx.material3.android)
implementation(libs.firebase.common.ktx)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.ui.tooling.preview.android)
ksp(libs.hilt.compiler)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.ai)

// Media3 ExoPlayer
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.ui)
implementation(libs.androidx.media3.transformer)
implementation(libs.androidx.media3.ui.compose)
implementation(libs.kotlinx.coroutines.guava)

androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/*
* 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.geminivideometadatacreation

import android.content.Intent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Code
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import com.android.ai.samples.geminivideometadatacreation.player.VideoPlayer
import com.android.ai.samples.geminivideometadatacreation.player.VideoSelectionDropdown
import com.android.ai.samples.geminivideometadatacreation.ui.ButtonGrid
import com.android.ai.samples.geminivideometadatacreation.ui.OutputTextDisplay
import com.android.ai.samples.geminivideometadatacreation.ui.ThumbnailScreen
import com.android.ai.samples.geminivideometadatacreation.util.sampleVideoList
import com.android.ai.samples.geminivideometadatacreation.viewmodel.MetadataCreationState
import com.android.ai.samples.geminivideometadatacreation.viewmodel.MetadataType
import com.android.ai.samples.geminivideometadatacreation.viewmodel.VideoMetadataCreationState
import com.android.ai.samples.geminivideometadatacreation.viewmodel.VideoMetadataCreationViewModel

/**
* Composable function for the AI Video Metadata Creation screen.
*
* This screen allows users to select a video, play it, and generate metadata of its content
* using Firebase AI. It also provides text-to-speech functionality to read out
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VideoMetadataCreationScreen(viewModel: VideoMetadataCreationViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
var isDropdownExpanded by remember { mutableStateOf(false) }

val exoPlayer = remember(context) {
ExoPlayer.Builder(context).build().apply {
playWhenReady = true
}
}

LaunchedEffect(uiState.selectedVideoUri) {
uiState.selectedVideoUri?.let {
exoPlayer.setMediaItem(MediaItem.fromUri(it))
exoPlayer.prepare()
}
}

Scaffold(
topBar = {
TopAppBar(
colors = topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.primary,
),
title = {
Text(text = stringResource(R.string.video_metadata_creation_title))
},
actions = {
SeeCodeButton()
},
)
},
) { innerPadding ->
Column(
modifier = Modifier
.padding(16.dp)
.padding(innerPadding)
.fillMaxHeight(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
VideoSelectionDropdown(
selectedVideoUri = uiState.selectedVideoUri,
isDropdownExpanded = isDropdownExpanded,
videoOptions = sampleVideoList,
onVideoUriSelected = { uri ->
viewModel.onVideoSelected(uri)
},
onDropdownExpanded = { isDropdownExpanded = it },
)

VideoPlayer(
player = exoPlayer,
modifier = Modifier
.fillMaxWidth()
.weight(0.25f),
)

MetadataCreationSection(
uiState = uiState,
onDismissError = { viewModel.dismissError() },
onMetadataTypeClicked = {
viewModel.onMetadataTypeSelected(it)
viewModel.createMetadata(it)
},
modifier = Modifier.weight(0.75f),
)
}
}

DisposableEffect(key1 = exoPlayer) {
onDispose {
exoPlayer.release()
}
}
}

@Composable
private fun MetadataCreationSection(
uiState: VideoMetadataCreationState,
onDismissError: () -> Unit,
onMetadataTypeClicked: (MetadataType) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier,
) {
ButtonGrid(
selectedMetadataType = uiState.selectedMetadataType,
onMetadataCreationClicked = onMetadataTypeClicked,
)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we reset the content of OutputTextDisplay when the user selects a different video?

Copy link
Contributor Author

@MayuriKhinvasara MayuriKhinvasara Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack. Fixed

when (val metadataCreationState = uiState.metadataCreationState) {
is MetadataCreationState.InProgress -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
}

is MetadataCreationState.Error -> {
AlertDialog(
onDismissRequest = onDismissError,
title = { Text("Error") },
text = { Text(metadataCreationState.message) },
confirmButton = {
Button(onClick = onDismissError) {
Text("OK")
}
},
)
}

is MetadataCreationState.Success -> {
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(16.dp)) {
OutputTextDisplay(metadataCreationState.metadataText)
ThumbnailScreen(thumbnailState = metadataCreationState.thumbnailState)
}
}

MetadataCreationState.Idle -> {
// Default state - No button is selected unless exp licitly selected
}
}
}
}

@Composable
fun SeeCodeButton() {
val context = LocalContext.current
val githubLink =
"https://github.com/android/ai-samples/tree/main/ai-catalog/samples/gemini-video-metadata-creation"
Button(
onClick = {
val intent = Intent(Intent.ACTION_VIEW, githubLink.toUri())
context.startActivity(intent)
},
) {
Icon(Icons.Filled.Code, contentDescription = "See code")
Text(
modifier = Modifier.padding(start = 8.dp),
fontSize = 12.sp,
text = stringResource(R.string.see_code),
)
}
}
Loading
Loading