GGUF: make GgufMetadata serializable in order to be compatible with Room

This commit is contained in:
Han Yin 2025-04-19 21:13:11 -07:00
parent 8ae0c3d2fa
commit 7ed79319e5
7 changed files with 45 additions and 47 deletions

View File

@ -2,9 +2,11 @@ package com.example.llama.revamp.data.local
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.example.llama.revamp.data.model.ModelInfo
import com.example.llama.revamp.util.GgufMetadata
import com.example.llama.revamp.util.GgufMetadataConverters
// TODO-han.yin: Add GgufMetaData
@Entity(tableName = "models")
data class ModelEntity(
@ -13,6 +15,8 @@ data class ModelEntity(
val name: String,
val path: String,
val sizeInBytes: Long,
@field:TypeConverters(GgufMetadataConverters::class)
val metadata: GgufMetadata,
val dateAdded: Long,
val lastUsed: Long?
) {
@ -21,6 +25,7 @@ data class ModelEntity(
name = name,
path = path,
sizeInBytes = sizeInBytes,
metadata = metadata,
lastUsed = lastUsed,
)
}

View File

@ -1,5 +1,6 @@
package com.example.llama.revamp.data.model
import com.example.llama.revamp.util.GgufMetadata
import com.example.llama.revamp.util.formatSize
/**
@ -10,6 +11,7 @@ data class ModelInfo(
val name: String,
val path: String,
val sizeInBytes: Long,
val metadata: GgufMetadata,
val lastUsed: Long? = null
) {
val formattedSize: String

View File

@ -192,8 +192,8 @@ class ModelRepositoryImpl @Inject constructor(
Log.i(TAG, "Extracting GGUF Metadata from $filePath")
GgufMetadataReader().readStructuredMetadata(filePath)
} catch (e: Exception) {
Log.e(TAG, "Failed to extract GGUF metadata: ${e.message}", e)
null
Log.e(TAG, "Cannot extract GGUF metadata: ${e.message}", e)
throw e
}
// Create model entity and save via DAO
@ -202,15 +202,11 @@ class ModelRepositoryImpl @Inject constructor(
name = fileName.substringBeforeLast('.'),
path = modelFile.absolutePath,
sizeInBytes = modelFile.length(),
// TODO-han.yin: add metadata here
metadata = metadata,
dateAdded = System.currentTimeMillis(),
lastUsed = null
).let {
modelDao.insertModel(it)
importJob = null
currentModelFile = null
it.toModelInfo()
}

View File

@ -174,41 +174,7 @@ private fun ModelInfoContent(
Spacer(modifier = Modifier.height(4.dp))
// Model details row (parameters, quantization, size)
Row {
if (model.parameters != null) {
Text(
text = model.parameters,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (model.quantization != null) {
Text(
text = "${model.quantization}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Text(
text = "${model.formattedSize}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(4.dp))
// Context length
if (model.contextLength != null) {
Text(
text = "Context Length: ${model.contextLength}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
// TODO-han.yin: make use of GGUF metadata
// Last used date
model.lastUsed?.let { lastUsed ->

View File

@ -85,7 +85,7 @@ fun ModelsManagementScreen(
if (isMultiSelectionMode) {
viewModel.toggleModelSelectionById(model.id)
} else {
viewModel.viewModelDetails(model.id)
viewModel.viewModelDetails(model)
}
},
modifier = Modifier.padding(bottom = 8.dp),
@ -95,7 +95,7 @@ fun ModelsManagementScreen(
if (!isMultiSelectionMode) {
{
ModelCardActions.InfoButton(
onClick = { viewModel.viewModelDetails(model.id) }
onClick = { viewModel.viewModelDetails(model) }
)
}
} else null

View File

@ -1,11 +1,15 @@
package com.example.llama.revamp.util
import androidx.room.TypeConverter
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.io.IOException
/**
* Structured metadata of GGUF
*/
@Serializable
data class GgufMetadata(
// Basic file info
val version: GgufVersion,
@ -69,6 +73,7 @@ data class GgufMetadata(
val formattedContextLength: String?
get() = dimensions?.contextLength?.let { "$it tokens" }
@Serializable
enum class GgufVersion(val code: Int, val label: String) {
/** First public draft; littleendian only, no alignment key. */
LEGACY_V1(1, "Legacy v1"),
@ -88,6 +93,7 @@ data class GgufMetadata(
override fun toString(): String = "$label (code=$code)"
}
@Serializable
data class BasicInfo(
val uuid: String? = null,
val name: String? = null,
@ -95,6 +101,7 @@ data class GgufMetadata(
val sizeLabel: String? = null, // Size label like "7B"
)
@Serializable
data class AuthorInfo(
val organization: String? = null,
val author: String? = null,
@ -105,6 +112,7 @@ data class GgufMetadata(
val licenseLink: String? = null,
)
@Serializable
data class AdditionalInfo(
val type: String? = null,
val description: String? = null,
@ -112,6 +120,7 @@ data class GgufMetadata(
val languages: List<String>? = null,
)
@Serializable
data class ArchitectureInfo(
val architecture: String? = null,
val fileType: Int? = null,
@ -120,6 +129,7 @@ data class GgufMetadata(
val quantizationVersion: Int? = null,
)
@Serializable
data class BaseModelInfo(
val name: String? = null,
val author: String? = null,
@ -131,6 +141,7 @@ data class GgufMetadata(
val repoUrl: String? = null,
)
@Serializable
data class TokenizerInfo(
val model: String? = null,
val bosTokenId: Int? = null,
@ -142,6 +153,7 @@ data class GgufMetadata(
val chatTemplate: String? = null,
)
@Serializable
data class DimensionsInfo(
val contextLength: Int? = null,
val embeddingSize: Int? = null,
@ -149,6 +161,7 @@ data class GgufMetadata(
val feedForwardSize: Int? = null,
)
@Serializable
data class AttentionInfo(
val headCount: Int? = null,
val headCountKv: Int? = null,
@ -158,6 +171,7 @@ data class GgufMetadata(
val layerNormRmsEpsilon: Float? = null,
)
@Serializable
data class RopeInfo(
val frequencyBase: Float? = null,
val dimensionCount: Int? = null,
@ -168,12 +182,25 @@ data class GgufMetadata(
val finetuned: Boolean? = null,
)
@Serializable
data class ExpertsInfo(
val count: Int? = null,
val usedCount: Int? = null,
)
}
class GgufMetadataConverters {
private val json = Json { encodeDefaults = false; ignoreUnknownKeys = true }
@TypeConverter
fun toJson(value: GgufMetadata?): String? =
value?.let { json.encodeToString(GgufMetadata.serializer(), it) }
@TypeConverter
fun fromJson(value: String?): GgufMetadata? =
value?.let { json.decodeFromString(GgufMetadata.serializer(), it) }
}
/**
* Numerical codes used by `general.file_type` (see llama.cpp repo's `constants.py`).
* The `label` matches what llamacli prints.

View File

@ -2,6 +2,7 @@ package com.example.llama.revamp.viewmodel
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.llama.revamp.data.model.ModelInfo
@ -126,8 +127,9 @@ class ModelsManagementViewModel @Inject constructor(
_managementState.value = ModelManagementState.Idle
}
fun viewModelDetails(modelId: String) {
// TODO-han.yin: Stub for now. Would navigate to model details screen or show dialog
fun viewModelDetails(model: ModelInfo) {
// TODO-han.yin: print logs for now. Would navigate to model details screen or show dialog
Log.i(TAG, "${model.name} metadata: \n" + model.metadata.toString())
}
/**