From 3efb014dbbde434d24cf7059a3edcf6655f680eb Mon Sep 17 00:00:00 2001 From: Kristina Simakova Date: Thu, 18 Jun 2026 16:46:25 +0100 Subject: [PATCH 1/4] Add AI Video Clipper --- AI Video Clipper/app/.gitignore | 1 + AI Video Clipper/app/build.gradle.kts | 66 ++++ AI Video Clipper/app/google-services.json | 0 AI Video Clipper/app/proguard-rules.pro | 21 ++ .../videoediting/ExampleInstrumentedTest.kt | 24 ++ .../app/src/main/AndroidManifest.xml | 30 ++ .../videoediting/AIResponseJsonSchema.kt | 18 ++ .../videoediting/FirebaseStorageRepository.kt | 27 ++ .../example/videoediting/GeminiRepository.kt | 54 ++++ .../com/example/videoediting/GenAIParser.kt | 67 ++++ .../com/example/videoediting/MainActivity.kt | 30 ++ .../videoediting/MediaSelectionScreen.kt | 288 ++++++++++++++++++ .../videoediting/MediaSelectionViewModel.kt | 130 ++++++++ .../videoediting/VideoAnalysisResultScreen.kt | 197 ++++++++++++ .../videoediting/VideoAnalysisViewModel.kt | 126 ++++++++ .../example/videoediting/ui/theme/Color.kt | 11 + .../example/videoediting/ui/theme/Theme.kt | 47 +++ .../com/example/videoediting/ui/theme/Type.kt | 18 ++ .../res/drawable/ic_launcher_background.xml | 170 +++++++++++ .../res/drawable/ic_launcher_foreground.xml | 30 ++ .../main/res/drawable/rounded_pause_24.xml | 6 + .../res/drawable/rounded_play_arrow_24.xml | 6 + .../main/res/mipmap-anydpi/ic_launcher.xml | 6 + .../res/mipmap-anydpi/ic_launcher_round.xml | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../app/src/main/res/values/colors.xml | 10 + .../app/src/main/res/values/prompts.xml | 50 +++ .../app/src/main/res/values/strings.xml | 12 + .../app/src/main/res/values/themes.xml | 5 + .../app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 ++ .../example/videoediting/ExampleUnitTest.kt | 17 ++ AI Video Clipper/build.gradle.kts | 6 + AI Video Clipper/gradle.properties | 15 + .../gradle/gradle-daemon-jvm.properties | 12 + AI Video Clipper/gradle/libs.versions.toml | 50 +++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45457 bytes .../gradle/wrapper/gradle-wrapper.properties | 9 + AI Video Clipper/gradlew | 251 +++++++++++++++ AI Video Clipper/gradlew.bat | 94 ++++++ AI Video Clipper/settings.gradle.kts | 26 ++ 50 files changed, 1968 insertions(+) create mode 100644 AI Video Clipper/app/.gitignore create mode 100644 AI Video Clipper/app/build.gradle.kts create mode 100644 AI Video Clipper/app/google-services.json create mode 100644 AI Video Clipper/app/proguard-rules.pro create mode 100644 AI Video Clipper/app/src/androidTest/java/com/example/videoediting/ExampleInstrumentedTest.kt create mode 100644 AI Video Clipper/app/src/main/AndroidManifest.xml create mode 100644 AI Video Clipper/app/src/main/java/com/example/videoediting/AIResponseJsonSchema.kt create mode 100644 AI Video Clipper/app/src/main/java/com/example/videoediting/FirebaseStorageRepository.kt create mode 100644 AI Video Clipper/app/src/main/java/com/example/videoediting/GeminiRepository.kt create mode 100644 AI Video Clipper/app/src/main/java/com/example/videoediting/GenAIParser.kt create mode 100644 AI Video Clipper/app/src/main/java/com/example/videoediting/MainActivity.kt create mode 100644 AI Video Clipper/app/src/main/java/com/example/videoediting/MediaSelectionScreen.kt create mode 100644 AI Video Clipper/app/src/main/java/com/example/videoediting/MediaSelectionViewModel.kt create mode 100644 AI Video Clipper/app/src/main/java/com/example/videoediting/VideoAnalysisResultScreen.kt create mode 100644 AI Video Clipper/app/src/main/java/com/example/videoediting/VideoAnalysisViewModel.kt create mode 100644 AI Video Clipper/app/src/main/java/com/example/videoediting/ui/theme/Color.kt create mode 100644 AI Video Clipper/app/src/main/java/com/example/videoediting/ui/theme/Theme.kt create mode 100644 AI Video Clipper/app/src/main/java/com/example/videoediting/ui/theme/Type.kt create mode 100644 AI Video Clipper/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 AI Video Clipper/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 AI Video Clipper/app/src/main/res/drawable/rounded_pause_24.xml create mode 100644 AI Video Clipper/app/src/main/res/drawable/rounded_play_arrow_24.xml create mode 100644 AI Video Clipper/app/src/main/res/mipmap-anydpi/ic_launcher.xml create mode 100644 AI Video Clipper/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml create mode 100644 AI Video Clipper/app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 AI Video Clipper/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 AI Video Clipper/app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 AI Video Clipper/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 AI Video Clipper/app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 AI Video Clipper/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 AI Video Clipper/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 AI Video Clipper/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 AI Video Clipper/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 AI Video Clipper/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 AI Video Clipper/app/src/main/res/values/colors.xml create mode 100644 AI Video Clipper/app/src/main/res/values/prompts.xml create mode 100644 AI Video Clipper/app/src/main/res/values/strings.xml create mode 100644 AI Video Clipper/app/src/main/res/values/themes.xml create mode 100644 AI Video Clipper/app/src/main/res/xml/backup_rules.xml create mode 100644 AI Video Clipper/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 AI Video Clipper/app/src/test/java/com/example/videoediting/ExampleUnitTest.kt create mode 100644 AI Video Clipper/build.gradle.kts create mode 100644 AI Video Clipper/gradle.properties create mode 100644 AI Video Clipper/gradle/gradle-daemon-jvm.properties create mode 100644 AI Video Clipper/gradle/libs.versions.toml create mode 100644 AI Video Clipper/gradle/wrapper/gradle-wrapper.jar create mode 100644 AI Video Clipper/gradle/wrapper/gradle-wrapper.properties create mode 100755 AI Video Clipper/gradlew create mode 100644 AI Video Clipper/gradlew.bat create mode 100644 AI Video Clipper/settings.gradle.kts diff --git a/AI Video Clipper/app/.gitignore b/AI Video Clipper/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/AI Video Clipper/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/AI Video Clipper/app/build.gradle.kts b/AI Video Clipper/app/build.gradle.kts new file mode 100644 index 0000000..305c3d2 --- /dev/null +++ b/AI Video Clipper/app/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.serialization) + id("com.google.gms.google-services") +} + +android { + namespace = "com.example.videoediting" + compileSdk = 37 + + defaultConfig { + applicationId = "com.example.videoediting" + minSdk = 30 + targetSdk = 37 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.ai) + implementation(libs.firebase.common) + implementation(libs.firebase.storage) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.media3.common) + implementation(libs.androidx.media3.effect) + implementation(libs.androidx.media3.transformer) + implementation(libs.androidx.media3.ui.compose) + implementation(libs.androidx.media3.inspector) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.coroutines.guava) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) + testImplementation(libs.junit) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(libs.androidx.junit) + debugImplementation(libs.androidx.compose.ui.test.manifest) + debugImplementation(libs.androidx.compose.ui.tooling) +} \ No newline at end of file diff --git a/AI Video Clipper/app/google-services.json b/AI Video Clipper/app/google-services.json new file mode 100644 index 0000000..e69de29 diff --git a/AI Video Clipper/app/proguard-rules.pro b/AI Video Clipper/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/AI Video Clipper/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/AI Video Clipper/app/src/androidTest/java/com/example/videoediting/ExampleInstrumentedTest.kt b/AI Video Clipper/app/src/androidTest/java/com/example/videoediting/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..bdddc5c --- /dev/null +++ b/AI Video Clipper/app/src/androidTest/java/com/example/videoediting/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.videoediting + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.videoediting", appContext.packageName) + } +} \ No newline at end of file diff --git a/AI Video Clipper/app/src/main/AndroidManifest.xml b/AI Video Clipper/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ea13587 --- /dev/null +++ b/AI Video Clipper/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AI Video Clipper/app/src/main/java/com/example/videoediting/AIResponseJsonSchema.kt b/AI Video Clipper/app/src/main/java/com/example/videoediting/AIResponseJsonSchema.kt new file mode 100644 index 0000000..59be042 --- /dev/null +++ b/AI Video Clipper/app/src/main/java/com/example/videoediting/AIResponseJsonSchema.kt @@ -0,0 +1,18 @@ +package com.example.videoediting + +import com.google.firebase.ai.type.Schema + +val jsonSchema = Schema.obj(mapOf( + "videos" to Schema.array( + Schema.obj(mapOf( + "uri" to Schema.string(), + "mostEngagingSegment" to Schema.obj( + mapOf( + "startMs" to Schema.long("A start ms value for clipping"), + "endMs" to Schema.long("End ms value for clipping"), + "reasoning" to Schema.string("reasoning for not finding a clipping range") + ) + ), + )) + )) +) \ No newline at end of file diff --git a/AI Video Clipper/app/src/main/java/com/example/videoediting/FirebaseStorageRepository.kt b/AI Video Clipper/app/src/main/java/com/example/videoediting/FirebaseStorageRepository.kt new file mode 100644 index 0000000..58f99d2 --- /dev/null +++ b/AI Video Clipper/app/src/main/java/com/example/videoediting/FirebaseStorageRepository.kt @@ -0,0 +1,27 @@ +package com.example.videoediting + +import android.net.Uri +import android.util.Log +import com.google.firebase.storage.FirebaseStorage +import kotlinx.coroutines.tasks.await +import java.util.UUID + +class FirebaseStorageRepository(private val storage: FirebaseStorage) { + + suspend fun uploadFile(uri: Uri): String? { + return try { + val storageRef = storage.reference + val videoRef = storageRef.child("videos/${UUID.randomUUID()}.mp4") + val uploadTask = videoRef.putFile(uri) + uploadTask.await() // Wait for the upload to complete + val path = videoRef.path + val bucket = videoRef.bucket + val storageUrl = "gs://$bucket$path" + Log.d("FirebaseStorageRepo", "File uploaded successfully: $storageUrl") + storageUrl + } catch (e: Exception) { + Log.e("FirebaseStorageRepo", "Error uploading file", e) + null + } + } +} diff --git a/AI Video Clipper/app/src/main/java/com/example/videoediting/GeminiRepository.kt b/AI Video Clipper/app/src/main/java/com/example/videoediting/GeminiRepository.kt new file mode 100644 index 0000000..8133684 --- /dev/null +++ b/AI Video Clipper/app/src/main/java/com/example/videoediting/GeminiRepository.kt @@ -0,0 +1,54 @@ +package com.example.videoediting + +import android.net.Uri +import android.util.Log +import com.google.firebase.Firebase +import com.google.firebase.ai.ai +import com.google.firebase.ai.type.GenerativeBackend +import com.google.firebase.ai.type.content +import com.google.firebase.ai.type.generationConfig +import com.google.firebase.ai.type.thinkingConfig +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class GeminiRepository { + + private val aiModel = Firebase.ai(backend = GenerativeBackend.vertexAI()) + .generativeModel( + modelName = "gemini-2.5-pro", + generationConfig = generationConfig { + temperature = 0f + responseMimeType = "application/json" + responseSchema = jsonSchema + thinkingConfig = thinkingConfig { + thinkingBudget = 1024 + includeThoughts = true + } + } + ) + + suspend fun generateEditResponse(promptData: String, uriPairs: List>): GeminiResponse = withContext(Dispatchers.IO) { + val gsUris = uriPairs.map { it.first } + val requestContent = content { + gsUris.forEach { gsUri -> + fileData(gsUri, "video/mp4") + } + + text(promptData) + } + val response = aiModel.generateContent(requestContent) + + val aiThoughts = if (response.thoughtSummary.isNullOrEmpty()) null else response.thoughtSummary + val aiResponseText = if (response.text.isNullOrEmpty()) null else response.text + Log.d("GeminiRepository", "response $aiResponseText") + val aiTokensUsed = response.usageMetadata?.thoughtsTokenCount ?: -1 + + GeminiResponse(aiThoughts, aiResponseText, aiTokensUsed) + } +} + +data class GeminiResponse( + val thoughts: String?, + val responseText: String?, + val tokensUsed: Int +) diff --git a/AI Video Clipper/app/src/main/java/com/example/videoediting/GenAIParser.kt b/AI Video Clipper/app/src/main/java/com/example/videoediting/GenAIParser.kt new file mode 100644 index 0000000..5315b6e --- /dev/null +++ b/AI Video Clipper/app/src/main/java/com/example/videoediting/GenAIParser.kt @@ -0,0 +1,67 @@ +package com.example.videoediting + +import android.util.Log +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +object GenAIParser { + private const val TAG = "GenAIParser" + + // 1. JSON DTOs matching the exact schema returned by Gemini + @Serializable + private data class MostEngagingSegmentDto( + val startMs: Long? = null, + val endMs: Long? = null, + val reasoning: String? = null + ) + + @Serializable + private data class VideoSegmentDto( + val uri: String, + val mostEngagingSegment: MostEngagingSegmentDto? = null + ) + + @Serializable + private data class AIResponseDto( + val videos: List = emptyList() + ) + + // 2. Public Domain Models (Unchanged to prevent breaking callers) + data class SegmentTimes(val startMs: Long, val endMs: Long) + data class VideoSegment(val uri: String, val segmentTimes: SegmentTimes?, val reasoning: String?) + + private val jsonConfig = Json { + ignoreUnknownKeys = true // Resilient to extra fields returned by the AI + } + + /** + * Parses a JSON string conforming to AIResponseJsonSchema and returns a list of VideoSegment objects. + * + * @param jsonString The JSON string response from the Gemini agent. + * @return A [List] of [VideoSegment] objects. Returns an empty list on failure. + */ + fun parseVideoSegments(jsonString: String): List { + return try { + val responseDto = jsonConfig.decodeFromString(jsonString) + responseDto.videos.map { dto -> + val segmentDto = dto.mostEngagingSegment + var segmentTimes: SegmentTimes? = null + var reasoning: String? = null + + if (segmentDto != null) { + val startMs = segmentDto.startMs ?: -1L + val endMs = segmentDto.endMs ?: -1L + reasoning = segmentDto.reasoning + + if (startMs >= 0 && endMs > startMs) { + segmentTimes = SegmentTimes(startMs, endMs) + } + } + VideoSegment(dto.uri, segmentTimes, reasoning) + } + } catch (e: Exception) { + Log.e(TAG, "Failed to parse video segments JSON with kotlinx-serialization", e) + emptyList() + } + } +} diff --git a/AI Video Clipper/app/src/main/java/com/example/videoediting/MainActivity.kt b/AI Video Clipper/app/src/main/java/com/example/videoediting/MainActivity.kt new file mode 100644 index 0000000..ab6c062 --- /dev/null +++ b/AI Video Clipper/app/src/main/java/com/example/videoediting/MainActivity.kt @@ -0,0 +1,30 @@ +package com.example.videoediting + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.annotation.OptIn +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.ui.Modifier +import androidx.media3.common.util.UnstableApi +import com.example.videoediting.ui.theme.GeminiVideoEditingTheme + +class MainActivity : ComponentActivity() { + @OptIn(UnstableApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + GeminiVideoEditingTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + MediaSelectionScreen( + modifier = Modifier.padding(innerPadding) + ) + } + } + } + } +} \ No newline at end of file diff --git a/AI Video Clipper/app/src/main/java/com/example/videoediting/MediaSelectionScreen.kt b/AI Video Clipper/app/src/main/java/com/example/videoediting/MediaSelectionScreen.kt new file mode 100644 index 0000000..7810cd1 --- /dev/null +++ b/AI Video Clipper/app/src/main/java/com/example/videoediting/MediaSelectionScreen.kt @@ -0,0 +1,288 @@ +package com.example.videoediting + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.result.PickVisualMediaRequest +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.AutoAwesome +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.media3.common.util.UnstableApi +import com.example.videoediting.ui.theme.GeminiVideoEditingTheme + +@UnstableApi +@Composable +fun MediaSelectionScreen( + modifier: Modifier = Modifier, + viewModel: MediaSelectionViewModel = viewModel() +) { + val uiState by viewModel.uiState.collectAsState() + + val pickVideoLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickMultipleVisualMedia(), + onResult = { uris -> + if (uris.isNotEmpty()) { + viewModel.updateSelectedVideos(uris) + } + } + ) + + // Handle Snackbar for errors + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(uiState.errorMessage) { + uiState.errorMessage?.let { msg -> + snackbarHostState.showSnackbar(msg) + viewModel.clearError() + } + } + + if (uiState.aiResponse != null && uiState.selectedVideoUris.isNotEmpty()) { + VideoAnalysisResultScreen( + jsonResponse = uiState.aiResponse!!, + uriPairs = uiState.uriPairs, + modifier = modifier + ) + } else { + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + modifier = modifier.fillMaxSize() + ) { paddingValues -> + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(paddingValues) + .padding(24.dp) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Center) + ) { + // Central Icon with circle background + Box( + modifier = Modifier + .size(120.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primaryContainer), // Theme-based circle background + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier.size(60.dp), + contentAlignment = Alignment.Center + ) { + // Background card + Box( + modifier = Modifier + .size(40.dp) + .offset(x = (-4).dp, y = (-4).dp) + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.4f)) + ) + + // Foreground card + Box( + modifier = Modifier + .size(40.dp) + .offset(x = 4.dp, y = 4.dp) + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.primary), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Filled.PlayArrow, + contentDescription = "Play", + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(24.dp) + ) + } + } + } + + Spacer(modifier = Modifier.height(32.dp)) + + Text( + text = if (uiState.selectedVideoUris.isNotEmpty()) stringResource(R.string.video_selected) else stringResource(R.string.no_media_selected), + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = if (uiState.selectedVideoUris.isNotEmpty()) { + stringResource(R.string.videos_selected, uiState.selectedVideoUris.size) + } else { + stringResource(R.string.choose_videos) + }, + fontSize = 16.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Spacer(modifier = Modifier.height(40.dp)) + + Button( + onClick = { + pickVideoLauncher.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly) + ) + }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ), + shape = RoundedCornerShape(50.dp), // Pill shaped + modifier = Modifier.height(56.dp) + ) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = "Add", + tint = MaterialTheme.colorScheme.onPrimary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.select_videos), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onPrimary + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + OutlinedTextField( + value = uiState.intentText, + onValueChange = { viewModel.updateIntentText(it) }, + placeholder = { Text(stringResource(R.string.describe_intent)) }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + colors = OutlinedTextFieldDefaults.colors( + unfocusedBorderColor = MaterialTheme.colorScheme.outline, + focusedBorderColor = MaterialTheme.colorScheme.primary + ) + ) + } + + Button( + onClick = { + viewModel.onEditWithGeminiClicked() + }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + disabledContainerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f) + ), + shape = RoundedCornerShape(8.dp), + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .align(Alignment.BottomCenter), + enabled = uiState.selectedVideoUris.isNotEmpty() && !uiState.isLoading + ) { + Icon( + imageVector = Icons.Filled.AutoAwesome, + contentDescription = "Process", + tint = MaterialTheme.colorScheme.onPrimary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.edit_with_gemini), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onPrimary + ) + } + + // Loading Dialog + if (uiState.isLoading) { + Dialog(onDismissRequest = { /* Do nothing to prevent dismiss */ }) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + // Top colored border + Box( + modifier = Modifier + .fillMaxWidth() + .height(6.dp) + .background(MaterialTheme.colorScheme.primary) // Dynamic primary border + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Loader + CircularProgressIndicator( + modifier = Modifier.size(32.dp), + color = MaterialTheme.colorScheme.primary, + strokeWidth = 3.dp + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = stringResource(R.string.gemini_processing), + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.applying_edits), + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } + } + } + } + } + } + } + } +} + +@Preview(showBackground = true) +@UnstableApi +@Composable +fun MediaSelectionScreenPreview() { + GeminiVideoEditingTheme { + // Just for previewing theme + } +} diff --git a/AI Video Clipper/app/src/main/java/com/example/videoediting/MediaSelectionViewModel.kt b/AI Video Clipper/app/src/main/java/com/example/videoediting/MediaSelectionViewModel.kt new file mode 100644 index 0000000..2b9f413 --- /dev/null +++ b/AI Video Clipper/app/src/main/java/com/example/videoediting/MediaSelectionViewModel.kt @@ -0,0 +1,130 @@ +package com.example.videoediting + +import android.app.Application +import android.net.Uri +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.google.firebase.Firebase +import com.google.firebase.storage.storage +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +data class MediaSelectionUiState( + val selectedVideoUris: List = emptyList(), + val isLoading: Boolean = false, + val intentText: String = "", + val errorMessage: String? = null, + val uriPairs: List> = emptyList(), + val aiResponse: String? = null, + val aiThoughts: String? = null +) + +class MediaSelectionViewModel(application: Application) : AndroidViewModel(application) { + + // In a real app with DI, these would be injected + private val storageRepository by lazy { + val storage = Firebase.storage.apply { + maxUploadRetryTimeMillis = 30000 + maxOperationRetryTimeMillis = 30000 + } + FirebaseStorageRepository(storage) + } + private val geminiRepository = GeminiRepository() + + private val _uiState = MutableStateFlow(MediaSelectionUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun updateSelectedVideos(uris: List) { + _uiState.update { it.copy(selectedVideoUris = uris) } + } + + fun updateIntentText(text: String) { + _uiState.update { it.copy(intentText = text) } + } + + fun clearError() { + _uiState.update { it.copy(errorMessage = null) } + } + + fun onEditWithGeminiClicked() { + val currentState = _uiState.value + if (currentState.selectedVideoUris.isEmpty()) return + + _uiState.update { it.copy(isLoading = true, errorMessage = null) } + + viewModelScope.launch { + try { + // 1. Upload files in parallel + val newUriPairs = coroutineScope { + currentState.selectedVideoUris.map { localUri -> + async { + val uploadedUri = storageRepository.uploadFile(localUri) + if (uploadedUri != null) { + uploadedUri to localUri + } else { + Log.e("MediaSelectionVM", "Failed to upload $localUri") + _uiState.update { it.copy(errorMessage = "Failed to upload video: $localUri") } + null + } + } + }.awaitAll().filterNotNull() + } + + _uiState.update { it.copy(uriPairs = newUriPairs) } + + if (newUriPairs.isEmpty()) { + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "No files were uploaded successfully. Cannot proceed." + ) + } + return@launch + } + + // 2. Generate content + // include a list of files to analyze + val gsUris = newUriPairs.map { it.first } + val uriListForPrompt = gsUris.joinToString("\n") { "- $it" } + + val context = getApplication() + val promptData = context.resources.getString(R.string.generate_simple_preamble, currentState.intentText, uriListForPrompt) + + val response = geminiRepository.generateEditResponse(promptData, newUriPairs) + + if (response.responseText != null) { + _uiState.update { + it.copy( + isLoading = false, + aiResponse = response.responseText, + aiThoughts = response.thoughts + ) + } + } else { + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to get a response from Gemini." + ) + } + } + + } catch (e: Exception) { + Log.e("MediaSelectionVM", "Error processing video", e) + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "An error occurred: ${e.localizedMessage}" + ) + } + } + } + } +} diff --git a/AI Video Clipper/app/src/main/java/com/example/videoediting/VideoAnalysisResultScreen.kt b/AI Video Clipper/app/src/main/java/com/example/videoediting/VideoAnalysisResultScreen.kt new file mode 100644 index 0000000..e3b2941 --- /dev/null +++ b/AI Video Clipper/app/src/main/java/com/example/videoediting/VideoAnalysisResultScreen.kt @@ -0,0 +1,197 @@ +package com.example.videoediting + +import android.net.Uri +import androidx.annotation.OptIn +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AutoAwesome +import androidx.compose.material3.* +import androidx.compose.runtime.* +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.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.media3.common.util.ExperimentalApi +import androidx.media3.common.util.UnstableApi +import androidx.media3.transformer.CompositionPlayer +import androidx.media3.ui.compose.PlayerSurface +import androidx.media3.ui.compose.buttons.PlayPauseButton +import androidx.media3.ui.compose.state.PlayPauseButtonState +import androidx.media3.ui.compose.state.rememberPlayPauseButtonState + +@UnstableApi +@OptIn(ExperimentalApi::class) +@Composable +fun VideoAnalysisResultScreen( + jsonResponse: String, + uriPairs: List>, + modifier: Modifier = Modifier, + viewModel: VideoAnalysisViewModel = viewModel(), +) { + val uiState by viewModel.uiState.collectAsState() + + DisposableEffect(viewModel) { + onDispose { + viewModel.releasePlayer() + } + } + + LaunchedEffect(jsonResponse, uriPairs) { + viewModel.initialize(jsonResponse, uriPairs) + } + + if (uiState.isLoading) { + LoadingScreen(modifier) + } else { + ResultContent( + uiState = uiState, + modifier = modifier + ) + } +} + +@Composable +private fun LoadingScreen(modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + } +} + +@UnstableApi +@OptIn(ExperimentalApi::class) +@Composable +private fun ResultContent( + uiState: VideoAnalysisUiState, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + item { + VideoPlayerSection(uiState.player) + } + + item { + ReasoningHeader() + } + + itemsIndexed(uiState.videos) { index, video -> + ReasoningCard(index, video) + } + } +} + +@UnstableApi +@OptIn(ExperimentalApi::class) +@Composable +private fun VideoPlayerSection(player: CompositionPlayer?) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(480.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Color.Black) + ) { + if (player != null) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() + ) { + PlayerSurface( + player = player, modifier = Modifier.weight(1f) + ) + + val buttonState = rememberPlayPauseButtonState(player) + PlayPauseButton(buttonState) + } + } else { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = Color.White) + } + } + } +} + +@OptIn(UnstableApi::class) +@Composable +fun PlayPauseButton(buttonState: PlayPauseButtonState) { + val icon = + if (buttonState.showPlay) painterResource(R.drawable.rounded_play_arrow_24) else painterResource( + R.drawable.rounded_pause_24 + ) + val contentDescription = if (buttonState.showPlay) "Play" else "Pause" + FilledIconButton(onClick = buttonState::onClick, enabled = buttonState.isEnabled) { + Icon(icon, contentDescription) + } +} +@Composable +private fun ReasoningHeader() { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + ) { + Icon( + imageVector = Icons.Filled.AutoAwesome, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Reasonings", + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold + ), + color = MaterialTheme.colorScheme.onBackground + ) + } +} + +@Composable +private fun ReasoningCard(index: Int, video: GenAIParser.VideoSegment) { + Card( + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ), + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Segment ${index + 1}", + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = video.reasoning ?: "No reasoning provided", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 20.sp + ) + } + } +} diff --git a/AI Video Clipper/app/src/main/java/com/example/videoediting/VideoAnalysisViewModel.kt b/AI Video Clipper/app/src/main/java/com/example/videoediting/VideoAnalysisViewModel.kt new file mode 100644 index 0000000..b17e56c --- /dev/null +++ b/AI Video Clipper/app/src/main/java/com/example/videoediting/VideoAnalysisViewModel.kt @@ -0,0 +1,126 @@ +package com.example.videoediting + +import android.app.Application +import android.net.Uri +import android.util.Log +import androidx.annotation.OptIn +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import androidx.media3.common.MediaItem +import androidx.media3.common.util.ExperimentalApi +import androidx.media3.common.util.UnstableApi +import androidx.media3.inspector.MetadataRetriever +import androidx.media3.transformer.Composition +import androidx.media3.transformer.Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL +import androidx.media3.transformer.CompositionPlayer +import androidx.media3.transformer.EditedMediaItem +import androidx.media3.transformer.EditedMediaItemSequence +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.guava.await +import kotlinx.coroutines.launch + +@UnstableApi +@OptIn(ExperimentalApi::class) +data class VideoAnalysisUiState( + val videos: List = emptyList(), + val durations: Map = emptyMap(), + val isLoading: Boolean = true, + val player: CompositionPlayer? = null, +) + +@UnstableApi +class VideoAnalysisViewModel(application: Application) : AndroidViewModel(application) { + private val _uiState = MutableStateFlow(VideoAnalysisUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun initialize(jsonResponse: String, uriPairs: List>) { + if (_uiState.value.videos.isNotEmpty()) return // Already initialized + + val videos = GenAIParser.parseVideoSegments(jsonResponse) + _uiState.update { it.copy(videos = videos) } + + val context = getApplication() + + viewModelScope.launch { + val durations = coroutineScope { + videos.map { video -> + async { + val localUri = uriPairs.find { it.first == video.uri }?.second + if (localUri != null) { + try { + val mediaItem = MediaItem.fromUri(localUri) + val retriever = MetadataRetriever.Builder(context, mediaItem).build() + val durationUs = retriever.retrieveDurationUs().await() + retriever.close() + video.uri to durationUs + } catch (e: Exception) { + Log.e("VideoAnalysisVM", "Error retrieving duration for ${video.uri}", e) + null + } + } else { + null + } + } + }.awaitAll().filterNotNull().toMap() + } + _uiState.update { it.copy(durations = durations, isLoading = false) } + setupPlayer(uriPairs) + } + } + + @UnstableApi + @OptIn(ExperimentalApi::class) + private fun setupPlayer(uriPairs: List>) { + val context = getApplication() + val currentState = _uiState.value + val editedMediaItems = currentState.videos.map { video -> + val localUri = uriPairs.find { it.first == video.uri }?.second + val durationUs = currentState.durations[video.uri] ?: 0L + + val mediaItemBuilder = MediaItem.Builder().setUri(localUri) + + video.segmentTimes?.let { + val totalDurationMs = durationUs / 1000 + val endMs = it.endMs.coerceIn(0, totalDurationMs) + val startMs = it.startMs.coerceIn(0, endMs) + mediaItemBuilder.setClippingConfiguration( + MediaItem.ClippingConfiguration.Builder() + .setStartPositionMs(startMs) + .setEndPositionMs(endMs) + .build() + ) + } + + EditedMediaItem.Builder(mediaItemBuilder.build()) + .setDurationUs(durationUs) + .build() + } + + val composition = Composition.Builder( + EditedMediaItemSequence.withAudioAndVideoFrom(editedMediaItems) + ).setHdrMode(HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL).build() + + val player = CompositionPlayer.Builder(context).build().apply { + setComposition(composition) + prepare() + } + + _uiState.update { it.copy(player = player) } + } + + fun releasePlayer() { + _uiState.value.player?.release() + _uiState.update { it.copy(player = null) } + } + + override fun onCleared() { + super.onCleared() + releasePlayer() + } +} diff --git a/AI Video Clipper/app/src/main/java/com/example/videoediting/ui/theme/Color.kt b/AI Video Clipper/app/src/main/java/com/example/videoediting/ui/theme/Color.kt new file mode 100644 index 0000000..550eb70 --- /dev/null +++ b/AI Video Clipper/app/src/main/java/com/example/videoediting/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.example.videoediting.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/AI Video Clipper/app/src/main/java/com/example/videoediting/ui/theme/Theme.kt b/AI Video Clipper/app/src/main/java/com/example/videoediting/ui/theme/Theme.kt new file mode 100644 index 0000000..31f5bd6 --- /dev/null +++ b/AI Video Clipper/app/src/main/java/com/example/videoediting/ui/theme/Theme.kt @@ -0,0 +1,47 @@ +package com.example.videoediting.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 +) + +@Composable +fun GeminiVideoEditingTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/AI Video Clipper/app/src/main/java/com/example/videoediting/ui/theme/Type.kt b/AI Video Clipper/app/src/main/java/com/example/videoediting/ui/theme/Type.kt new file mode 100644 index 0000000..8e30844 --- /dev/null +++ b/AI Video Clipper/app/src/main/java/com/example/videoediting/ui/theme/Type.kt @@ -0,0 +1,18 @@ +package com.example.videoediting.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) +) \ No newline at end of file diff --git a/AI Video Clipper/app/src/main/res/drawable/ic_launcher_background.xml b/AI Video Clipper/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..61bb79e --- /dev/null +++ b/AI Video Clipper/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AI Video Clipper/app/src/main/res/drawable/ic_launcher_foreground.xml b/AI Video Clipper/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..966abaf --- /dev/null +++ b/AI Video Clipper/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/AI Video Clipper/app/src/main/res/drawable/rounded_pause_24.xml b/AI Video Clipper/app/src/main/res/drawable/rounded_pause_24.xml new file mode 100644 index 0000000..a67f446 --- /dev/null +++ b/AI Video Clipper/app/src/main/res/drawable/rounded_pause_24.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AI Video Clipper/app/src/main/res/drawable/rounded_play_arrow_24.xml b/AI Video Clipper/app/src/main/res/drawable/rounded_play_arrow_24.xml new file mode 100644 index 0000000..9ff05ee --- /dev/null +++ b/AI Video Clipper/app/src/main/res/drawable/rounded_play_arrow_24.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AI Video Clipper/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/AI Video Clipper/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 0000000..5ad9ce1 --- /dev/null +++ b/AI Video Clipper/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AI Video Clipper/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/AI Video Clipper/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 0000000..5ad9ce1 --- /dev/null +++ b/AI Video Clipper/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AI Video Clipper/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/AI Video Clipper/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG literal 0 HcmV?d00001 diff --git a/AI Video Clipper/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/AI Video Clipper/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9 GIT binary patch literal 2898 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 literal 0 HcmV?d00001 diff --git a/AI Video Clipper/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/AI Video Clipper/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i literal 0 HcmV?d00001 diff --git a/AI Video Clipper/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/AI Video Clipper/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0 GIT binary patch literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 diff --git a/AI Video Clipper/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/AI Video Clipper/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f GIT binary patch literal 3918 zcmV-U53%r4Nk&FS4*&pHMM6+kP&il$0000G0001A003VA06|PpNSy@$00HoY|G(*G z+qV7x14$dSO^Re!iqt-AAIE9iwr$(CZQJL$blA4B`>;C3fBY6Q8_YSjb2%a=fc}4E zrSzssacq<^nmW|Rs93PJni30R<8w<(bK_$LO4L?!_OxLl$}K$MUEllnMK|rg=f3;y z*?;3j|Nh>)p0JQ3A~rf(MibH2r+)3cyV1qF&;8m{w-S*y+0mM){KTK^M5}ksc`qX3 zy>rf^b>~l>SSHds8(I@hz3&PD@LmEs4&prkT=BjsBCXTMhN$_)+kvnl0bLKW5rEsj z*d#KXGDB4P&>etx0X+`R19yC=LS)j!mgs5M0L~+o-T~Jl!p!AJxnGAhV%~rhYUL4hlWhgES3Kb5oA&X z{}?3OBSS-{!v$nCIGj->(-TAG)8LR{htr41^gxsT8yqt2@DEG6Yl`Uma3Nd4;YUoW zTbkYl3CMU5ypMF3EIkYmWL|*BknM`0+Kq6CpvO(y$#j94e+q{vI{Zp8cV_6RK!`&C zob$*5Q|$IZ09dW=L!V zw@#2wviu|<#3lgGE8GEhcx+zBt`} zOwP8j9X%^f7i_bth4PiJ$LYtFJSCN$3xwDN;8mr*B;CJwBP2G0TMq0uNt7S^DO_wE zepk!Wrn#Z#03j{`c*Rf~y3o7?J}w?tEELRUR2cgxB*Y{LzA#pxHgf}q?u5idu>077 zd^=p)`nA}6e`|@`p?u}YU66PP_MA}Zqqe!c{nK&z%Jwq1N4e_q<#4g^xaz=ao;u|6 zwpRcW2Lax=ZGbx=Q*HhlJ`Ns#Y*r0*%!T?P*TTiX;rb)$CGLz=rSUum$)3Qyv{BL2 zO*=OI2|%(Yz~`pNEOnLp>+?T@glq-DujlIp?hdJeZ7ctP4_OKx|5@EOps3rr(pWzg zK4d3&oN-X2qN(d_MkfwB4I)_)!I_6nj2iA9u^pQ{;GckGLxBGrJUM2Wdda!k)Y>lq zmjws>dVQ*vW9lvEMkiN3wE-__6OWD0txS&Qn0n22cyj4Q*8(nG4!G{6OOwNvsrPIL zCl-$W9UwkEUVuLwyD%|inbOF*xMODZ4VMEVAq_zUxZ+K#Gdqf!DW$5f)?7UNOFMz! zrB~tuu=6X2FE(p^iqgxr+?ZK;=yz`e;C$#_@D9Lj-+TDVOrva>(#*PVbaHO>A)mhl z07OJWCqYC60518$!&c`eNBcBW%GnfaQ*$eazV^2_AW?j)h;J1nUjN(I9=0+!RVx~% z3@Tf!P0TE+98jA?WceK-}A1% zW!K)lyKcGqy#M~})315-A#2NXQ`?6NR#Apo=S!oF=JfpX>iR*49ec{7AN$xxpK{D$ z2d%Fz&rdfSqourN$~Y^NFIMV1CZ?J*bMx~H3k&meGtH@q9ra2vZxmA$S(#jaaj-g4 ztJmxG+DLV<*q<|sDXPp$X>E)#S}Vm&sRaO5P&goh2><}FEdZSXDqsL$06sAkh(e+v zAsBhKSRexgwg6tIy~GFJzaTxXD(}|+0eOwFDA%rn`X;MVwDHT9=4=g%OaJ9s%3b9>9EUTnnp0t;2Zpa{*>mk~hZqItE_!dQ zOtC>8`$l|mV43Jbudf0N6&&X;{=z}Zi}d1`2qmJ}i|0*GsulD3>GgQXHN)pkR6sf1 z?5ZU%&xtL}oH;YiAA)d*^Ndw2T$+Mjuzyzz@-SM`9df7LqTxLuIwC~S0092~+=qYv z@*ja;?Wt!T!{U?c*Z0YtGe)XbI&y-?B&G2$`JDM)(dIV9G`Sc#6?sI60de6kv+)Qb zUW~2|WjvJq3TA8`0+sWA3zRhY9a~ow)O~&StBkG2{*{TGiY~S8ep{V&Vo2l<6LWsu z^#p0-v*t2?3&aA1)ozu|%efSR=XnpX$lvTeRdKlvM!@|pM5p2w3u-6 zU>}t2xiYLS+{|%C65AzX+23Mtlq?BS&YdYcYsVjoiE&rT>;Necn6l^K)T^lmE`5u{ zm1i+-a-gc;Z&v-{;8r)z6NYfBUv+=_L}ef}qa9FX01)+Aaf+;xj(mL6|JUzGJR1|fnanb%?BPPIp>SCjP|8qE5qJ{=n5ZGw?81z3(k;pzH%1CtlX50{E7h)$h{qGKfzC`e2o`*IqA#tjA z`Fz&^%$b9F*N`)U-#6>a)Z`55`$Dd0cfcs0$d13^ONrdCu9xcv_=n#WQo8stcz3jP9|2EvdI-RhJM3%Q%oM&!OlShM|0 z?gz?wHZSnm45njLtsz8PVT1S&jAlbKg5kVam$p16=EK@Sj4EP0OtH zmJDmdc^v)x>56Qg_wmYHz6h)>kl_h$>0@J!ypv%APmjZTAQVLy6Fu50RGY&JAVNhx zrF_qG6`x9MkT;1SFWo$)l{M$;3qUDn9JwE}z zRl#E_bDRJFii61kPgBybIgp8dNW!Cc1b*^YYk-#oWLJvtM_v^hQx~9?8LD4VFFxBF z3MlrsSC%f9Oupn*ctPL0U1fwfX?`tRhPD{PSLFPQOmIt$mDy0SgpNVvHS+f#Do>h1Gn?LZU9(KaN>Q_=Y*_T zvtD7%_u^^+{g`0VGzg(VZrpVQ6Ub5M=tI_p7T93R8@3Zulu3|#{iNcu!oiHxZ4Rf*( zfmiN$$ru(*_Zqn=`Gq#OuHRTSwp7uH_SokR&|)RuW5yo=Z|_4?qU-JU+tpt>!B&Is z@N(=SG;bpVc;AO@zbmMM zScqq1)b-ZQIrs={oD}|?6y{$HNB1U0^LsBh8JI&3!GBZxOXI<}&5-$lgkAaYqhOTb z?2vEnZ$-kk;*M_17(upJF3%+iH*s0-r{vttXVB2OUwI1s^+G(Ft(U8gYFXC}#P&E^ z>T@C^tS`Z7{6HT4_nF~n>JlZtk5&qDBl6r|^kzQYe`wq!C)n@$c>WOPA61NDFj<<6 zGW71NMMhwAl!U-yqrq2xrSFqRCI8acw7?}3j;ynxo*-b7Co;g5r%^j=H@9({PXXBf z@r>U>>N;E)81wx`B4f%{PB~MHka_);%kBCb(d|Jy5!MqJ%2p`t&@L)4$T2j&-WHvG zv3(uyA_gwqNu(k?jQTtv3dgPKRZoH8prxe7>pQBW5L&dpumS&5Ld2?(sCpJjvc4L5 zEnh&?91WVm)ZdTj=fjJ$pPDdgAttLXuke+?KdKxu*;kTC(r!tQk6;gxj4h%FdHAt(^M3YvYj(!tOeN)+Hvj6+< zzyJRG?^lZfWuR#t!tUKP&(?%3v&Zd$R2YN>lB(Lq`OInY48%4%yTv2 zYe1{G`3)(PDEio5Y@-I5tUf`c%%OCJMtSW56g3iEg%3`$7XSJJHyA z<|7&N)5Xrlgv~%BO24eFd;Hd;uiK%D`EdK|quUeRZDqbh9l)%j%J#0lfrZumvA<_w zu&=AVvdChf6}eqh(bUz`(`Ue*p01{fBAcTgKyDYLs_I+YyJEk+rM@avU~>fB$n)HS zM7pfJydu`i%gfS<{PF94kZDv$t>06sAkheDzu40NJ$5CMW%n^Lls?8^p^QGWURbKu3ZduZQZ((s2? zzE`}<{;Zt7<$C|9R8A~DJ~@%x>TfP zF>TX8)@v|t)q4GjRt<}5s6hLHwRel7>V@&r-O|Av(yh;Q1A{E>Ir>p+%dHD|=l+lT zpr(Dg&>#Nu=!)6bCLr-ZS%|;h)Ij$+e@r8_{qO19QvDe=&1tmpY*0lcA^Cc-#{9fQ z<~$*<&P$Q<_jy#<$40PMofM7aQ}C=jphI`4kLg}Z7CIN#26D{-4v-_CA-LiE@(%{y!BzsU%gG`Q?sjLUf%qFSl0y)2#ae*+EI>s|i`d^V$Dn)qmzqRq6VJRY|{4ujsIU%#bnqU6MR&-1I_43=|5(6Jr;Jvert) zE?S|Tmn}Tv<-??sxV5@9t}3D=>YZ0JrQe$CO~|EY=Lj9RM&4svQHPQL6%pV5fPFiH zfXDx;l@~et{*{U*#c#Dvzu)|znDO7$#CRx)Z&yp-}SrD{&|(MQtfUz~n35@RLfUy=aqrhCX0M}J_r5QsK~NmRCR|Nm&L z41UdsLjWxSUlL41r^0K&nCCK>fdR-!MYjFg(z9_mF^C|#ZQw?`)f6uVzF^`bRnVY& zo}@M06J&_+>w9@jpaO4snmU;0t-(zYW1qVBHtuD!d?%?AtN7Plp><-1Y8Rqb20ZaP zTCgn*-Sri4Q8Xn>=gNaWQ57%!D35UkA@ksOlPB*Dvw}t02ENAqw|kFhn%ZyyW%+t{ zNdM!uqEM^;2}f+tECHbwLmH*!nZVrb$-az%t50Y2pg(HqhvY-^-lb}>^6l{$jOI6} zo_kBzj%8aX|6H5M0Y<)7pzz_wLkIpRm!;PzY)9+24wk2&TT{w--phDGDCOz{cN_ca zpnm7`$oDy=HX%0i-`769*0M6(e5j-?(?24%)<)&46y0e&6@HCDZAm9W6Ib#Y#BF6- z=30crHGg+RRTe%VBC>T00OV6F+gQDAK38Ne3N9bm|62tPccBJi)5{B z4zc^Db72XiBd}v$CF|yU{Z=M|DZ%-(XarYNclODlb1Kz1_EKLy(NSLCN`eUl(rBCL zT*jx@wNvze0|TSqgE(QArOZU)_?qH(sj#TwzElLs9q)(0u!_P|R%Cy_0JFQxgGV>1 zz4?_uq<8_gM0`c*Hh|;UMz~vrg1gQXp{ufg`hM_qU;U>+zmvc5blCLSq@PrEBSGR# z&8=2Z4uXN`F3p73ueD1l{s{k$WipAvSh5W7ABe?4)t;r@V?y`bNB5FvBuE|0VRTb< zM1Hn^?DSsJY+sX@T5xW=#>T9VEV|?<(=6|ge$X6Sb05!LFdjDcoq*gM(Zq=t;_)Le&jyt(&9jzR73noru`a# zN*<`KwGa^gZU3-)MSLF0aFag#f0<>E(bYTeHmtdbns#|I)-$)mJ`q9ctQ8g0=ET?| zdO}eZ*b_p>ygRTtR^5Ggdam=Zb5wmd{}np+Jn1d_=M`~P=M67jj})fH4ztb5yQqQW z^C|C&^LHAK-u+ooIK)yM)QM?t;|<{P;;{`p=BclzAN#JzL4jCwXkQB1Dy{=^KR`=~ zTrr)y7eiYBzSNs_DvO=4A6#EgGS-zY%Vi)N*Yb`U;6o}KR}dq{r9pT5wqZ@3NOE8- z9-(}D|Nc5732CSYQbL)!gPQ#RbD8BhK3dl{sUuPvei0tkvnJBxDEAYTesU8H$)g(Plra{VH(v3u^CO1~(+ zU0O7#)jaS4{NcwA+LuSm&VBcX2#Im3xg)W}ySNw%->orn1taZ&+d)}8gJTqA!u|5P z{yv?zol_3|(1(%M(EVU=cp?L`{Pi|ixk{U)*guFML3P!OSlz;zGA#T+E@8@cgQ_mv1o7RSU=Zo_82F?&&2r;WE z@wk}JHYEZ9nYUc(Vv~iTCa3u8e4q(yq<29VoNbKk|`mq%I6u)My=gPIDuUb&lzf4`MEA9^g8u z)vp8|$$HE9m_BTV?lOosIGa4jud=jIbw)O2eCMfyw2*S8?hjWw^nqws$O*M$3I1)x zR0PWFb3$ySOcGTe1dz%N0l;RPc`x%05FtT^f^j{YCP}*Q=lvp4$ZXrTZQHhO+w%wJn3c8j%+5C3UAFD&%8dBl_qi9D5g8fry}6Ev z2_Q~)5^N$!IU`BPh1O|=BxQ#*C5*}`lluC515$lxc-vNC)IgW=K|=z7o%cWFpndn= zX}f{`!VK02_kU+Q5a3m37J;c} zTzbxteE{GNf?yLt5X=Bzc-mio^Up0nunMCgp*ZJ;%MJvPM3QK)BryP(_v@ei4UvHr z6+sbCifQaOkL6-;5fL8$W($zZ_;CZp305C;~$hhRquZr-r)jjd1z z31%ZK{-(`P#|Um_Sivn@p$-vz46uqT>QG0B1w9znfS9A8PB2LaHdzA|_)yjXVR*l{ zkcu3@vEf7bxH0nkh`q?8FmoO_Ucui*>_a~P?qQrlZ9@+D7%MTpSnztpylXrt5!-k8_QPB?YL8Kx_On8WD zgT+111d(Op$^$&KLAN5+@?>f7F4~wFi(8TL8+szgVmcMDTp5l&k6~=rA{Dt}!gb^r zSWY<)M7D|Z2P0cEodj6E42PV>&>DFmQpgt)E-|#sSUU@uKed+F680H@<;-x{p|nuH4!_mn85rx>wz;0mPi2ZkL#k6;sznu?cXh!T0S>{w6 zL^gvR05NY64l*<+_L>On$rjx9!US;l;LX6@z}yi#2XHh)F@Oo+l)h%fq$v}DNmF2> zfs^_t0)3N-W<9-N?uedVv{)-J0W5mh#29QM5R5h&KuiRM=0Zvnf#lF=K#WlCgc#9c zS;qvh(P$!_a8JwyhI^ZJV2k+B6Z^64?w|1?5gyo6y{}923CRZfYVe1#?F% z7h2SUiNO3;T#JUOyovSs@@C1GtwipycA=*x5{BpIZ_#GCMuV8XK=x;qCNy{d7?wA~ zC+=vjls;ci&zW=6$H~4^K%v{p}Ab?U%C6Z4p%eC<3ExqU$XR<}LLF67A$Sr20DR_pJ3yeBa~ z^sw{V0FI5;UpwXsScYuhbqGQ`YQ25;6p6W^+tgL&;Ml;>S3CGpSZ>VrTn0m1$y$HU z&65)I!c?oREz};c=nLCliriqQX->4uivHTgd${GqeAlf*!P^B|jkU|*IdNP(&6C>4 zqOW$)Nw9nvjy^&`?E|gotDV{JmJ9Q~vuhy<`^C4XIUDt|j4o6rK^e8_(=YqC zuaR6TRVf@tUFHB079o4MBIh{M~4>WwnGgesQH*3?w(RA%hCZ*7)b!aNV=yOQ%o_Y=Lt0Sl*(9^jfRnC210Om$=y>*o|3z} zAR&vAdrB#mWoaB0fJSw9xw|Am$fzK>rx-~R#7IFSAwdu_EI|SRfB*yl0w8oX09H^q zAjl2?0I)v*odGJ40FVGaF&2qJq9Gv`>V>2r0|c`GX8h>CX8eHcOy>S0@<;M3<_6UM z7yCEpug5NZL!H_0>Hg_HasQGxR`rY&Z{geOy?N92Z z{lER^um|$*?*G63*njwc(R?NT)Bei*3jVzR>FWUDb^gKhtL4A=kE_1p-%Fo2`!8M} z(0AjuCiS;G{?*^1tB-uY%=)SRx&D)pK4u@>f6@KPe3}2j_har$>HqzH;UCR^ssFD0 z7h+VLO4o@_Yt>>AeaZKUxqyvxWCAjKB>qjQ30UA)#w z&=RmdwlT`7a8J8Yae=7*c8XL|{@%wA8uvCqfsNX^?UZsS>wX}QD{K}ad4y~iO*p%4 z_cS{u7Ek%?WV6em2(U9#d8(&JDirb^u~7wK4+xP$iiI6IlD|a&S)6o=kG;59N|>K1 zn(0mUqbG3YIY7dQd+*4~)`!S9m7H6HP6YcKHhBc#b%1L}VIisp%;TckEkcu0>lo@u995$<*Em;XNodjTiCdC%R+TX|_ZR#|1`RR|`^@Teh zl#w@8fI1FTx2Dy+{blUT{`^kY*V-AZUd?ZZqCS4gW(kY5?retkLbF=>p=59Nl|=sf zo1Pc|{{N4>5nt#627ylGF`3n>X%`w%bw-Y~zWM_{Si$dc82|=YhISal{N7OY?O`C4 zD|qb}6nLWJ`hUyL+E>-;ricg9J@ZNYP(x(Sct&OI$Y!QWr*=^VN;G3#i>^1n4e#Je zOVhbFbLpXVu*16enDM+ic;97@R~u&kh__kgP#!R`*rQEnA+_dLkNP~L`0alC|J;c; zeiK=s8;BsLE)KbG3BD&Br@(Ha@SBT&$?xX`=$;eeel=|R_dIr6-Ro?=HEjnsJ_b`1 zK6Yg^-6;^2aW!xeTK)A~3Rm|L^FCHB_I>jIju7ZGo&N_1*QHkxH2!!%@o4iZ?vntS;&zJdPe1dH#04YD93A44o-MpfD zP{rn_aq>U%RDvC2+bp;xPlsOzauIi3*Lf42`jVKKZCRuKdYhi>FDuL2l=v{$BCN#Q6796s%r-AG$Q^t(3c@ zD?w0UhYr11@feiyl9kY_@H8~|xlmO<8PfQmj1!$@WieW@VxR@Psxfe-v9WCi1+f>F4VL?0O~K7T?m4-u|pSkBpUJZZe*16_wAp zSYZ@;k`3;W3UHKUWc8QeI}0jH5Ly=cGWQPw(Kr2fm=-5L(d`lcXofy8tJY3@Tuadz zYWXR{mW7XT!RF#RVCe%}=tM*O6!AD3^(!8un~opNI%Uko7$5t@<8+?; zTxDys(MyyGsUjtSu9$+|_-t!U3fVb1dkK?l`17<+jfl=hrBHnDSV>^R1=TnQeyqbW z>ov#l%!1|S!1>8UUxIdhQq`_klcHVx0{?#>K3#$4GlXncwldt!g17TcvKq-jo_996 z>oA=tH9CqRl6Yw?Uc`am!V?lHJbizOJaVaScf1UP5e7Dbgabq=b!B~T&_F6?ooU>w%x0A zH~&MHJ=q`fCH{U<7MDXE4SD32cDZA)WJeWkllJ`UspWaS#eDe^kg^oU_A14UE9zG-a^g{xaXf$})Wik>gT zl#dkzGr(;h0JZDuFn(+k8wNq?PZ5grQ<+sM?wBGt@JnH6v0#or-5wBQWKU~(S_> zkE!tc*ZJ1Y&*p(xX84POb3cClRMd!^qJ#CAZfIepEj-<`VURS_yCz0(?*Ixcj4 z-!zV1_QZhpm=0<;*(nm+F>T=)o?ep@CK5I%g^VAA+RB25ab?7)A~z~egru=I1S|@v zH7tXV!0wmGS^qj#e+MY;C5eUjEAp$Y?LDkS^QPZ}8WN85?r$u<-Epi;yZ1|J2J`se z$D6DpH~2F=eI0B&=UFAUnJvZAmClJlK)sutJ?M>xpZiWV&0=G4MZP+x+p>EX=HbCz zxls%Mw?*u^;LbHWIWCyq+yi)`GmFn9J112CZda_u@YIP%i;srFg_paU02Ifij*7}l z&CF-(3|>*a|+vbNR`^RP=9G?ymEJ0Z~)d&c*UE$UMepZ zcITr{0WqhxkjUnM15js_gW=e3Uh|y6ZReaXHIz-=p`x5VvB&rH9y>Amv@^WmXFEw) zQXYrk3feir=a{jMQ+wDIkkFnZ$k{sJakHn*?u za%4b!00ev8NVLM1TY=cl?KB&55BY_MU-sg?c>=Dbz_W{(Z~c?HJi*XpYL)C6Bd8WH zt+v-#0&o~@t4qESi*)+eW%@VD0|o^yF)n0hME$UtXF$*Lvh}7sso{`|pn*JDIy5^Fm3s$5*zEE=?u5<=l8FJc3r%+H} zdfoNl2J0^~!-*mOL5o-x32|e0Im*E!yY7F7E5N)W3>+v_LBydlEx?4$RL5f2oYRD# zaR0wv(-p~wO0eLDl3K=%`{5+0Gd$ktO=W)gWlGZJ0`K z$_RNA=ckrfa;H0KA~dR^p�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s literal 0 HcmV?d00001 diff --git a/AI Video Clipper/app/src/main/res/values/colors.xml b/AI Video Clipper/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..09837df --- /dev/null +++ b/AI Video Clipper/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/AI Video Clipper/app/src/main/res/values/prompts.xml b/AI Video Clipper/app/src/main/res/values/prompts.xml new file mode 100644 index 0000000..016ea87 --- /dev/null +++ b/AI Video Clipper/app/src/main/res/values/prompts.xml @@ -0,0 +1,50 @@ + + + + You are an expert video analyst and cinematic editor. Your task is to analyze the provided videos and recommend how to edit them for maximum impact. + + Please provide two things: + 1. Identify the single most engaging, climactic, or visually striking segment in EACH video provided in the "FILES TO ANALYZE" list. + 2. Following the user\'s intent, suggest a sequence of these segments and return them in a JSON object. + + Analyze movement, lighting, subject focus, and emotional peaks for each video. + + Your final output must be a single JSON object containing an entry for EVERY video listed. + + 1. JSON Structure for Response + Your primary output format is a JSON object containing an array with video URIs and the most engaging segment of the video. + + CRITICAL: The "uri" field for each object in the "videos" array MUST be an EXACT string match to one of the URIs provided in the "FILES TO ANALYZE" section. Do not shorten, change, or invent URIs. + + -BEGIN JSON STRUCTURE DEFINITION- + + { + "videos": [ + { + "uri": "string", + "mostEngagingSegment": { + "startMs": "long", + "endMs": "long", + "reasoning": "string", + }, + } + ] + } + -END JSON STRUCTURE DEFINITION- + + 2. Error Handling + If you cannot fulfill the user\'s request, return a JSON object with a single error key. The + explanation must be human-readable and at most 15 words. + + -BEGIN ERROR TEMPLATE- + + { + "error": "A brief explanation of why the request could not be fulfilled." + } + -END ERROR TEMPLATE- + + Desired user intent: %1$s + FILES TO ANALYZE: %2$s + CRITICAL: You must return a JSON object where the \'uri\' field for each entry exactly matches one of the \'gs://\' URIs listed above. + + \ No newline at end of file diff --git a/AI Video Clipper/app/src/main/res/values/strings.xml b/AI Video Clipper/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..3d890fb --- /dev/null +++ b/AI Video Clipper/app/src/main/res/values/strings.xml @@ -0,0 +1,12 @@ + + Gemini Video Editing + Video Selected + No Media Selected + Choose videos from your device to begin the AI editing process. + %1$d video(s) selected + Select Videos + Describe your intent / e.g. create a 15-second highlight reel + Edit with Gemini + Gemini is processing your video... + Applying AI edits ... + \ No newline at end of file diff --git a/AI Video Clipper/app/src/main/res/values/themes.xml b/AI Video Clipper/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..ecb1e56 --- /dev/null +++ b/AI Video Clipper/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +