Skip to content

Commit 1aa628e

Browse files
authored
Merge pull request #2 from tryptz/claude/test-app-xGIH3
feat: auto-detect GGUF model settings on import
2 parents 0d53ab2 + 49ff4db commit 1aa628e

6 files changed

Lines changed: 437 additions & 13 deletions

File tree

app/src/main/java/com/tryptz/neuron/data/local/entity/Entities.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ data class LocalModelEntity(
5656
val fileSizeBytes: Long,
5757
val chatTemplate: String,
5858
val contextLength: Int,
59+
val architecture: String? = null,
60+
val quantization: String? = null,
61+
val parameterCount: Long? = null,
5962
val installedAt: Long
6063
)
6164

app/src/main/java/com/tryptz/neuron/data/repository/ModelRepository.kt

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ class ModelRepository @Inject constructor(
7474
uri: Uri,
7575
name: String,
7676
chatTemplate: ChatTemplate,
77-
contextLength: Int
77+
contextLength: Int,
78+
architecture: String? = null,
79+
quantization: String? = null,
80+
parameterCount: Long? = null
7881
): Result<LocalModelEntity> = withContext(Dispatchers.IO) {
7982
runCatching {
8083
val fileName = resolveFileName(uri)
@@ -92,6 +95,9 @@ class ModelRepository @Inject constructor(
9295
fileSizeBytes = destFile.length(),
9396
chatTemplate = chatTemplate.raw,
9497
contextLength = contextLength,
98+
architecture = architecture,
99+
quantization = quantization,
100+
parameterCount = parameterCount,
95101
installedAt = System.currentTimeMillis()
96102
)
97103
localModelDao.insert(entity)
@@ -105,22 +111,35 @@ class ModelRepository @Inject constructor(
105111
localModelDao.deleteById(id)
106112
}
107113

108-
fun buildLocalDescriptor(entity: LocalModelEntity): ModelDescriptor =
109-
ModelDescriptor(
114+
fun buildLocalDescriptor(entity: LocalModelEntity): ModelDescriptor {
115+
val quant = entity.quantization?.let { q ->
116+
Quantization.entries.find { it.label.equals(q, ignoreCase = true) }
117+
} ?: Quantization.Q4_K_M
118+
val fileSizeMb = (entity.fileSizeBytes / (1024 * 1024)).toInt()
119+
val paramsStr = entity.parameterCount?.let { formatParamCount(it) } ?: "Unknown"
120+
121+
return ModelDescriptor(
110122
modelId = ModelId.LOCAL,
111123
name = entity.name,
112-
family = "local",
113-
totalParams = "Unknown",
114-
quantization = Quantization.Q4_K_M,
115-
fileSizeMb = (entity.fileSizeBytes / (1024 * 1024)).toInt(),
116-
ramRequiredMb = (entity.fileSizeBytes / (1024 * 1024)).toInt() + 500,
124+
family = entity.architecture ?: "local",
125+
totalParams = paramsStr,
126+
quantization = quant,
127+
fileSizeMb = fileSizeMb,
128+
ramRequiredMb = fileSizeMb + 500,
117129
maxContext = entity.contextLength,
118-
supportedBackends = listOf(InferenceBackend.GPU, InferenceBackend.CPU),
130+
supportedBackends = listOf(InferenceBackend.CPU, InferenceBackend.GPU),
119131
chatTemplate = ChatTemplate.fromRaw(entity.chatTemplate),
120132
huggingFaceRepo = "",
121133
huggingFaceFile = "",
122134
localId = entity.id
123135
)
136+
}
137+
138+
private fun formatParamCount(params: Long): String = when {
139+
params >= 1_000_000_000 -> "%.1fB".format(params / 1_000_000_000.0)
140+
params >= 1_000_000 -> "%.0fM".format(params / 1_000_000.0)
141+
else -> "${params}"
142+
}
124143

125144
private fun resolveFileName(uri: Uri): String {
126145
val cursor = context.contentResolver.query(uri, null, null, null, null)

app/src/main/java/com/tryptz/neuron/ui/modelmanager/ModelManagerScreen.kt

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ fun ModelManagerScreen(
9393
chatTemplate = state.importChatTemplate,
9494
contextLength = state.importContextLength,
9595
isImporting = state.isImporting,
96+
isDetecting = state.isDetecting,
97+
detectedArch = state.importArchitecture,
98+
detectedQuant = state.importQuantization,
9699
error = state.importError,
97100
onNameChange = viewModel::updateImportName,
98101
onTemplateChange = viewModel::updateImportChatTemplate,
@@ -438,7 +441,11 @@ private fun LocalModelCard(
438441
Column(modifier = Modifier.weight(1f)) {
439442
Text(model.name, style = MaterialTheme.typography.titleMedium)
440443
Text(
441-
"${model.fileName} · ${ChatTemplate.fromRaw(model.chatTemplate).name}",
444+
buildString {
445+
model.architecture?.let { append("$it · ") }
446+
model.quantization?.let { append("$it · ") }
447+
append(ChatTemplate.fromRaw(model.chatTemplate).name)
448+
},
442449
style = MaterialTheme.typography.bodySmall,
443450
color = MaterialTheme.colorScheme.onSurfaceVariant
444451
)
@@ -450,7 +457,8 @@ private fun LocalModelCard(
450457
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
451458
StatItem("Size", model.fileSizeBytes.formatBytes())
452459
StatItem("Context", "${model.contextLength}")
453-
StatItem("Template", ChatTemplate.fromRaw(model.chatTemplate).name)
460+
model.architecture?.let { StatItem("Arch", it) }
461+
model.quantization?.let { StatItem("Quant", it) }
454462
}
455463

456464
Spacer(Modifier.height(12.dp))
@@ -481,6 +489,9 @@ private fun ImportModelDialog(
481489
chatTemplate: ChatTemplate,
482490
contextLength: Int,
483491
isImporting: Boolean,
492+
isDetecting: Boolean,
493+
detectedArch: String?,
494+
detectedQuant: String?,
484495
error: String?,
485496
onNameChange: (String) -> Unit,
486497
onTemplateChange: (ChatTemplate) -> Unit,
@@ -503,6 +514,46 @@ private fun ImportModelDialog(
503514
color = MaterialTheme.colorScheme.onSurfaceVariant
504515
)
505516

517+
// Detected metadata banner
518+
if (isDetecting) {
519+
Row(verticalAlignment = Alignment.CenterVertically) {
520+
CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
521+
Spacer(Modifier.width(8.dp))
522+
Text(
523+
"Detecting model settings...",
524+
style = MaterialTheme.typography.bodySmall,
525+
color = MaterialTheme.colorScheme.onSurfaceVariant
526+
)
527+
}
528+
} else if (detectedArch != null || detectedQuant != null) {
529+
Surface(
530+
shape = MaterialTheme.shapes.small,
531+
color = MaterialTheme.colorScheme.primaryContainer
532+
) {
533+
Row(
534+
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
535+
verticalAlignment = Alignment.CenterVertically
536+
) {
537+
Icon(
538+
Icons.Default.AutoFixHigh,
539+
contentDescription = null,
540+
modifier = Modifier.size(16.dp),
541+
tint = MaterialTheme.colorScheme.onPrimaryContainer
542+
)
543+
Spacer(Modifier.width(8.dp))
544+
Text(
545+
buildString {
546+
append("Detected: ")
547+
listOfNotNull(detectedArch, detectedQuant)
548+
.joinTo(this, " · ")
549+
},
550+
style = MaterialTheme.typography.labelMedium,
551+
color = MaterialTheme.colorScheme.onPrimaryContainer
552+
)
553+
}
554+
}
555+
}
556+
506557
OutlinedTextField(
507558
value = name,
508559
onValueChange = onNameChange,
@@ -585,7 +636,7 @@ private fun ImportModelDialog(
585636
}
586637
},
587638
confirmButton = {
588-
TextButton(onClick = onConfirm, enabled = !isImporting && name.isNotBlank()) {
639+
TextButton(onClick = onConfirm, enabled = !isImporting && !isDetecting && name.isNotBlank()) {
589640
Text("Import")
590641
}
591642
},

app/src/main/java/com/tryptz/neuron/ui/modelmanager/viewmodel/ModelManagerViewModel.kt

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.tryptz.neuron.ui.modelmanager.viewmodel
22

3+
import android.content.Context
34
import android.net.Uri
45
import androidx.lifecycle.ViewModel
56
import androidx.lifecycle.viewModelScope
@@ -10,9 +11,14 @@ import com.tryptz.neuron.data.repository.ModelRepository
1011
import com.tryptz.neuron.domain.model.*
1112
import com.tryptz.neuron.download.ModelDownloadWorker
1213
import com.tryptz.neuron.util.DeviceMonitor
14+
import com.tryptz.neuron.util.GgufMetadata
15+
import com.tryptz.neuron.util.GgufMetadataReader
1316
import dagger.hilt.android.lifecycle.HiltViewModel
17+
import dagger.hilt.android.qualifiers.ApplicationContext
18+
import kotlinx.coroutines.Dispatchers
1419
import kotlinx.coroutines.flow.*
1520
import kotlinx.coroutines.launch
21+
import kotlinx.coroutines.withContext
1622
import javax.inject.Inject
1723

1824
data class ModelManagerUiState(
@@ -32,11 +38,17 @@ data class ModelManagerUiState(
3238
val importChatTemplate: ChatTemplate = ChatTemplate.CHATML,
3339
val importContextLength: Int = 4096,
3440
val isImporting: Boolean = false,
41+
val isDetecting: Boolean = false,
42+
val detectedMetadata: GgufMetadata? = null,
43+
val importArchitecture: String? = null,
44+
val importQuantization: String? = null,
45+
val importParamCount: Long? = null,
3546
val importError: String? = null
3647
)
3748

3849
@HiltViewModel
3950
class ModelManagerViewModel @Inject constructor(
51+
@ApplicationContext private val appContext: Context,
4052
private val modelRepo: ModelRepository,
4153
private val workManager: WorkManager,
4254
private val deviceMonitor: DeviceMonitor
@@ -136,9 +148,33 @@ class ModelManagerViewModel @Inject constructor(
136148
importName = displayName,
137149
importChatTemplate = ChatTemplate.CHATML,
138150
importContextLength = 4096,
151+
isDetecting = true,
152+
detectedMetadata = null,
153+
importArchitecture = null,
154+
importQuantization = null,
155+
importParamCount = null,
139156
importError = null
140157
)
141158
}
159+
160+
viewModelScope.launch {
161+
val metadata = withContext(Dispatchers.IO) {
162+
GgufMetadataReader.read(appContext, uri)
163+
}
164+
165+
_uiState.update {
166+
it.copy(
167+
isDetecting = false,
168+
detectedMetadata = metadata,
169+
importName = metadata?.name ?: displayName,
170+
importChatTemplate = metadata?.inferredChatTemplate ?: ChatTemplate.CHATML,
171+
importContextLength = metadata?.contextLength ?: 4096,
172+
importArchitecture = metadata?.architecture,
173+
importQuantization = metadata?.displayQuantization,
174+
importParamCount = metadata?.parameterCount
175+
)
176+
}
177+
}
142178
}
143179

144180
fun updateImportName(name: String) {
@@ -168,7 +204,10 @@ class ModelManagerViewModel @Inject constructor(
168204
uri = uri,
169205
name = state.importName,
170206
chatTemplate = state.importChatTemplate,
171-
contextLength = state.importContextLength
207+
contextLength = state.importContextLength,
208+
architecture = state.importArchitecture,
209+
quantization = state.importQuantization,
210+
parameterCount = state.importParamCount
172211
).onSuccess {
173212
_uiState.update { it.copy(showImportDialog = false, importUri = null, isImporting = false) }
174213
}.onFailure { e ->

0 commit comments

Comments
 (0)