Skip to content

Commit 95bd007

Browse files
video-summarization-fixes (#52)
(1) Improved State Management: The outputText state in VideoSummarizationScreen.kt has been refactored to use a more robust OutputTextState sealed class and derivedStateOf. This allows for better handling of different states (e.g., success, loading, error) and more reactive UI updates, such as conditionally showing the 'Listen' button only when output text is successfully generated. (2) Video List Refactoring: The hardcoded list of sample videos has been refactored from an instantiated VideoList class to a top-level sampleVideoList constant in VideoList.kt. This simplifies access and aligns with common Kotlin practices for static data. (3) Linting and Code Cleanup: Various minor lint fixes and code cleanups have been applied across the codebase. This includes adopting the androidx.core.net.toUri() extension function for URI creation, optimizing variable declarations (e.g., using val instead of var where appropriate), removing unused state variables, and deleting outdated comment blocks. (4) Text-to-Speech Logic Fix: A fix was implemented in TextToSpeechControls.kt to correctly check the Text-to-Speech language support by using textToSpeech?.voice?.locale instead of the less precise textToSpeech?.language
1 parent 0a7e52b commit 95bd007

File tree

6 files changed

+93
-99
lines changed

6 files changed

+93
-99
lines changed

ai-catalog/samples/gemini-video-summarization/src/main/java/com/android/ai/samples/geminivideosummary/VideoSummarizationScreen.kt

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,16 @@ import androidx.compose.ui.platform.LocalContext
4848
import androidx.compose.ui.res.stringResource
4949
import androidx.compose.ui.unit.dp
5050
import androidx.compose.ui.unit.sp
51+
import androidx.core.net.toUri
5152
import androidx.hilt.navigation.compose.hiltViewModel
5253
import androidx.media3.common.MediaItem
5354
import androidx.media3.exoplayer.ExoPlayer
5455
import com.android.ai.samples.geminivideosummary.player.VideoPlayer
5556
import com.android.ai.samples.geminivideosummary.player.VideoSelectionDropdown
5657
import com.android.ai.samples.geminivideosummary.ui.OutputTextDisplay
5758
import com.android.ai.samples.geminivideosummary.ui.TextToSpeechControls
58-
import com.android.ai.samples.geminivideosummary.util.VideoList
59+
import com.android.ai.samples.geminivideosummary.util.sampleVideoList
60+
import com.android.ai.samples.geminivideosummary.viewmodel.OutputTextState
5961
import com.android.ai.samples.geminivideosummary.viewmodel.VideoSummarizationViewModel
6062
import com.google.com.android.ai.samples.geminivideosummary.R
6163
import java.util.Locale
@@ -71,18 +73,17 @@ import java.util.Locale
7173
fun VideoSummarizationScreen(viewModel: VideoSummarizationViewModel = hiltViewModel()) {
7274

7375
val context = LocalContext.current
74-
val videoList = VideoList().videos
75-
var selectedVideoUri by remember { mutableStateOf<Uri?>(videoList.first().uri) }
76+
var selectedVideoUri by remember { mutableStateOf<Uri?>(sampleVideoList.first().uri) }
7677
var isDropdownExpanded by remember { mutableStateOf(false) }
77-
var newVideoUrl by remember { mutableStateOf("") }
78-
val outputText by viewModel.outputText.collectAsState()
78+
val outputTextState by viewModel.outputText.collectAsState()
79+
val showListenButton by remember { mutableStateOf(outputTextState is OutputTextState.Success) }
7980
var textForSpeech by remember { mutableStateOf("") }
8081
var textToSpeech: TextToSpeech? by remember { mutableStateOf(null) }
8182
var isInitialized by remember { mutableStateOf(false) }
8283
var isSpeaking by remember { mutableStateOf(false) }
8384
var isPaused by remember { mutableStateOf(false) }
8485

85-
val videoOptions = videoList.map { it.title to it.uri }
86+
val videoOptions = sampleVideoList
8687

8788
val exoPlayer = remember(context) {
8889
ExoPlayer.Builder(context).build().apply {
@@ -151,11 +152,6 @@ fun VideoSummarizationScreen(viewModel: VideoSummarizationViewModel = hiltViewMo
151152
selectedVideoUri = uri
152153
viewModel.clearOutputText()
153154
},
154-
onNewVideoUrlChanged = { url ->
155-
run {
156-
newVideoUrl = url
157-
}
158-
},
159155
onDropdownExpanded = { expanded ->
160156
isDropdownExpanded = expanded
161157
},
@@ -167,20 +163,27 @@ fun VideoSummarizationScreen(viewModel: VideoSummarizationViewModel = hiltViewMo
167163

168164
Spacer(modifier = Modifier.height(16.dp))
169165

170-
Button(modifier = Modifier.fillMaxWidth(), onClick = {
171-
onSummarizeButtonClick(
172-
selectedVideoUri, textToSpeech, onSpeakingStateChange = { speaking, paused ->
173-
isSpeaking = speaking
174-
isPaused = paused
175-
}, viewModel,
176-
)
177-
}) {
166+
Button(
167+
modifier = Modifier.fillMaxWidth(),
168+
onClick = {
169+
onSummarizeButtonClick(
170+
selectedVideoUri, textToSpeech,
171+
onSpeakingStateChange = { speaking, paused ->
172+
isSpeaking = speaking
173+
isPaused = paused
174+
},
175+
viewModel,
176+
)
177+
},
178+
) {
178179
Text(stringResource(R.string.summarize_video_button))
179180
}
180181
Spacer(modifier = Modifier.height(16.dp))
181182

182-
if (outputText.toString().isNotEmpty()) {
183-
textForSpeech = outputText.toString()
183+
if (showListenButton) {
184+
if (outputTextState is OutputTextState.Success) {
185+
textForSpeech = (outputTextState as OutputTextState.Success).text
186+
}
184187

185188
Spacer(modifier = Modifier.height(8.dp))
186189

@@ -208,7 +211,7 @@ fun VideoSummarizationScreen(viewModel: VideoSummarizationViewModel = hiltViewMo
208211

209212
Spacer(modifier = Modifier.height(16.dp))
210213

211-
OutputTextDisplay(outputText = outputText, modifier = Modifier.weight(1f))
214+
OutputTextDisplay(outputTextState, modifier = Modifier.weight(1f))
212215

213216
Spacer(modifier = Modifier.height(16.dp))
214217
}
@@ -227,13 +230,9 @@ fun onSelectedVideoChange(
227230
textToSpeech: TextToSpeech?,
228231
onSpeakingStateChange: (speaking: Boolean, paused: Boolean) -> Unit,
229232
) {
230-
if (selectedVideoUri != null) {
231-
if (selectedVideoUri == Uri.parse("")) {
232-
// do nothing
233-
} else {
234-
exoPlayer.setMediaItem(MediaItem.fromUri(selectedVideoUri))
235-
exoPlayer.prepare()
236-
}
233+
selectedVideoUri?.takeIf { it.toString().isNotEmpty() }?.let { uri ->
234+
exoPlayer.setMediaItem(MediaItem.fromUri(uri))
235+
exoPlayer.prepare()
237236
textToSpeech?.stop()
238237
onSpeakingStateChange(false, true)
239238
}
@@ -253,8 +252,7 @@ fun onSummarizeButtonClick(
253252
}
254253

255254
fun initializeTextToSpeech(context: Context, onInitialized: (Boolean) -> Unit): TextToSpeech {
256-
var textToSpeech: TextToSpeech? = null
257-
textToSpeech = TextToSpeech(context) { status ->
255+
val textToSpeech = TextToSpeech(context) { status ->
258256
if (status == TextToSpeech.SUCCESS) {
259257
onInitialized(true)
260258
} else {
@@ -269,10 +267,12 @@ fun initializeTextToSpeech(context: Context, onInitialized: (Boolean) -> Unit):
269267
fun SeeCodeButton(context: Context) {
270268
val githubLink =
271269
"https://github.com/android/ai-samples/tree/main/ai-catalog/samples/gemini-video-summarization"
272-
Button(onClick = {
273-
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(githubLink))
274-
context.startActivity(intent)
275-
}) {
270+
Button(
271+
onClick = {
272+
val intent = Intent(Intent.ACTION_VIEW, githubLink.toUri())
273+
context.startActivity(intent)
274+
},
275+
) {
276276
Icon(Icons.Filled.Code, contentDescription = "See code")
277277
Text(
278278
modifier = Modifier.padding(start = 8.dp),

ai-catalog/samples/gemini-video-summarization/src/main/java/com/android/ai/samples/geminivideosummary/player/VideoPlayer.kt

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,6 @@ import androidx.compose.ui.viewinterop.AndroidView
2424
import androidx.media3.exoplayer.ExoPlayer
2525
import androidx.media3.ui.PlayerView
2626

27-
/*
28-
* Copyright 2024 Google LLC
29-
*
30-
* Licensed under the Apache License, Version 2.0 (the "License");
31-
* you may not use this file except in compliance with the License.
32-
* You may obtain a copy of the License at
33-
*
34-
* https://www.apache.org/licenses/LICENSE-2.0
35-
*
36-
* Unless required by applicable law or agreed to in writing, software
37-
* distributed under the License is distributed on an "AS IS" BASIS,
38-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
39-
* See the License for the specific language governing permissions and
40-
* limitations under the License.
41-
*/
42-
4327
/*
4428
* A Composable function that displays video using ExoPlayer within a PlayerView in Jetpack Compose.
4529
*/

ai-catalog/samples/gemini-video-summarization/src/main/java/com/android/ai/samples/geminivideosummary/player/VideoSelectionDropdown.kt

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ import androidx.compose.material3.OutlinedTextField
2828
import androidx.compose.material3.Text
2929
import androidx.compose.runtime.Composable
3030
import androidx.compose.ui.Modifier
31+
import androidx.compose.ui.res.stringResource
32+
import com.android.ai.samples.geminivideosummary.util.VideoItem
33+
import com.google.com.android.ai.samples.geminivideosummary.R
3134

3235
/**
3336
* A composable function that displays a dropdown menu for selecting a video from a list of options.
@@ -36,22 +39,21 @@ import androidx.compose.ui.Modifier
3639
fun VideoSelectionDropdown(
3740
selectedVideoUri: Uri?,
3841
isDropdownExpanded: Boolean,
39-
videoOptions: List<Pair<String, Uri>>,
42+
videoOptions: List<VideoItem>,
4043
onVideoUriSelected: (Uri) -> Unit,
41-
onNewVideoUrlChanged: (String) -> Unit,
4244
onDropdownExpanded: (Boolean) -> Unit,
4345
) {
4446
Box {
4547
OutlinedTextField(
4648
value = selectedVideoUri?.let {
47-
videoOptions.firstOrNull { it.second == selectedVideoUri }?.first
48-
} ?: "Select Video",
49+
videoOptions.firstOrNull { videoItem -> videoItem.uri == selectedVideoUri }?.let { stringResource(it.titleResId) }
50+
} ?: stringResource(R.string.select_video_placeholder),
4951
onValueChange = { },
5052
readOnly = true,
5153
trailingIcon = {
5254
Icon(
5355
imageVector = Icons.Filled.ArrowDropDown,
54-
contentDescription = "Dropdown",
56+
contentDescription = stringResource(R.string.dropdown_content_description),
5557
modifier = Modifier.clickable { onDropdownExpanded(!isDropdownExpanded) },
5658
)
5759
},
@@ -64,11 +66,10 @@ fun VideoSelectionDropdown(
6466
onDismissRequest = { onDropdownExpanded(false) },
6567
modifier = Modifier.fillMaxWidth(),
6668
) {
67-
videoOptions.forEach { (label, uri) ->
68-
DropdownMenuItem(text = { Text(label) }, onClick = {
69-
onVideoUriSelected(uri)
69+
videoOptions.forEach { videoItem ->
70+
DropdownMenuItem(text = { Text(stringResource(videoItem.titleResId)) }, onClick = {
71+
onVideoUriSelected(videoItem.uri)
7072
onDropdownExpanded(false)
71-
onNewVideoUrlChanged("")
7273
})
7374
}
7475
}

ai-catalog/samples/gemini-video-summarization/src/main/java/com/android/ai/samples/geminivideosummary/ui/TextToSpeechControls.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ private fun handleSpeakButtonClick(
122122
onSpeakingStateChange: (speaking: Boolean, paused: Boolean) -> Unit,
123123
) {
124124
// Check if the voice and language is supported
125-
val result = textToSpeech?.language?.let {
125+
val result = textToSpeech?.voice?.locale?.let {
126126
textToSpeech.setLanguage(selectedAccent)
127127
}
128128
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {

ai-catalog/samples/gemini-video-summarization/src/main/java/com/android/ai/samples/geminivideosummary/util/VideoList.kt

Lines changed: 34 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,47 +16,44 @@
1616
package com.android.ai.samples.geminivideosummary.util
1717

1818
import android.net.Uri
19-
20-
/**
21-
* Class containing a list of hardcoded video URIs and their titles.
22-
*/
23-
class VideoList {
24-
val videos = listOf(
25-
VideoItem(
26-
"Big Buck Bunny",
27-
Uri.parse("https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"),
28-
),
29-
VideoItem(
30-
"Android Spotlight Week (Shorts video)",
31-
Uri.parse("https://storage.googleapis.com/exoplayer-test-media-0/shorts_android_developers/shorts_10.mp4"),
32-
),
33-
VideoItem(
34-
"Rio De Janerio",
35-
Uri.parse("gs://cloud-samples-data/generative-ai/video/rio_de_janeiro_beyond_the_map_rio.mp4"),
36-
),
37-
VideoItem(
38-
"Youtube Link (On Device Watch Next with Google TV)",
39-
Uri.parse("https://www.youtube.com/watch?v=QFMIP5GOo70"),
40-
),
41-
VideoItem(
42-
"Tears of Steel",
43-
Uri.parse("https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4"),
44-
),
45-
VideoItem(
46-
"For Bigger Blazes",
47-
Uri.parse("https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4"),
48-
),
49-
VideoItem(
50-
"For Bigger Escape",
51-
Uri.parse("https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4"),
52-
),
53-
)
54-
}
19+
import androidx.core.net.toUri
20+
import com.google.com.android.ai.samples.geminivideosummary.R
5521

5622
/**
5723
* Data class to represent a video item with a title and URI.
5824
*/
5925
data class VideoItem(
60-
val title: String,
26+
val titleResId: Int,
6127
val uri: Uri,
6228
)
29+
30+
val sampleVideoList = listOf(
31+
VideoItem(
32+
R.string.video_title_big_buck_bunny,
33+
"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4".toUri(),
34+
),
35+
VideoItem(
36+
R.string.video_title_android_spotlight_shorts,
37+
"https://storage.googleapis.com/exoplayer-test-media-0/shorts_android_developers/shorts_10.mp4".toUri(),
38+
),
39+
VideoItem(
40+
R.string.video_title_rio_de_janeiro,
41+
"gs://cloud-samples-data/generative-ai/video/rio_de_janeiro_beyond_the_map_rio.mp4".toUri(),
42+
),
43+
VideoItem(
44+
R.string.video_title_youtube_google_tv,
45+
"https://www.youtube.com/watch?v=QFMIP5GOo70".toUri(),
46+
),
47+
VideoItem(
48+
R.string.video_title_tears_of_steel,
49+
"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4".toUri(),
50+
),
51+
VideoItem(
52+
R.string.video_title_for_bigger_blazes,
53+
"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4".toUri(),
54+
),
55+
VideoItem(
56+
R.string.video_title_for_bigger_escape,
57+
"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4".toUri(),
58+
),
59+
)

ai-catalog/samples/gemini-video-summarization/src/main/res/values/strings.xml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
<?xml version="1.0" encoding="utf-8"?>
21
<resources>
2+
<string name="app_name">Gemini Video Summary</string>
3+
<string name="select_video_placeholder">Select Video</string>
34
<string name="summarize_video_button">Summarize Video</string>
45
<string name="video_summarization_title">Video summarization</string>
56
<string name="output_text_combined">%s%s</string>
@@ -9,4 +10,15 @@
910
<string name="text_listen_to_ai_output">Listen to AI output</string>
1011
<string name="pause">Pause</string>
1112
<string name="see_code">See Code</string>
13+
<string name="dropdown_content_description">Dropdown for selecting a video</string>
14+
15+
<!--Video titles for list of sample videos-->
16+
<string name="video_title_big_buck_bunny">Big Buck Bunny</string>
17+
<string name="video_title_android_spotlight_shorts">Android Spotlight Week (Shorts video)</string>
18+
<string name="video_title_rio_de_janeiro">Rio De Janerio</string>
19+
<string name="video_title_youtube_google_tv">Youtube Link (On Device Watch Next with Google TV)</string>
20+
<string name="video_title_tears_of_steel">Tears of Steel</string>
21+
<string name="video_title_for_bigger_blazes">For Bigger Blazes</string>
22+
<string name="video_title_for_bigger_escape">For Bigger Escape</string>
23+
1224
</resources>

0 commit comments

Comments
 (0)