Skip to content

Commit f3280f3

Browse files
Updates regarding LLM's in the application, Version 3.1.0
1 parent 3541ea3 commit f3280f3

28 files changed

Lines changed: 1010 additions & 40 deletions

app/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,17 @@ dependencies {
120120
// DataStore
121121
implementation(libs.androidx.datastore.preferences)
122122

123+
// Room
124+
implementation(libs.androidx.room.runtime)
125+
implementation(libs.androidx.room.ktx)
126+
ksp(libs.androidx.room.compiler)
127+
123128
// Splash Screen
124129
implementation(libs.androidx.core.splashscreen)
125130

131+
// Networking
132+
implementation(libs.okhttp)
133+
126134
// Oboe
127135
implementation(libs.oboe)
128136

app/src/main/java/com/thivyanstudios/hark/MainActivity.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.thivyanstudios.hark
22

33
import android.annotation.SuppressLint
4+
import android.content.Intent
45
import android.os.Build
56
import android.os.Bundle
67
import android.view.MotionEvent
@@ -61,7 +62,7 @@ class MainActivity : ComponentActivity() {
6162

6263
// Keep splash screen until the app is ready to draw
6364
splashScreen.setKeepOnScreenCondition {
64-
false
65+
mainViewModel.uiState.value.isLoading
6566
}
6667

6768
enableEdgeToEdge()
@@ -101,16 +102,29 @@ class MainActivity : ComponentActivity() {
101102
HarkAppContent(
102103
uiState = uiState,
103104
snackbarHostState = snackbarHostState,
105+
mainViewModel = mainViewModel,
104106
settingsViewModel = settingsViewModel,
105107
onToggleStreaming = { toggleStreaming() },
106-
onClearTranscription = { mainViewModel.clearTranscription() }
108+
onClearTranscription = { mainViewModel.clearTranscription() },
109+
onShareTranscription = { text -> shareTranscription(text) }
107110
)
108111
}
109112
}
110113

111114
audioServiceManager.startService()
112115
}
113116

117+
private fun shareTranscription(text: String) {
118+
val sendIntent: Intent = Intent().apply {
119+
action = Intent.ACTION_SEND
120+
putExtra(Intent.EXTRA_TEXT, text)
121+
type = "text/plain"
122+
}
123+
124+
val shareIntent = Intent.createChooser(sendIntent, getString(R.string.share_transcription_title))
125+
startActivity(shareIntent)
126+
}
127+
114128
private fun toggleStreaming() {
115129
HarkLog.i("MainActivity", "Toggle streaming button clicked")
116130
if (permissionManager.hasPermissions()) {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.thivyanstudios.hark.data
2+
3+
import com.thivyanstudios.hark.data.local.TranscriptionDao
4+
import com.thivyanstudios.hark.data.local.TranscriptionEntity
5+
import kotlinx.coroutines.flow.Flow
6+
import javax.inject.Inject
7+
import javax.inject.Singleton
8+
9+
@Singleton
10+
class TranscriptionRepository @Inject constructor(
11+
private val transcriptionDao: TranscriptionDao
12+
) {
13+
val allTranscriptions: Flow<List<TranscriptionEntity>> = transcriptionDao.getAllTranscriptions()
14+
15+
suspend fun insert(text: String) {
16+
if (text.isBlank()) return
17+
transcriptionDao.insertTranscription(TranscriptionEntity(text = text))
18+
}
19+
20+
suspend fun deleteAll() {
21+
transcriptionDao.deleteAll()
22+
}
23+
24+
suspend fun delete(id: Long) {
25+
transcriptionDao.deleteById(id)
26+
}
27+
}

app/src/main/java/com/thivyanstudios/hark/data/UserPreferencesRepository.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ class UserPreferencesRepository @Inject constructor(
3838
val WHISPER_THREADS = androidx.datastore.preferences.core.intPreferencesKey("whisper_threads")
3939
val WHISPER_LANGUAGE = androidx.datastore.preferences.core.stringPreferencesKey("whisper_language")
4040
val WHISPER_TRANSLATE = booleanPreferencesKey("whisper_translate")
41+
val SILENCE_THRESHOLD = floatPreferencesKey("silence_threshold")
42+
val TRANSCRIPTION_FONT_SIZE = floatPreferencesKey("transcription_font_size")
43+
val SELECTED_MODEL_ID = androidx.datastore.preferences.core.stringPreferencesKey("selected_model_id")
4144
}
4245

4346
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
@@ -61,7 +64,10 @@ class UserPreferencesRepository @Inject constructor(
6164
transcriptModeEnabled = preferences[PreferenceKeys.TRANSCRIPT_MODE_ENABLED] ?: false,
6265
whisperThreads = preferences[PreferenceKeys.WHISPER_THREADS] ?: 4,
6366
whisperLanguage = preferences[PreferenceKeys.WHISPER_LANGUAGE] ?: "en",
64-
whisperTranslate = preferences[PreferenceKeys.WHISPER_TRANSLATE] ?: false
67+
whisperTranslate = preferences[PreferenceKeys.WHISPER_TRANSLATE] ?: false,
68+
silenceThreshold = preferences[PreferenceKeys.SILENCE_THRESHOLD] ?: 0.005f,
69+
transcriptionFontSize = preferences[PreferenceKeys.TRANSCRIPTION_FONT_SIZE] ?: 22f,
70+
selectedModelId = preferences[PreferenceKeys.SELECTED_MODEL_ID] ?: "ggml-base-q8_0.bin"
6571
)
6672
}
6773
.flowOn(ioDispatcher)
@@ -110,6 +116,18 @@ class UserPreferencesRepository @Inject constructor(
110116
it[PreferenceKeys.WHISPER_TRANSLATE] = isEnabled
111117
}
112118

119+
suspend fun setSilenceThreshold(threshold: Float) = update {
120+
it[PreferenceKeys.SILENCE_THRESHOLD] = threshold
121+
}
122+
123+
suspend fun setTranscriptionFontSize(size: Float) = update {
124+
it[PreferenceKeys.TRANSCRIPTION_FONT_SIZE] = size
125+
}
126+
127+
suspend fun setSelectedModelId(id: String) = update {
128+
it[PreferenceKeys.SELECTED_MODEL_ID] = id
129+
}
130+
113131
private suspend fun update(action: (MutablePreferences) -> Unit) {
114132
try {
115133
dataStore.edit { action(it) }
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package com.thivyanstudios.hark.data
2+
3+
import android.content.Context
4+
import com.thivyanstudios.hark.data.model.WhisperModel
5+
import com.thivyanstudios.hark.di.IoDispatcher
6+
import com.thivyanstudios.hark.util.HarkLog
7+
import dagger.hilt.android.qualifiers.ApplicationContext
8+
import kotlinx.coroutines.CoroutineDispatcher
9+
import kotlinx.coroutines.flow.MutableStateFlow
10+
import kotlinx.coroutines.flow.StateFlow
11+
import kotlinx.coroutines.flow.asStateFlow
12+
import kotlinx.coroutines.withContext
13+
import okhttp3.OkHttpClient
14+
import okhttp3.Request
15+
import java.io.File
16+
import java.io.FileOutputStream
17+
import javax.inject.Inject
18+
import javax.inject.Singleton
19+
20+
@Singleton
21+
class WhisperModelManager @Inject constructor(
22+
@ApplicationContext private val context: Context,
23+
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
24+
) {
25+
private val okHttpClient = OkHttpClient()
26+
27+
private val _downloadProgress = MutableStateFlow<Map<String, Float>>(emptyMap())
28+
val downloadProgress: StateFlow<Map<String, Float>> = _downloadProgress.asStateFlow()
29+
30+
private val _modelStoreUpdateTrigger = MutableStateFlow(System.currentTimeMillis())
31+
val modelStoreUpdateTrigger: StateFlow<Long> = _modelStoreUpdateTrigger.asStateFlow()
32+
33+
fun notifyModelStoreChanged() {
34+
_modelStoreUpdateTrigger.value = System.currentTimeMillis()
35+
}
36+
37+
val availableModels = listOf(
38+
WhisperModel(
39+
id = "ggml-base-q8_0.bin",
40+
name = "Base (Standard)",
41+
description = "Good balance of speed and accuracy. Best for most devices.",
42+
url = "", // Included in assets
43+
sizeBytes = 77_000_000L,
44+
isAsset = true
45+
),
46+
WhisperModel(
47+
id = "ggml-tiny.en-q8_0",
48+
name = "Tiny (Fastest)",
49+
description = "Lowest battery usage, but less accurate.",
50+
url = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en-q8_0.bin",
51+
sizeBytes = 31_000_000L
52+
),
53+
WhisperModel(
54+
id = "ggml-base.en-q8_0",
55+
name = "Base English (Improved)",
56+
description = "Optimized for English speech.",
57+
url = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en-q8_0.bin",
58+
sizeBytes = 77_000_000L
59+
),
60+
WhisperModel(
61+
id = "ggml-large-v3-turbo-q5_0",
62+
name = "Turbo (Flagship Only)",
63+
description = "Maximum accuracy. Requires a modern, powerful device.",
64+
url = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo-q5_0.bin",
65+
sizeBytes = 550_000_000L
66+
)
67+
)
68+
69+
fun isModelDownloaded(model: WhisperModel): Boolean {
70+
if (model.isAsset) return true
71+
val file = File(context.filesDir, model.fileName)
72+
return file.exists() && file.length() >= model.sizeBytes * 0.9 // Simple check
73+
}
74+
75+
suspend fun downloadModel(model: WhisperModel): Boolean = withContext(ioDispatcher) {
76+
if (model.isAsset) return@withContext true
77+
78+
val file = File(context.filesDir, model.fileName)
79+
if (isModelDownloaded(model)) return@withContext true
80+
81+
val request = Request.Builder().url(model.url).build()
82+
try {
83+
val response = okHttpClient.newCall(request).execute()
84+
if (!response.isSuccessful) return@withContext false
85+
86+
val body = response.body ?: return@withContext false
87+
val totalBytes = body.contentLength()
88+
89+
body.byteStream().use { input ->
90+
FileOutputStream(file).use { output ->
91+
val buffer = ByteArray(8192)
92+
var bytesRead: Int
93+
var totalRead = 0L
94+
95+
while (input.read(buffer).also { bytesRead = it } != -1) {
96+
output.write(buffer, 0, bytesRead)
97+
totalRead += bytesRead
98+
if (totalBytes > 0) {
99+
val progress = totalRead.toFloat() / totalBytes
100+
_downloadProgress.value = _downloadProgress.value + (model.id to progress)
101+
}
102+
}
103+
}
104+
}
105+
_downloadProgress.value = _downloadProgress.value - model.id
106+
HarkLog.i("WhisperModelManager", "Downloaded model: ${model.name}")
107+
notifyModelStoreChanged()
108+
true
109+
} catch (e: Exception) {
110+
HarkLog.e("WhisperModelManager", "Failed to download model: ${model.name}", e)
111+
file.delete()
112+
_downloadProgress.value = _downloadProgress.value - model.id
113+
false
114+
}
115+
}
116+
117+
fun getModelFile(modelId: String): File? {
118+
val model = availableModels.find { it.id == modelId } ?: return null
119+
return File(context.filesDir, model.fileName)
120+
}
121+
122+
fun deleteModel(modelId: String): Boolean {
123+
val model = availableModels.find { it.id == modelId } ?: return false
124+
if (model.isAsset) return false
125+
126+
val file = File(context.filesDir, model.fileName)
127+
return if (file.exists()) {
128+
val deleted = file.delete()
129+
if (deleted) notifyModelStoreChanged()
130+
deleted
131+
} else false
132+
}
133+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.thivyanstudios.hark.data.local
2+
3+
import androidx.room.Database
4+
import androidx.room.RoomDatabase
5+
6+
@Database(entities = [TranscriptionEntity::class], version = 1, exportSchema = false)
7+
abstract class HarkDatabase : RoomDatabase() {
8+
abstract fun transcriptionDao(): TranscriptionDao
9+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.thivyanstudios.hark.data.local
2+
3+
import androidx.room.Dao
4+
import androidx.room.Insert
5+
import androidx.room.OnConflictStrategy
6+
import androidx.room.Query
7+
import kotlinx.coroutines.flow.Flow
8+
9+
@Dao
10+
interface TranscriptionDao {
11+
@Query("SELECT * FROM transcriptions ORDER BY timestamp DESC")
12+
fun getAllTranscriptions(): Flow<List<TranscriptionEntity>>
13+
14+
@Insert(onConflict = OnConflictStrategy.REPLACE)
15+
suspend fun insertTranscription(transcription: TranscriptionEntity)
16+
17+
@Query("DELETE FROM transcriptions")
18+
suspend fun deleteAll()
19+
20+
@Query("DELETE FROM transcriptions WHERE id = :id")
21+
suspend fun deleteById(id: Long)
22+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.thivyanstudios.hark.data.local
2+
3+
import androidx.room.Entity
4+
import androidx.room.PrimaryKey
5+
6+
@Entity(tableName = "transcriptions")
7+
data class TranscriptionEntity(
8+
@PrimaryKey(autoGenerate = true)
9+
val id: Long = 0,
10+
val text: String,
11+
val timestamp: Long = System.currentTimeMillis()
12+
)

app/src/main/java/com/thivyanstudios/hark/data/model/UserPreferences.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,8 @@ data class UserPreferences(
1313
val transcriptModeEnabled: Boolean = false,
1414
val whisperThreads: Int = 4,
1515
val whisperLanguage: String = "en",
16-
val whisperTranslate: Boolean = false
16+
val whisperTranslate: Boolean = false,
17+
val silenceThreshold: Float = 0.005f,
18+
val transcriptionFontSize: Float = 22f,
19+
val selectedModelId: String = "ggml-base-q8_0.bin"
1720
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.thivyanstudios.hark.data.model
2+
3+
data class WhisperModel(
4+
val id: String,
5+
val name: String,
6+
val description: String,
7+
val url: String,
8+
val sizeBytes: Long,
9+
val isAsset: Boolean = false
10+
) {
11+
val fileName: String get() = if (isAsset) id else "$id.bin"
12+
}

0 commit comments

Comments
 (0)