Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 5 additions & 8 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.compose.compiler)
}

android {
namespace = "com.runanywhere.kotlin_starter_example"
compileSdk = 35
compileSdk = 36

defaultConfig {
applicationId = "com.runanywhere.kotlin_starter_example"
minSdk = 26
targetSdk = 35
targetSdk = 36
versionCode = 1
versionName = "1.0"

Expand All @@ -37,10 +36,6 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = "17"
}

buildFeatures {
compose = true
}
Expand All @@ -65,11 +60,13 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.navigation.compose)
debugImplementation(libs.androidx.compose.ui.tooling)

// Markdown
implementation(libs.markdown.renderer)

// Coroutines
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|smallestScreenSize|density"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.Kotlinstarterexample">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import com.runanywhere.kotlin_starter_example.services.ModelService
import com.runanywhere.kotlin_starter_example.ui.screens.ChatScreen
import com.runanywhere.kotlin_starter_example.ui.screens.HomeScreen
import com.runanywhere.kotlin_starter_example.ui.screens.SpeechToTextScreen
import com.runanywhere.kotlin_starter_example.ui.screens.SplashScreen
import com.runanywhere.kotlin_starter_example.ui.screens.TextToSpeechScreen
import com.runanywhere.kotlin_starter_example.ui.screens.ToolCallingScreen
import com.runanywhere.kotlin_starter_example.ui.screens.LoraScreen
import com.runanywhere.kotlin_starter_example.ui.screens.VisionScreen
import com.runanywhere.kotlin_starter_example.ui.screens.VoicePipelineScreen
import com.runanywhere.kotlin_starter_example.ui.theme.KotlinStarterTheme
Expand Down Expand Up @@ -71,16 +73,27 @@ fun RunAnywhereApp() {

NavHost(
navController = navController,
startDestination = "home"
startDestination = "splash"
) {
composable("splash") {
SplashScreen(
onSplashComplete = {
navController.navigate("home") {
popUpTo("splash") { inclusive = true }
}
}
)
}

composable("home") {
HomeScreen(
onNavigateToChat = { navController.navigate("chat") },
onNavigateToSTT = { navController.navigate("stt") },
onNavigateToTTS = { navController.navigate("tts") },
onNavigateToVoicePipeline = { navController.navigate("voice_pipeline") },
onNavigateToToolCalling = { navController.navigate("tool_calling") },
onNavigateToVision = { navController.navigate("vision") }
onNavigateToVision = { navController.navigate("vision") },
onNavigateToLora = { navController.navigate("lora") }
)
}

Expand Down Expand Up @@ -125,5 +138,12 @@ fun RunAnywhereApp() {
modelService = modelService
)
}

composable("lora") {
LoraScreen(
onNavigateBack = { navController.popBackStack() },
modelService = modelService
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,33 +74,104 @@ class ModelService : ViewModel() {
private set
var isVLMLoaded by mutableStateOf(false)
private set


// LoRA base model state
var isLoraBaseDownloading by mutableStateOf(false)
private set
var loraBaseDownloadProgress by mutableStateOf(0f)
private set
var isLoraBaseLoading by mutableStateOf(false)
private set
var isLoraBaseLoaded by mutableStateOf(false)
private set

var isVoiceAgentReady by mutableStateOf(false)
private set

var errorMessage by mutableStateOf<String?>(null)
private set

companion object {
// Model IDs - using officially supported models
const val LLM_MODEL_ID = "smollm2-360m-instruct-q8_0"
const val LLM_MODEL_ID = "qwen2.5-0.5b-instruct-q6_k"
const val STT_MODEL_ID = "sherpa-onnx-whisper-tiny.en"
const val TTS_MODEL_ID = "vits-piper-en_US-lessac-medium"
const val VLM_MODEL_ID = "smolvlm-256m-instruct"


// LoRA-compatible base model (Qwen 2.5 supports LoRA adapters)
const val LORA_BASE_MODEL_ID = "qwen2.5-0.5b-instruct-q6_k"

// LoRA adapter definitions
data class LoraAdapterDef(
val id: String,
val name: String,
val description: String,
val url: String,
val filename: String,
val examplePrompts: List<String>
)

val LORA_ADAPTERS = listOf(
LoraAdapterDef(
id = "code-assistant-lora",
name = "Code Assistant",
description = "Enhances code generation and programming assistance",
url = "https://huggingface.co/Void2377/Qwen/resolve/main/lora/code-assistant-Q8_0.gguf",
filename = "code-assistant-Q8_0.gguf",
examplePrompts = listOf(
"Write a Python function to reverse a linked list",
"Explain the difference between a stack and a queue with code examples",
)
),
LoraAdapterDef(
id = "reasoning-logic-lora",
name = "Reasoning Logic",
description = "Improves logical reasoning and step-by-step problem solving",
url = "https://huggingface.co/Void2377/Qwen/resolve/main/lora/reasoning-logic-Q8_0.gguf",
filename = "reasoning-logic-Q8_0.gguf",
examplePrompts = listOf(
"If all roses are flowers and some flowers fade quickly, can we conclude some roses fade quickly?",
"A farmer has 17 sheep. All but 9 die. How many are left?",
)
),
LoraAdapterDef(
id = "medical-qa-lora",
name = "Medical QA",
description = "Enhances medical question answering and health-related responses",
url = "https://huggingface.co/Void2377/Qwen/resolve/main/lora/medical-qa-Q8_0.gguf",
filename = "medical-qa-Q8_0.gguf",
examplePrompts = listOf(
"What are the common symptoms of vitamin D deficiency?",
"Explain the difference between Type 1 and Type 2 diabetes",
)
),
LoraAdapterDef(
id = "creative-writing-lora",
name = "Creative Writing",
description = "Improves creative writing, storytelling, and literary style",
url = "https://huggingface.co/Void2377/Qwen/resolve/main/lora/creative-writing-Q8_0.gguf",
filename = "creative-writing-Q8_0.gguf",
examplePrompts = listOf(
"Write a short story about a robot discovering emotions for the first time",
"Describe a sunset over the ocean using vivid sensory language",
)
),
)

/**
* Register default models with the SDK.
* Includes LLM, STT, TTS, and VLM (multi-file model with mmproj).
* Includes LLM, STT, TTS, VLM. Qwen 2.5 serves as both default LLM and LoRA base.
*/
fun registerDefaultModels() {
// LLM Model - SmolLM2 360M (small, fast, good for demos)
// LLM Model - Qwen 2.5 0.5B Instruct (supports LoRA adapters)
RunAnywhere.registerModel(
id = LLM_MODEL_ID,
name = "SmolLM2 360M Instruct Q8_0",
url = "https://huggingface.co/HuggingFaceTB/SmolLM2-360M-Instruct-GGUF/resolve/main/smollm2-360m-instruct-q8_0.gguf",
name = "Qwen 2.5 0.5B Instruct Q6_K",
url = "https://huggingface.co/Triangle104/Qwen2.5-0.5B-Instruct-Q6_K-GGUF/resolve/main/qwen2.5-0.5b-instruct-q6_k.gguf",
framework = InferenceFramework.LLAMA_CPP,
modality = ModelCategory.LANGUAGE,
memoryRequirement = 400_000_000
memoryRequirement = 600_000_000,
supportsLora = true
)

// STT Model - Whisper Tiny English (fast transcription)
Expand Down Expand Up @@ -333,6 +404,55 @@ class ModelService : ViewModel() {
}
}

/**
* Download and load LoRA-compatible base model (Qwen 2.5 0.5B)
* This unloads any existing LLM first, then loads the LoRA-compatible base.
*/
fun downloadAndLoadLoraBase() {
if (isLoraBaseDownloading || isLoraBaseLoading) return

viewModelScope.launch {
try {
errorMessage = null

// Unload existing LLM if loaded (can only have one at a time)
if (isLLMLoaded) {
RunAnywhere.unloadLLMModel()
isLLMLoaded = false
}

// Check if already downloaded
if (!isModelDownloaded(LORA_BASE_MODEL_ID)) {
isLoraBaseDownloading = true
loraBaseDownloadProgress = 0f

RunAnywhere.downloadModel(LORA_BASE_MODEL_ID)
.catch { e ->
errorMessage = "Qwen download failed: ${e.message}"
}
.collect { progress ->
loraBaseDownloadProgress = progress.progress
}

isLoraBaseDownloading = false
}

// Load the model
isLoraBaseLoading = true
RunAnywhere.loadLLMModel(LORA_BASE_MODEL_ID)
isLoraBaseLoaded = true
isLLMLoaded = true
isLoraBaseLoading = false

refreshModelState()
} catch (e: Exception) {
errorMessage = "Qwen load failed: ${e.message}"
isLoraBaseDownloading = false
isLoraBaseLoading = false
}
}
}

/**
* Download and load all models for voice agent
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.runanywhere.kotlin_starter_example.ui.components

import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.unit.dp
import com.runanywhere.kotlin_starter_example.ui.theme.AppTheme

@Composable
fun AppButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
color: Color = AppTheme.colors.accent,
content: @Composable RowScope.() -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val scale by animateFloatAsState(
targetValue = if (isPressed) 0.97f else 1f,
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
label = "press"
)

Button(
onClick = onClick,
modifier = modifier.height(44.dp).scale(scale),
enabled = enabled,
shape = RoundedCornerShape(11.dp),
colors = ButtonDefaults.buttonColors(
containerColor = color,
contentColor = Color.White,
disabledContainerColor = color.copy(alpha = 0.3f),
disabledContentColor = Color.White.copy(alpha = 0.4f)
),
contentPadding = PaddingValues(horizontal = 20.dp, vertical = 0.dp),
interactionSource = interactionSource,
elevation = ButtonDefaults.buttonElevation(defaultElevation = 0.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
content = content
)
}
}

@Composable
fun AppOutlinedButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
content: @Composable RowScope.() -> Unit
) {
val colors = AppTheme.colors
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val scale by animateFloatAsState(
targetValue = if (isPressed) 0.97f else 1f,
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
label = "press"
)

OutlinedButton(
onClick = onClick,
modifier = modifier.height(44.dp).scale(scale),
enabled = enabled,
shape = RoundedCornerShape(11.dp),
colors = ButtonDefaults.outlinedButtonColors(
containerColor = colors.surfaceContainer,
contentColor = colors.textPrimary,
disabledContentColor = colors.textSecondary
),
border = ButtonDefaults.outlinedButtonBorder(enabled = enabled).copy(
brush = SolidColor(colors.border)
),
contentPadding = PaddingValues(horizontal = 20.dp, vertical = 0.dp),
interactionSource = interactionSource,
elevation = ButtonDefaults.buttonElevation(defaultElevation = 0.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
content = content
)
}
}
Loading