[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 package com.example.llama.data.local.converter
import androidx.room.TypeConverter import androidx.room.TypeConverter
import com.example.llama.util.GgufMetadata import com.example.llama.data.model.GgufMetadata
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
class GgufMetadataConverters { class GgufMetadataConverters {

View File

@ -5,7 +5,7 @@ import androidx.room.PrimaryKey
import androidx.room.TypeConverters import androidx.room.TypeConverters
import com.example.llama.data.local.converter.GgufMetadataConverters import com.example.llama.data.local.converter.GgufMetadataConverters
import com.example.llama.data.model.ModelInfo import com.example.llama.data.model.ModelInfo
import com.example.llama.util.GgufMetadata import com.example.llama.data.model.GgufMetadata
@Entity(tableName = "models") @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 package com.example.llama.data.model
import com.example.llama.util.FileType import android.llama.cpp.gguf.FileType
import com.example.llama.util.GgufMetadata
import com.example.llama.util.formatContextLength import com.example.llama.util.formatContextLength
import com.example.llama.util.formatFileByteSize import com.example.llama.util.formatFileByteSize

View File

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

View File

@ -1,5 +1,6 @@
package com.example.llama.ui.screens package com.example.llama.ui.screens
import android.llama.cpp.gguf.FileType
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi 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.ModelCardContentArchitectureRow
import com.example.llama.ui.components.ModelCardContentContextRow import com.example.llama.ui.components.ModelCardContentContextRow
import com.example.llama.ui.components.ModelCardContentTitleRow import com.example.llama.ui.components.ModelCardContentTitleRow
import com.example.llama.util.FileType
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale 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 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,
@ -28,46 +26,6 @@ data class GgufMetadata(
val rope: RopeInfo? = null, val rope: RopeInfo? = null,
val experts: ExpertsInfo? = 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) { 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"),
@ -87,7 +45,6 @@ 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,7 +52,6 @@ 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,
@ -106,7 +62,6 @@ 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,
@ -114,7 +69,6 @@ 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,
@ -123,7 +77,6 @@ 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,
@ -135,7 +88,6 @@ 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,
@ -147,7 +99,6 @@ 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,
@ -155,7 +106,6 @@ 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,
@ -165,7 +115,6 @@ 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,
@ -176,7 +125,6 @@ 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,

View File

@ -1,8 +1,8 @@
package com.example.llama.util package android.llama.cpp.gguf
import java.io.File import java.io.File
import java.io.InputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream
private val DEFAULT_SKIP_KEYS = setOf( private val DEFAULT_SKIP_KEYS = setOf(
"tokenizer.chat_template", "tokenizer.chat_template",