diff --git a/AI Video Clipper/README.md b/AI Video Clipper/README.md
new file mode 100644
index 0000000..e3de518
--- /dev/null
+++ b/AI Video Clipper/README.md
@@ -0,0 +1,63 @@
+# Gemini Video Editing App
+
+---
+An Android application demonstrating automated video highlight generation and intelligent cinematic editing suggestion using Vertex AI for Firebase (Gemini 2.5 Pro) and Firebase Cloud Storage.
+Users can select multiple videos, specify editing goals in natural language (e.g., "create a fast-paced 15-second action reel"), upload them to Cloud Storage, and let Gemini analyze the content to recommend and preview video trims and edits.
+
+## Prerequisites & Project Setup
+This project uses **Firebase Cloud Storage** to host video files and **Vertex AI for Firebase** (Gemini 2.5 Pro) for advanced media analysis and editing recommendations. Follow the setup steps below to configure your Firebase and Google Cloud project.
+### 1. Firebase Project Configuration
+1. Go to the [Firebase Console](https://console.firebase.google.com/).
+2. Click **Add project** and create a new project (or select an existing one).
+3. Vertex AI for Firebase requires the project to be on the pay-as-you-go **Blaze plan**. In the Firebase Console, upgrade your project by clicking **Upgrade** in the bottom-left corner.
+4. Register your Android App:
+ - Click the Android icon to add a new app.
+ - Enter the Android package name: `com.example.videoediting`.
+ - Click **Register app**.
+5. Download the `google-services.json` configuration file and place it in the `app/` directory of this project:
+ ```path
+ /app/google-services.json
+ ```
+---
+### 2. Enable Firebase Storage
+1. In the Firebase Console sidebar, go to **Build** > **Storage**.
+2. Click **Get Started**.
+3. Select a starting rule configuration (e.g., test mode) and choose your Storage location (e.g., `us-central1`).
+4. Once created, note your bucket URI (formatted as `gs://YOUR_PROJECT_ID.firebasestorage.app` or `gs://YOUR_PROJECT_ID.appspot.com`).
+5. **Create a `videos` folder**:
+ - Within the Storage console dashboard, click the **New folder** icon.
+ - Name the folder `videos`.
+ - *Note: While the app programmatically creates this folder upon upload, creating it manually ensures it is initialized for your project.*
+6. Set the **Storage Rules** to allow read/write access for uploads. For example, during development:
+ ```javascript
+ rules_version = '2';
+ service firebase.storage {
+ match /b/{bucket}/o {
+ match /videos/{allPaths=**} {
+ allow read, write: if true;
+ }
+ }
+ }
+ ```
+---
+### 3. Enable Vertex AI for Firebase (AI Logic)
+1. In the Firebase Console sidebar, go to **Build** > **Vertex AI** (or **Build with Gemini**).
+2. Click **Get Started** and follow the prompts.
+3. This activates the necessary Vertex AI APIs on the underlying Google Cloud project and sets up billing integration.
+---
+### 4. Configure Google Cloud Storage Permissions for Vertex AI
+Since Gemini processes video files directly from Google Cloud Storage, the Google Cloud Vertex AI service agent requires permission to read objects from your Storage bucket.
+1. Find your **Google Cloud Project Number**:
+ - In the Firebase Console, click the Gear icon next to **Project Overview** and select **Project Settings**.
+ - Copy the **Project number** (e.g., `123456789012`).
+2. Open the [Google Cloud Console IAM Page](https://console.cloud.google.com/iam-admin/iam).
+3. Ensure you are in the correct project.
+4. Check the **Include Google-provided role grants** box in the top-right corner of the IAM principal list.
+5. Search for the **Vertex AI Service Agent** service account:
+ ```text
+ service-PROJECT_NUMBER@gcp-sa-aiplatform.iam.gserviceaccount.com
+ ```
+ *(Replace `PROJECT_NUMBER` with your actual project number).*
+6. Click the edit icon (pencil) next to this service account principal to modify its roles.
+7. Click **Add another role** and select **Storage Object Viewer** (`roles/storage.objectViewer`).
+8. Save the permissions.
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 0000000..c209e78
Binary files /dev/null and b/AI Video Clipper/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
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 0000000..b2dfe3d
Binary files /dev/null and b/AI Video Clipper/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
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 0000000..4f0f1d6
Binary files /dev/null and b/AI Video Clipper/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/AI Video Clipper/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/AI Video Clipper/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/AI Video Clipper/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
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 0000000..948a307
Binary files /dev/null and b/AI Video Clipper/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
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 0000000..1b9a695
Binary files /dev/null and b/AI Video Clipper/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/AI Video Clipper/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/AI Video Clipper/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/AI Video Clipper/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/AI Video Clipper/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/AI Video Clipper/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/AI Video Clipper/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/AI Video Clipper/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/AI Video Clipper/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/AI Video Clipper/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/AI Video Clipper/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/AI Video Clipper/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/AI Video Clipper/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
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 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AI Video Clipper/app/src/main/res/xml/backup_rules.xml b/AI Video Clipper/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..2391320
--- /dev/null
+++ b/AI Video Clipper/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/AI Video Clipper/app/src/main/res/xml/data_extraction_rules.xml b/AI Video Clipper/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..c6c3bb0
--- /dev/null
+++ b/AI Video Clipper/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AI Video Clipper/app/src/test/java/com/example/videoediting/ExampleUnitTest.kt b/AI Video Clipper/app/src/test/java/com/example/videoediting/ExampleUnitTest.kt
new file mode 100644
index 0000000..5553392
--- /dev/null
+++ b/AI Video Clipper/app/src/test/java/com/example/videoediting/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.example.videoediting
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/AI Video Clipper/build.gradle.kts b/AI Video Clipper/build.gradle.kts
new file mode 100644
index 0000000..758f63b
--- /dev/null
+++ b/AI Video Clipper/build.gradle.kts
@@ -0,0 +1,6 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.compose) apply false
+ id("com.google.gms.google-services") version "4.4.4" apply false
+}
\ No newline at end of file
diff --git a/AI Video Clipper/gradle.properties b/AI Video Clipper/gradle.properties
new file mode 100644
index 0000000..34c5e9e
--- /dev/null
+++ b/AI Video Clipper/gradle.properties
@@ -0,0 +1,15 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
\ No newline at end of file
diff --git a/AI Video Clipper/gradle/gradle-daemon-jvm.properties b/AI Video Clipper/gradle/gradle-daemon-jvm.properties
new file mode 100644
index 0000000..6c1139e
--- /dev/null
+++ b/AI Video Clipper/gradle/gradle-daemon-jvm.properties
@@ -0,0 +1,12 @@
+#This file is generated by updateDaemonJvm
+toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
+toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
+toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
+toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
+toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect
+toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect
+toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
+toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
+toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect
+toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect
+toolchainVersion=21
diff --git a/AI Video Clipper/gradle/libs.versions.toml b/AI Video Clipper/gradle/libs.versions.toml
new file mode 100644
index 0000000..330c8a7
--- /dev/null
+++ b/AI Video Clipper/gradle/libs.versions.toml
@@ -0,0 +1,50 @@
+[versions]
+agp = "9.2.1"
+coreKtx = "1.19.0"
+firebaseBom = "34.14.1"
+junit = "4.13.2"
+junitVersion = "1.3.0"
+espressoCore = "3.7.0"
+lifecycleRuntimeKtx = "2.10.0"
+activityCompose = "1.13.0"
+kotlin = "2.4.0"
+composeBom = "2026.05.01"
+media3 = "1.10.1"
+kotlinxSerializationJson = "1.11.0"
+kotlinxCoroutines = "1.11.0"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
+firebase-ai = { group = "com.google.firebase", name = "firebase-ai" }
+firebase-common = { group = "com.google.firebase", name = "firebase-common" }
+firebase-storage = { group = "com.google.firebase", name = "firebase-storage" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
+androidx-media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3" }
+androidx-media3-effect = { group = "androidx.media3", name = "media3-effect", version.ref = "media3" }
+androidx-media3-transformer = { group = "androidx.media3", name = "media3-transformer", version.ref = "media3" }
+androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }
+androidx-media3-inspector = { group = "androidx.media3", name = "media3-inspector", version.ref = "media3" }
+androidx-media3-ui-compose = { group = "androidx.media3", name = "media3-ui-compose", version.ref = "media3" }
+kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
+kotlinx-coroutines-guava = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-guava", version.ref = "kotlinxCoroutines" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+
diff --git a/AI Video Clipper/gradle/wrapper/gradle-wrapper.jar b/AI Video Clipper/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8bdaf60
Binary files /dev/null and b/AI Video Clipper/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/AI Video Clipper/gradle/wrapper/gradle-wrapper.properties b/AI Video Clipper/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..9518354
--- /dev/null
+++ b/AI Video Clipper/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,9 @@
+#Mon Apr 20 12:42:37 BST 2026
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/AI Video Clipper/gradlew b/AI Video Clipper/gradlew
new file mode 100755
index 0000000..ef07e01
--- /dev/null
+++ b/AI Video Clipper/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH="\\\"\\\""
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/AI Video Clipper/gradlew.bat b/AI Video Clipper/gradlew.bat
new file mode 100644
index 0000000..5eed7ee
--- /dev/null
+++ b/AI Video Clipper/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/AI Video Clipper/settings.gradle.kts b/AI Video Clipper/settings.gradle.kts
new file mode 100644
index 0000000..bc1878b
--- /dev/null
+++ b/AI Video Clipper/settings.gradle.kts
@@ -0,0 +1,26 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+plugins {
+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "Gemini Video Editing"
+include(":app")