Skip to content

Commit e2f8874

Browse files
authored
Added YouTube summarizer app
2 parents bac662b + 31f41e4 commit e2f8874

63 files changed

Lines changed: 4842 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/android-leap-chat-test.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ on:
44
branches: [ main ]
55
paths:
66
- 'Android/LeapChat/**'
7+
- 'Android/YouTubeSummarizer/**'
78
- '.github/workflows/android-leap-chat-test.yml'
89
pull_request:
910
branches: [ main ]
1011
paths:
1112
- 'Android/LeapChat/**'
13+
- 'Android/YouTubeSummarizer/**'
1214
- '.github/workflows/android-leap-chat-test.yml'
1315
workflow_dispatch:
1416

@@ -54,3 +56,18 @@ jobs:
5456
--device model=MediumPhone.arm,version=36,locale=en,orientation=portrait \
5557
--project liquid-leap
5658
59+
unit-test-youtube-summarizer:
60+
runs-on: ubuntu-24.04
61+
steps:
62+
- uses: actions/checkout@v6
63+
with:
64+
persist-credentials: false
65+
- name: Set up JDK 21
66+
uses: actions/setup-java@v5
67+
with:
68+
java-version: '21'
69+
distribution: 'temurin'
70+
- name: Set up Gradle
71+
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
72+
- name: Run YouTubeSummarizer unit tests
73+
run: cd Android/YouTubeSummarizer && ./gradlew :app:test
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Add local.properties to gitignore
2+
local.properties
3+
4+
# Gradle
5+
.gradle/
6+
build/
7+
8+
# Android Studio
9+
.idea/
10+
.kotlin/
11+
*.iml
12+
*.iws
13+
*.ipr
14+
captures/
15+
.externalNativeBuild/
16+
.cxx/
17+
18+
# Signing configs
19+
*.jks
20+
*.keystore
21+
22+
# Generated files
23+
app/release/
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# tl;dw
2+
TLDW (Too Long Didn't Watch) is a summary generation app for YouTube videos.
3+
4+
# Demo
5+
6+
https://github.com/user-attachments/assets/0ba2de47-b673-4c53-b1fa-367dce514fe7
7+
8+
9+
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
plugins {
2+
alias(libs.plugins.android.application)
3+
alias(libs.plugins.kotlin.android)
4+
alias(libs.plugins.kotlin.compose)
5+
alias(libs.plugins.ktfmt)
6+
}
7+
8+
kotlin { jvmToolchain(17) }
9+
10+
ktfmt { googleStyle() }
11+
12+
android {
13+
namespace = "com.tldw.app"
14+
compileSdk = 36
15+
16+
defaultConfig {
17+
applicationId = "com.tldw.app"
18+
minSdk = 31
19+
targetSdk = 36
20+
versionCode = 1
21+
versionName = "1.0"
22+
}
23+
24+
buildTypes {
25+
release {
26+
isMinifyEnabled = false
27+
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
28+
}
29+
}
30+
31+
buildFeatures { compose = true }
32+
}
33+
34+
dependencies {
35+
implementation(libs.androidx.core.ktx)
36+
implementation(libs.androidx.lifecycle.runtime.ktx)
37+
implementation(libs.androidx.lifecycle.runtime.compose)
38+
implementation(libs.androidx.lifecycle.viewmodel.compose)
39+
implementation(libs.androidx.activity.compose)
40+
implementation(platform(libs.androidx.compose.bom))
41+
implementation(libs.androidx.ui)
42+
implementation(libs.androidx.ui.graphics)
43+
implementation(libs.androidx.ui.tooling.preview)
44+
implementation(libs.androidx.material3)
45+
implementation(libs.androidx.material.icons.extended)
46+
implementation(libs.kotlinx.coroutines.android)
47+
implementation(libs.okhttp)
48+
implementation(libs.gson)
49+
implementation(libs.leap.sdk.android)
50+
implementation(libs.leap.model.downloader)
51+
implementation(libs.coil.compose)
52+
implementation(libs.compose.markdown)
53+
54+
debugImplementation(libs.androidx.ui.tooling)
55+
debugImplementation(libs.androidx.ui.test.manifest)
56+
57+
testImplementation(libs.junit)
58+
testImplementation(libs.kotlinx.coroutines.test)
59+
testImplementation(libs.mockito.kotlin)
60+
testImplementation(libs.okhttp.mockwebserver)
61+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Default ProGuard rules file
2+
# See http://proguard.sourceforge.net/manual/usage.html for usage
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<uses-permission android:name="android.permission.INTERNET" />
5+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
6+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
7+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
8+
9+
<application
10+
android:allowBackup="true"
11+
android:icon="@mipmap/ic_launcher_round"
12+
android:label="@string/app_name"
13+
android:roundIcon="@mipmap/ic_launcher_round"
14+
android:supportsRtl="true"
15+
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar">
16+
17+
<activity
18+
android:name=".MainActivity"
19+
android:exported="true"
20+
android:label="@string/app_name"
21+
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar">
22+
23+
<!-- Main launcher intent -->
24+
<intent-filter>
25+
<action android:name="android.intent.action.MAIN" />
26+
<category android:name="android.intent.category.LAUNCHER" />
27+
</intent-filter>
28+
29+
<!-- Share receiver: handles URLs shared from YouTube and other apps -->
30+
<intent-filter>
31+
<action android:name="android.intent.action.SEND" />
32+
<category android:name="android.intent.category.DEFAULT" />
33+
<data android:mimeType="text/plain" />
34+
</intent-filter>
35+
36+
</activity>
37+
38+
</application>
39+
40+
</manifest>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.tldw.app
2+
3+
import com.tldw.app.domain.model.VideoInfo
4+
5+
object Consts {
6+
const val MODEL_NAME = "LFM2-350M"
7+
const val QUANTIZATION_SLUG = "Q8_0"
8+
const val TLDR_SYSTEM_PROMPT =
9+
"You are a helpful assistant. Given a YouTube video transcript, produce a concise TL;DR summary in 3-5 bullet points."
10+
const val MAX_TRANSCRIPT_CHARS = 4000
11+
12+
val VIDEO_PRINTING_PRESS =
13+
VideoInfo(
14+
videoId = "Y0NopNiSkKw",
15+
title = "The Complete History of the Printing Press | How Printing Changed the World",
16+
channelName = "History of Innovations",
17+
durationSeconds = 595L,
18+
viewCount = 425L,
19+
)
20+
21+
val VIDEO_SLEEP_SUPERPOWER =
22+
VideoInfo(
23+
videoId = "5MuIMqhT8DM",
24+
title = "Sleep Is Your Superpower | Matt Walker | TED",
25+
channelName = "TED",
26+
durationSeconds = 1158L,
27+
viewCount = 16505865L,
28+
)
29+
30+
val VIDEO_BUILD_GPT =
31+
VideoInfo(
32+
videoId = "kCc8FmEb1nY",
33+
title = "Let's build GPT: from scratch, in code, spelled out.",
34+
channelName = "Andrej Karpathy",
35+
durationSeconds = 6980L,
36+
viewCount = 7168320L,
37+
)
38+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.tldw.app
2+
3+
import android.content.Intent
4+
import android.os.Bundle
5+
import androidx.activity.ComponentActivity
6+
import androidx.activity.compose.setContent
7+
import androidx.activity.enableEdgeToEdge
8+
import androidx.activity.viewModels
9+
import androidx.compose.foundation.layout.Box
10+
import androidx.compose.foundation.layout.fillMaxSize
11+
import androidx.compose.material3.CircularProgressIndicator
12+
import androidx.compose.material3.Surface
13+
import androidx.compose.runtime.getValue
14+
import androidx.compose.ui.Alignment
15+
import androidx.compose.ui.Modifier
16+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
17+
import com.tldw.app.data.repository.ModelRepositoryImpl
18+
import com.tldw.app.domain.usecase.CheckModelDownloadedUseCase
19+
import com.tldw.app.ui.modelscreen.ModelDownloadScreenRoute
20+
import com.tldw.app.ui.theme.TldwTheme
21+
import com.tldw.app.ui.transcriptscreen.TranscriptScreenRoute
22+
23+
class MainActivity : ComponentActivity() {
24+
25+
val mainViewModel: MainViewModel by viewModels {
26+
val repository = ModelRepositoryImpl(applicationContext)
27+
MainViewModelFactory(CheckModelDownloadedUseCase(repository))
28+
}
29+
30+
override fun onCreate(savedInstanceState: Bundle?) {
31+
super.onCreate(savedInstanceState)
32+
enableEdgeToEdge()
33+
34+
val sharedUrl = resolveSharedUrl(intent)
35+
36+
setContent {
37+
TldwTheme {
38+
Surface(modifier = Modifier.fillMaxSize()) {
39+
val state by mainViewModel.state.collectAsStateWithLifecycle()
40+
41+
when {
42+
state.isCheckingStatus -> {
43+
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
44+
CircularProgressIndicator()
45+
}
46+
}
47+
state.isModelDownloaded -> {
48+
TranscriptScreenRoute(
49+
sharedUrl = sharedUrl,
50+
onNavigateToModelDownload = { mainViewModel.checkStatus() },
51+
)
52+
}
53+
else -> {
54+
ModelDownloadScreenRoute(onModelReady = { mainViewModel.checkStatus() })
55+
}
56+
}
57+
}
58+
}
59+
}
60+
}
61+
62+
private fun resolveSharedUrl(intent: Intent): String? =
63+
if (intent.action == Intent.ACTION_SEND && intent.type == "text/plain") {
64+
intent.getStringExtra(Intent.EXTRA_TEXT)
65+
} else {
66+
null
67+
}
68+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.tldw.app
2+
3+
import androidx.lifecycle.ViewModel
4+
import androidx.lifecycle.viewModelScope
5+
import com.tldw.app.domain.usecase.CheckModelDownloadedUseCase
6+
import kotlinx.coroutines.flow.MutableStateFlow
7+
import kotlinx.coroutines.flow.StateFlow
8+
import kotlinx.coroutines.flow.asStateFlow
9+
import kotlinx.coroutines.flow.update
10+
import kotlinx.coroutines.launch
11+
12+
data class MainState(val isCheckingStatus: Boolean = true, val isModelDownloaded: Boolean = false)
13+
14+
class MainViewModel(private val checkModelDownloadedUseCase: CheckModelDownloadedUseCase) :
15+
ViewModel() {
16+
17+
private val _state = MutableStateFlow(MainState())
18+
val state: StateFlow<MainState> = _state.asStateFlow()
19+
20+
init {
21+
checkStatus()
22+
}
23+
24+
fun checkStatus() {
25+
viewModelScope.launch {
26+
_state.update { it.copy(isCheckingStatus = true) }
27+
try {
28+
val isDownloaded = checkModelDownloadedUseCase()
29+
_state.update { it.copy(isCheckingStatus = false, isModelDownloaded = isDownloaded) }
30+
} catch (e: Exception) {
31+
// If it fails, assume not downloaded or handle gracefully.
32+
_state.update { it.copy(isCheckingStatus = false, isModelDownloaded = false) }
33+
}
34+
}
35+
}
36+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.tldw.app
2+
3+
import androidx.lifecycle.ViewModel
4+
import androidx.lifecycle.ViewModelProvider
5+
import com.tldw.app.domain.usecase.CheckModelDownloadedUseCase
6+
7+
class MainViewModelFactory(private val checkModelDownloadedUseCase: CheckModelDownloadedUseCase) :
8+
ViewModelProvider.Factory {
9+
@Suppress("UNCHECKED_CAST")
10+
override fun <T : ViewModel> create(modelClass: Class<T>): T {
11+
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
12+
return MainViewModel(checkModelDownloadedUseCase) as T
13+
}
14+
throw IllegalArgumentException("Unknown ViewModel class")
15+
}
16+
}

0 commit comments

Comments
 (0)