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.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.example.llama.revamp.data.model.ModelInfo 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") @Entity(tableName = "models")
data class ModelEntity( data class ModelEntity(
@ -13,6 +15,8 @@ data class ModelEntity(
val name: String, val name: String,
val path: String, val path: String,
val sizeInBytes: Long, val sizeInBytes: Long,
@field:TypeConverters(GgufMetadataConverters::class)
val metadata: GgufMetadata,
val dateAdded: Long, val dateAdded: Long,
val lastUsed: Long? val lastUsed: Long?
) { ) {
@ -21,6 +25,7 @@ data class ModelEntity(
name = name, name = name,
path = path, path = path,
sizeInBytes = sizeInBytes, sizeInBytes = sizeInBytes,
metadata = metadata,
lastUsed = lastUsed, lastUsed = lastUsed,
) )
} }

View File

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

View File

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

View File

@ -174,41 +174,7 @@ private fun ModelInfoContent(
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
// Model details row (parameters, quantization, size) // TODO-han.yin: make use of GGUF metadata
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
)
}
// Last used date // Last used date
model.lastUsed?.let { lastUsed -> model.lastUsed?.let { lastUsed ->

View File

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

View File

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

View File

@ -2,6 +2,7 @@ package com.example.llama.revamp.viewmodel
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.llama.revamp.data.model.ModelInfo import com.example.llama.revamp.data.model.ModelInfo
@ -126,8 +127,9 @@ class ModelsManagementViewModel @Inject constructor(
_managementState.value = ModelManagementState.Idle _managementState.value = ModelManagementState.Idle
} }
fun viewModelDetails(modelId: String) { fun viewModelDetails(model: ModelInfo) {
// TODO-han.yin: Stub for now. Would navigate to model details screen or show dialog // 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())
} }
/** /**