Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2e08d2a
Phase 1.1: feat(whisper): add whisper.cpp as git submodule
jkkj Feb 10, 2026
02c319f
Phase 1.2: build: add gradle toolchain plugin and include whisper module
jkkj Feb 10, 2026
caefb3c
Phase 1.3: build(whisper): add whisper module build configuration
jkkj Feb 10, 2026
b296991
Phase 1.4: build(whisper): add CMake build configuration
jkkj Feb 10, 2026
c5d4db8
Phase 1.5: feat(whisper): add JNI bindings for whisper.cpp
jkkj Feb 10, 2026
8ea788a
Phase 1.6: feat(whisper): add WhisperCallback interface and CPU config
jkkj Feb 10, 2026
40d4915
Phase 1.7: feat(whisper): add LibWhisper Kotlin wrapper
jkkj Feb 10, 2026
9905e7f
Phase 1.8: build: add JVM toolchain configuration
jkkj Feb 10, 2026
815099a
Phase 1.9: build: add whisper and wave-recorder dependencies
jkkj Feb 10, 2026
cd2cd64
Phase 2.1: feat(tts): add TTS strings and PreferencesManager
jkkj Feb 10, 2026
ac3efe4
Phase 2.2: feat(tts): add TextToSpeechManager
jkkj Feb 10, 2026
cf056bd
Phase 3.1: feat(stt): add STT strings, preferences, and language support
jkkj Feb 10, 2026
ff6c34c
Phase 3.2: feat(stt): add mic and stop notification icons
jkkj Feb 10, 2026
4235eda
Phase 3.3: feat(stt): add SpeechToTextManager
jkkj Feb 10, 2026
2159dc4
Phase 3.4: feat(whisper): add PopularWhisperModelsList
jkkj Feb 10, 2026
7e79d00
Phase 3.5: feat(whisper): add DownloadWhisperModelActivity
jkkj Feb 10, 2026
bb270ed
Phase 4.1: feat(voice): add voice chat strings and service manager
jkkj Feb 10, 2026
cf520d2
Phase 4.2: feat(voice): add VoiceChatService
jkkj Feb 10, 2026
f21a32d
Phase 5.1: feat(context): add context management and SmolLMManager im…
jkkj Feb 10, 2026
84c425c
Phase 6.1: feat(manifest): add permissions, services, and proguard rules
jkkj Feb 10, 2026
7bd3db7
Phase 7.1: feat(ui): add ChatMoreOptionsPopup with voice and context …
jkkj Feb 10, 2026
798d307
Phase 7.2: feat(ui): update ChatListDrawer with STT model management
jkkj Feb 10, 2026
0fab0fe
Phase 7.3: feat(ui): add voice options to EditChatSettingsScreen
jkkj Feb 10, 2026
53437d7
Phase 7.4: feat(viewmodel): add voice mode and context management to …
jkkj Feb 10, 2026
3d497d8
Phase 7.5: feat(ui): add voice mode UI to ChatActivity
jkkj Feb 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
local.properties
.kotlin
ktlint
ktlint.bat
ktlint.bat
whisper/build/
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "llama.cpp"]
path = llama.cpp
url = https://github.com/ggerganov/llama.cpp
[submodule "whisper/src/main/jni/whisper.cpp"]
path = whisper/src/main/jni/whisper.cpp
url = https://github.com/ggml-org/whisper.cpp.git
7 changes: 6 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
id("com.google.devtools.ksp")
kotlin("plugin.serialization") version "2.1.0"
kotlin("plugin.serialization") version "2.0.0"
}

android {
Expand Down Expand Up @@ -92,12 +92,17 @@ dependencies {

implementation(project(":smollm"))
implementation(project(":hf-model-hub-api"))
implementation(project(":whisper"))

// Android Wave Recorder for speech-to-text
implementation("com.github.squti:Android-Wave-Recorder:2.1.0")

// Koin: dependency injection
implementation(libs.koin.android)
implementation(libs.koin.annotations)
implementation(libs.koin.androidx.compose)
implementation(libs.androidx.ui.text.google.fonts)
implementation(libs.androidx.compose.foundation)
ksp(libs.koin.ksp.compiler)

// compose-markdown: Markdown rendering in Compose
Expand Down
5 changes: 4 additions & 1 deletion app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile

# Keep Whisper native callbacks
-keep class com.whispercpp.whisper.** { *; }
13 changes: 13 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>

<application
android:name=".SmolChatApplication"
Expand Down Expand Up @@ -43,6 +49,13 @@
<activity android:name=".ui.screens.model_download.DownloadModelActivity"/>

<activity android:name=".ui.screens.manage_tasks.ManageTasksActivity"/>

<activity android:name=".ui.screens.whisper_download.DownloadWhisperModelActivity"/>

<service
android:name=".service.VoiceChatService"
android:foregroundServiceType="microphone"
android:exported="false"/>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2024 Shubham Panchal
*
* 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
*
* http://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.
*/

package io.shubham0204.smollmandroid.data

import android.content.Context
import org.koin.core.annotation.Single

@Single
class PreferencesManager(context: Context) {
private val prefs = context.getSharedPreferences("smolchat_prefs", Context.MODE_PRIVATE)

var ttsEnabled: Boolean
get() = prefs.getBoolean("tts_enabled", false)
set(value) = prefs.edit().putBoolean("tts_enabled", value).apply()

var autoSubmitEnabled: Boolean
get() = prefs.getBoolean("auto_submit_enabled", false)
set(value) = prefs.edit().putBoolean("auto_submit_enabled", value).apply()

var autoSubmitDelayMs: Long
get() = prefs.getLong("auto_submit_delay_ms", 2000L)
set(value) = prefs.edit().putLong("auto_submit_delay_ms", value).apply()

var selectedWhisperModel: String
get() = prefs.getString("selected_whisper_model", DEFAULT_WHISPER_MODEL) ?: DEFAULT_WHISPER_MODEL
set(value) = prefs.edit().putString("selected_whisper_model", value).apply()

var sttLanguage: String
get() = prefs.getString("stt_language", DEFAULT_STT_LANGUAGE) ?: DEFAULT_STT_LANGUAGE
set(value) = prefs.edit().putString("stt_language", value).apply()

var autoContextTrimEnabled: Boolean
get() = prefs.getBoolean("auto_context_trim_enabled", false)
set(value) = prefs.edit().putBoolean("auto_context_trim_enabled", value).apply()

companion object {
const val DEFAULT_WHISPER_MODEL = "ggml-base.en.bin"
const val DEFAULT_STT_LANGUAGE = "en"

// Whisper supported languages with their display names
val SUPPORTED_LANGUAGES = listOf(
"en" to "English",
"de" to "German",
"fr" to "French",
"es" to "Spanish",
"it" to "Italian",
"pt" to "Portuguese",
"nl" to "Dutch",
"pl" to "Polish",
"ru" to "Russian",
"zh" to "Chinese",
"ja" to "Japanese",
"ko" to "Korean",
"ar" to "Arabic",
"hi" to "Hindi",
"tr" to "Turkish",
"uk" to "Ukrainian",
"cs" to "Czech",
"sv" to "Swedish",
"auto" to "Auto-detect",
)
}
}
60 changes: 42 additions & 18 deletions app/src/main/java/io/shubham0204/smollmandroid/llm/SmolLMManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package io.shubham0204.smollmandroid.llm

import android.os.Process
import android.util.Log
import io.shubham0204.smollm.SmolLM
import io.shubham0204.smollmandroid.data.AppDB
Expand Down Expand Up @@ -163,49 +164,72 @@ class SmolLMManager(private val appDB: AppDB) {
responseGenerationJob?.cancel()

responseGenerationJob = CoroutineScope(Dispatchers.Default).launch {
// Boost thread priority to reduce CPU throttling when screen is locked
// THREAD_PRIORITY_URGENT_AUDIO (-19) is the highest priority available
// to regular apps and signals to the system this is time-sensitive work
val originalPriority = Process.getThreadPriority(Process.myTid())
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO)
LOGD(">>> Thread priority boosted from $originalPriority to URGENT_AUDIO")
} catch (e: Exception) {
LOGD(">>> Failed to boost thread priority: ${e.message}")
}

try {
LOGD(">>> getResponse coroutine started on thread: ${Thread.currentThread().name}")
isInferenceOn = true
var response = ""

val duration = measureTime {
LOGD(">>> Starting response flow collection...")
instance.getResponseAsFlow(query).collect { piece ->
response += piece
withContext(Dispatchers.Main) {
onPartialResponseGenerated(response)
}
// Don't use Main dispatcher - callbacks are thread-safe
// Using Main blocks when screen is locked
onPartialResponseGenerated(response)
}
LOGD(">>> Response flow collection complete")
}

response = responseTransform(response)
LOGD(">>> Response transformed, length=${response.length}")

// Thread-safe access to chat
val currentChat = stateLock.withLock { chat }

if (currentChat != null) {
// Add response to database
LOGD(">>> Adding assistant message to DB...")
appDB.addAssistantMessage(currentChat.id, response)
LOGD(">>> Assistant message added")
}

withContext(Dispatchers.Main) {
isInferenceOn = false
onSuccess(
SmolLMResponse(
response = response,
generationSpeed = instance.getResponseGenerationSpeed(),
generationTimeSecs = duration.inWholeSeconds.toInt(),
contextLengthUsed = instance.getContextLengthUsed(),
)
LOGD(">>> Calling onSuccess callback...")
isInferenceOn = false
onSuccess(
SmolLMResponse(
response = response,
generationSpeed = instance.getResponseGenerationSpeed(),
generationTimeSecs = duration.inWholeSeconds.toInt(),
contextLengthUsed = instance.getContextLengthUsed(),
)
}
)
LOGD(">>> onSuccess callback returned")
} catch (e: CancellationException) {
LOGD(">>> Response generation cancelled")
isInferenceOn = false
withContext(Dispatchers.Main) {
onCancelled()
}
onCancelled()
} catch (e: Exception) {
LOGD(">>> Response generation error: ${e.message}")
isInferenceOn = false
withContext(Dispatchers.Main) {
onError(e)
onError(e)
} finally {
// Restore original thread priority
try {
Process.setThreadPriority(originalPriority)
LOGD(">>> Thread priority restored to $originalPriority")
} catch (e: Exception) {
LOGD(">>> Failed to restore thread priority: ${e.message}")
}
}
}
Expand Down
Loading