[WIP] lib: move GgufMetadata into the lib submodule

This commit is contained in:
Han Yin 2025-06-26 14:10:12 -07:00
parent 4b3f6ef8d7
commit 6a5bc94ff1
8 changed files with 330 additions and 74 deletions

View File

@ -1,7 +1,7 @@
package com.example.llama.data.local.converter
import androidx.room.TypeConverter
import com.example.llama.util.GgufMetadata
import com.example.llama.data.model.GgufMetadata
import kotlinx.serialization.json.Json
class GgufMetadataConverters {

View File

@ -5,7 +5,7 @@ import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.example.llama.data.local.converter.GgufMetadataConverters
import com.example.llama.data.model.ModelInfo
import com.example.llama.util.GgufMetadata
import com.example.llama.data.model.GgufMetadata
@Entity(tableName = "models")

View File

@ -0,0 +1,309 @@
package com.example.llama.data.model
import kotlinx.serialization.Serializable
import android.llama.cpp.gguf.GgufMetadata as Domain
/**
* A local serializable domain replicate of [android.llama.cpp.gguf.GgufMetadata]
*/
@Serializable
data class GgufMetadata(
// Basic file info
val version: GgufVersion,
val tensorCount: Long,
val kvCount: Long,
// General info
val basic: BasicInfo,
val author: AuthorInfo? = null,
val additional: AdditionalInfo? = null,
val architecture: ArchitectureInfo? = null,
val baseModels: List<BaseModelInfo>? = null,
val tokenizer: TokenizerInfo? = null,
// Derivative info
val dimensions: DimensionsInfo? = null,
val attention: AttentionInfo? = null,
val rope: RopeInfo? = null,
val experts: ExpertsInfo? = null
) {
/** Human-readable full model name + size */
val fullModelName: String?
get() = when {
basic.nameLabel != null -> basic.nameLabel
basic.name != null && basic.sizeLabel != null -> "${basic.name}-${basic.sizeLabel}"
basic.name != null -> basic.name
else -> null
}
/** Humanreadable model name (spaces). */
val primaryName: String?
get() = basic.nameLabel
?: baseModels?.firstNotNullOfOrNull { it.name }
?: basic.name
/** CLIfriendly slug (hyphens). */
val primaryBasename: String?
get() = basic.name
?: baseModels?.firstNotNullOfOrNull { it.name?.replace(' ', '-') }
/** URL pointing to model homepage/repo. */
val primaryUrl: String?
get() = author?.url
?: baseModels?.firstNotNullOfOrNull { it.url }
val primaryRepoUrl: String?
get() = author?.repoUrl
?: baseModels?.firstNotNullOfOrNull { it.repoUrl }
/** Organisation string. */
val primaryOrganization: String?
get() = author?.organization
?: baseModels?.firstNotNullOfOrNull { it.organization }
/** Author string. */
val primaryAuthor: String?
get() = author?.author
?: baseModels?.firstNotNullOfOrNull { it.author }
@Serializable
enum class GgufVersion(val code: Int, val label: String) {
/** First public draft; littleendian only, no alignment key. */
LEGACY_V1(1, "Legacy v1"),
/** Added splitfile support and some extra metadata keys. */
EXTENDED_V2(2, "Extended v2"),
/** Current spec: endianaware, mandatory alignment, fully validated. */
VALIDATED_V3(3, "Validated v3");
companion object {
fun fromDomain(domain: Domain.GgufVersion): GgufVersion = when (domain) {
Domain.GgufVersion.LEGACY_V1 -> LEGACY_V1
Domain.GgufVersion.EXTENDED_V2 -> EXTENDED_V2
Domain.GgufVersion.VALIDATED_V3 -> VALIDATED_V3
}
}
override fun toString(): String = "$label (code=$code)"
}
@Serializable
data class BasicInfo(
val uuid: String? = null,
val name: String? = null,
val nameLabel: String? = null,
val sizeLabel: String? = null, // Size label like "7B"
) {
companion object {
fun fromDomain(domain: Domain.BasicInfo) = BasicInfo(
uuid = domain.uuid,
name = domain.name,
nameLabel = domain.nameLabel,
sizeLabel = domain.sizeLabel
)
}
}
@Serializable
data class AuthorInfo(
val organization: String? = null,
val author: String? = null,
val doi: String? = null,
val url: String? = null,
val repoUrl: String? = null,
val license: String? = null,
val licenseLink: String? = null,
) {
companion object {
fun fromDomain(domain: Domain.AuthorInfo) = AuthorInfo(
organization = domain.organization,
author = domain.author,
doi = domain.doi,
url = domain.url,
repoUrl = domain.repoUrl,
license = domain.license,
licenseLink = domain.licenseLink
)
}
}
@Serializable
data class AdditionalInfo(
val type: String? = null,
val description: String? = null,
val tags: List<String>? = null,
val languages: List<String>? = null,
) {
companion object {
fun fromDomain(domain: Domain.AdditionalInfo) = AdditionalInfo(
type = domain.type,
description = domain.description,
tags = domain.tags,
languages = domain.languages
)
}
}
@Serializable
data class ArchitectureInfo(
val architecture: String? = null,
val fileType: Int? = null,
val vocabSize: Int? = null,
val finetune: String? = null,
val quantizationVersion: Int? = null,
) {
companion object {
fun fromDomain(domain: Domain.ArchitectureInfo) = ArchitectureInfo(
architecture = domain.architecture,
fileType = domain.fileType,
vocabSize = domain.vocabSize,
finetune = domain.finetune,
quantizationVersion = domain.quantizationVersion
)
}
}
@Serializable
data class BaseModelInfo(
val name: String? = null,
val author: String? = null,
val version: String? = null,
val organization: String? = null,
val url: String? = null,
val doi: String? = null,
val uuid: String? = null,
val repoUrl: String? = null,
) {
companion object {
fun fromDomain(domain: Domain.BaseModelInfo) = BaseModelInfo(
name = domain.name,
author = domain.author,
version = domain.version,
organization = domain.organization,
url = domain.url,
doi = domain.doi,
uuid = domain.uuid,
repoUrl = domain.repoUrl
)
}
}
@Serializable
data class TokenizerInfo(
val model: String? = null,
val bosTokenId: Int? = null,
val eosTokenId: Int? = null,
val unknownTokenId: Int? = null,
val paddingTokenId: Int? = null,
val addBosToken: Boolean? = null,
val addEosToken: Boolean? = null,
val chatTemplate: String? = null,
) {
companion object {
fun fromDomain(domain: Domain.TokenizerInfo) = TokenizerInfo(
model = domain.model,
bosTokenId = domain.bosTokenId,
eosTokenId = domain.eosTokenId,
unknownTokenId = domain.unknownTokenId,
paddingTokenId = domain.paddingTokenId,
addBosToken = domain.addBosToken,
addEosToken = domain.addEosToken,
chatTemplate = domain.chatTemplate
)
}
}
@Serializable
data class DimensionsInfo(
val contextLength: Int? = null,
val embeddingSize: Int? = null,
val blockCount: Int? = null,
val feedForwardSize: Int? = null,
) {
companion object {
fun fromDomain(domain: Domain.DimensionsInfo) = DimensionsInfo(
contextLength = domain.contextLength,
embeddingSize = domain.embeddingSize,
blockCount = domain.blockCount,
feedForwardSize = domain.feedForwardSize
)
}
}
@Serializable
data class AttentionInfo(
val headCount: Int? = null,
val headCountKv: Int? = null,
val keyLength: Int? = null,
val valueLength: Int? = null,
val layerNormEpsilon: Float? = null,
val layerNormRmsEpsilon: Float? = null,
) {
companion object {
fun fromDomain(domain: Domain.AttentionInfo) = AttentionInfo(
headCount = domain.headCount,
headCountKv = domain.headCountKv,
keyLength = domain.keyLength,
valueLength = domain.valueLength,
layerNormEpsilon = domain.layerNormEpsilon,
layerNormRmsEpsilon = domain.layerNormRmsEpsilon
)
}
}
@Serializable
data class RopeInfo(
val frequencyBase: Float? = null,
val dimensionCount: Int? = null,
val scalingType: String? = null,
val scalingFactor: Float? = null,
val attnFactor: Float? = null,
val originalContextLength: Int? = null,
val finetuned: Boolean? = null,
) {
companion object {
fun fromDomain(domain: Domain.RopeInfo) = RopeInfo(
frequencyBase = domain.frequencyBase,
dimensionCount = domain.dimensionCount,
scalingType = domain.scalingType,
scalingFactor = domain.scalingFactor,
attnFactor = domain.attnFactor,
originalContextLength = domain.originalContextLength,
finetuned = domain.finetuned
)
}
}
@Serializable
data class ExpertsInfo(
val count: Int? = null,
val usedCount: Int? = null,
) {
companion object {
fun fromDomain(domain: Domain.ExpertsInfo) = ExpertsInfo(
count = domain.count,
usedCount = domain.usedCount
)
}
}
companion object {
fun fromDomain(domain: Domain) = GgufMetadata(
version = GgufVersion.fromDomain(domain.version),
tensorCount = domain.tensorCount,
kvCount = domain.kvCount,
basic = BasicInfo.fromDomain(domain.basic),
author = domain.author?.let { AuthorInfo.fromDomain(it) },
additional = domain.additional?.let { AdditionalInfo.fromDomain(it) },
architecture = domain.architecture?.let { ArchitectureInfo.fromDomain(it) },
baseModels = domain.baseModels?.map { BaseModelInfo.fromDomain(it) },
tokenizer = domain.tokenizer?.let { TokenizerInfo.fromDomain(it) },
dimensions = domain.dimensions?.let { DimensionsInfo.fromDomain(it) },
attention = domain.attention?.let { AttentionInfo.fromDomain(it) },
rope = domain.rope?.let { RopeInfo.fromDomain(it) },
experts = domain.experts?.let { ExpertsInfo.fromDomain(it) }
)
}
}

View File

@ -1,7 +1,6 @@
package com.example.llama.data.model
import com.example.llama.util.FileType
import com.example.llama.util.GgufMetadata
import android.llama.cpp.gguf.FileType
import com.example.llama.util.formatContextLength
import com.example.llama.util.formatFileByteSize

View File

@ -1,18 +1,18 @@
package com.example.llama.data.repository
import android.content.Context
import android.llama.cpp.gguf.GgufMetadataReader
import android.net.Uri
import android.os.StatFs
import android.util.Log
import com.example.llama.data.local.dao.ModelDao
import com.example.llama.data.local.entity.ModelEntity
import com.example.llama.data.model.GgufMetadata
import com.example.llama.data.model.ModelInfo
import com.example.llama.data.remote.HuggingFaceModel
import com.example.llama.data.remote.HuggingFaceRemoteDataSource
import com.example.llama.data.repository.ModelRepository.ImportProgressTracker
import com.example.llama.monitoring.StorageMetrics
import com.example.llama.util.GgufMetadata
import com.example.llama.util.GgufMetadataReader
import com.example.llama.util.copyWithBuffer
import com.example.llama.util.copyWithChannels
import com.example.llama.util.formatFileByteSize
@ -224,7 +224,7 @@ class ModelRepositoryImpl @Inject constructor(
val metadata = try {
val filePath = modelFile.absolutePath
Log.i(TAG, "Extracting GGUF Metadata from $filePath")
GgufMetadataReader().readStructuredMetadata(filePath)
GgufMetadata.fromDomain(GgufMetadataReader().readStructuredMetadata(filePath))
} catch (e: Exception) {
Log.e(TAG, "Cannot extract GGUF metadata: ${e.message}", e)
throw e

View File

@ -1,5 +1,6 @@
package com.example.llama.ui.screens
import android.llama.cpp.gguf.FileType
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
@ -25,7 +26,6 @@ import com.example.llama.data.model.ModelInfo
import com.example.llama.ui.components.ModelCardContentArchitectureRow
import com.example.llama.ui.components.ModelCardContentContextRow
import com.example.llama.ui.components.ModelCardContentTitleRow
import com.example.llama.util.FileType
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

View File

@ -1,13 +1,11 @@
package com.example.llama.util
package android.llama.cpp.gguf
import kotlinx.serialization.Serializable
import java.io.IOException
/**
* Structured metadata of GGUF
*/
@Serializable
data class GgufMetadata(
// Basic file info
val version: GgufVersion,
@ -28,46 +26,6 @@ data class GgufMetadata(
val rope: RopeInfo? = null,
val experts: ExpertsInfo? = null
) {
/** Human-readable full model name + size */
val fullModelName: String?
get() = when {
basic.nameLabel != null -> basic.nameLabel
basic.name != null && basic.sizeLabel != null -> "${basic.name}-${basic.sizeLabel}"
basic.name != null -> basic.name
else -> null
}
/** Humanreadable model name (spaces). */
val primaryName: String?
get() = basic.nameLabel
?: baseModels?.firstNotNullOfOrNull { it.name }
?: basic.name
/** CLIfriendly slug (hyphens). */
val primaryBasename: String?
get() = basic.name
?: baseModels?.firstNotNullOfOrNull { it.name?.replace(' ', '-') }
/** URL pointing to model homepage/repo. */
val primaryUrl: String?
get() = author?.url
?: baseModels?.firstNotNullOfOrNull { it.url }
val primaryRepoUrl: String?
get() = author?.repoUrl
?: baseModels?.firstNotNullOfOrNull { it.repoUrl }
/** Organisation string. */
val primaryOrganization: String?
get() = author?.organization
?: baseModels?.firstNotNullOfOrNull { it.organization }
/** Author string. */
val primaryAuthor: String?
get() = author?.author
?: baseModels?.firstNotNullOfOrNull { it.author }
@Serializable
enum class GgufVersion(val code: Int, val label: String) {
/** First public draft; littleendian only, no alignment key. */
LEGACY_V1(1, "Legacy v1"),
@ -87,7 +45,6 @@ data class GgufMetadata(
override fun toString(): String = "$label (code=$code)"
}
@Serializable
data class BasicInfo(
val uuid: String? = null,
val name: String? = null,
@ -95,7 +52,6 @@ data class GgufMetadata(
val sizeLabel: String? = null, // Size label like "7B"
)
@Serializable
data class AuthorInfo(
val organization: String? = null,
val author: String? = null,
@ -106,7 +62,6 @@ data class GgufMetadata(
val licenseLink: String? = null,
)
@Serializable
data class AdditionalInfo(
val type: String? = null,
val description: String? = null,
@ -114,7 +69,6 @@ data class GgufMetadata(
val languages: List<String>? = null,
)
@Serializable
data class ArchitectureInfo(
val architecture: String? = null,
val fileType: Int? = null,
@ -123,7 +77,6 @@ data class GgufMetadata(
val quantizationVersion: Int? = null,
)
@Serializable
data class BaseModelInfo(
val name: String? = null,
val author: String? = null,
@ -135,7 +88,6 @@ data class GgufMetadata(
val repoUrl: String? = null,
)
@Serializable
data class TokenizerInfo(
val model: String? = null,
val bosTokenId: Int? = null,
@ -147,7 +99,6 @@ data class GgufMetadata(
val chatTemplate: String? = null,
)
@Serializable
data class DimensionsInfo(
val contextLength: Int? = null,
val embeddingSize: Int? = null,
@ -155,7 +106,6 @@ data class GgufMetadata(
val feedForwardSize: Int? = null,
)
@Serializable
data class AttentionInfo(
val headCount: Int? = null,
val headCountKv: Int? = null,
@ -165,7 +115,6 @@ data class GgufMetadata(
val layerNormRmsEpsilon: Float? = null,
)
@Serializable
data class RopeInfo(
val frequencyBase: Float? = null,
val dimensionCount: Int? = null,
@ -176,7 +125,6 @@ data class GgufMetadata(
val finetuned: Boolean? = null,
)
@Serializable
data class ExpertsInfo(
val count: Int? = null,
val usedCount: Int? = null,

View File

@ -1,8 +1,8 @@
package com.example.llama.util
package android.llama.cpp.gguf
import java.io.File
import java.io.InputStream
import java.io.IOException
import java.io.InputStream
private val DEFAULT_SKIP_KEYS = setOf(
"tokenizer.chat_template",
@ -296,19 +296,19 @@ class GgufMetadataReader(
).takeUnless { count == null && usedCount == null }
return GgufMetadata(
version = version,
version = version,
tensorCount = tensorCnt,
kvCount = kvCnt,
basic = basic,
author = author,
additional = additional,
kvCount = kvCnt,
basic = basic,
author = author,
additional = additional,
architecture = architectureInfo,
baseModels = baseModels,
tokenizer = tokenizer,
dimensions = dimensions,
attention = attention,
rope = rope,
experts = experts
baseModels = baseModels,
tokenizer = tokenizer,
dimensions = dimensions,
attention = attention,
rope = rope,
experts = experts
)
}