lib: expose GgufMetadataReader as interface only

This commit is contained in:
Han Yin 2025-06-26 18:52:40 -07:00
parent 6a5bc94ff1
commit 130cba9aa6
3 changed files with 59 additions and 14 deletions

View File

@ -107,6 +107,7 @@ class ModelRepositoryImpl @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val modelDao: ModelDao, private val modelDao: ModelDao,
private val huggingFaceRemoteDataSource: HuggingFaceRemoteDataSource, private val huggingFaceRemoteDataSource: HuggingFaceRemoteDataSource,
private val ggufMetadataReader: GgufMetadataReader,
) : ModelRepository { ) : ModelRepository {
private val modelsDir = File(context.filesDir, INTERNAL_STORAGE_PATH) private val modelsDir = File(context.filesDir, INTERNAL_STORAGE_PATH)
@ -224,7 +225,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")
GgufMetadata.fromDomain(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

@ -3,6 +3,7 @@ package com.example.llama.di
import android.content.Context import android.content.Context
import android.llama.cpp.InferenceEngine import android.llama.cpp.InferenceEngine
import android.llama.cpp.InferenceEngineLoader import android.llama.cpp.InferenceEngineLoader
import android.llama.cpp.gguf.GgufMetadataReader
import com.example.llama.data.local.AppDatabase import com.example.llama.data.local.AppDatabase
import com.example.llama.data.remote.HuggingFaceApiService import com.example.llama.data.remote.HuggingFaceApiService
import com.example.llama.data.remote.HuggingFaceRemoteDataSource import com.example.llama.data.remote.HuggingFaceRemoteDataSource
@ -82,6 +83,10 @@ internal abstract class AppModule {
@Provides @Provides
fun providesSystemPromptDao(appDatabase: AppDatabase) = appDatabase.systemPromptDao() fun providesSystemPromptDao(appDatabase: AppDatabase) = appDatabase.systemPromptDao()
@Provides
@Singleton
fun providesGgufMetadataReader(): GgufMetadataReader = GgufMetadataReader.create()
@Provides @Provides
@Singleton @Singleton
fun provideOkhttpClient() = OkHttpClient.Builder() fun provideOkhttpClient() = OkHttpClient.Builder()

View File

@ -4,23 +4,62 @@ import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
private val DEFAULT_SKIP_KEYS = setOf( /**
"tokenizer.chat_template", * Interface for reading GGUF metadata from model files.
"tokenizer.ggml.scores", * Use `GgufMetadataReader.create()` to get an instance.
"tokenizer.ggml.tokens", */
"tokenizer.ggml.token_type" interface GgufMetadataReader {
) /**
* Reads and parses GGUF metadata from the specified file path.
*
* @param path The absolute path to the GGUF file
* @return Structured metadata extracted from the file
* @throws IOException if file cannot be read
* @throws IllegalArgumentException if file format is invalid
*/
suspend fun readStructuredMetadata(path: String): GgufMetadata
companion object {
private val DEFAULT_SKIP_KEYS = setOf(
"tokenizer.chat_template",
"tokenizer.ggml.scores",
"tokenizer.ggml.tokens",
"tokenizer.ggml.token_type"
)
/**
* Creates a default GgufMetadataReader instance
*/
fun create(): GgufMetadataReader = GgufMetadataReaderImpl(
skipKeys = DEFAULT_SKIP_KEYS,
arraySummariseThreshold = 1_000
)
/**
* Creates a GgufMetadataReader with custom configuration
*
* @param skipKeys Keys whose value should be skipped entirely (not kept in the result map)
* @param arraySummariseThreshold If 0, arrays longer get summarised, not materialised;
* If -1, never summarise.
*/
fun create(
skipKeys: Set<String> = DEFAULT_SKIP_KEYS,
arraySummariseThreshold: Int = 1_000
): GgufMetadataReader = GgufMetadataReaderImpl(
skipKeys = skipKeys,
arraySummariseThreshold = arraySummariseThreshold
)
}
}
/** /**
* Utility class to read GGUF model files and extract metadata key-value pairs. * Utility class to read GGUF model files and extract metadata key-value pairs.
* This parser reads the header and metadata of a GGUF v3 file (little-endian) and skips tensor data. * This parser reads the header and metadata of a GGUF v3 file (little-endian) and skips tensor data.
*/ */
class GgufMetadataReader( private class GgufMetadataReaderImpl(
/** Keys whose value should be skipped entirely (not kept in the resulting map). */ private val skipKeys: Set<String>,
private val skipKeys: Set<String> = DEFAULT_SKIP_KEYS, private val arraySummariseThreshold: Int,
/** If ≥0, arrays longer than this get summarised instead of materialised. -1 ⇒ never summarise. */ ) : GgufMetadataReader {
private val arraySummariseThreshold: Int = 1_000
) {
companion object { companion object {
private const val ARCH_LLAMA = "llama" private const val ARCH_LLAMA = "llama"
} }
@ -92,7 +131,7 @@ class GgufMetadataReader(
* @throws IOException if the file is not GGUF, the version is unsupported, * @throws IOException if the file is not GGUF, the version is unsupported,
* or the metadata block is truncated / corrupt. * or the metadata block is truncated / corrupt.
*/ */
fun readStructuredMetadata(path: String): GgufMetadata { override suspend fun readStructuredMetadata(path: String): GgufMetadata {
File(path).inputStream().buffered().use { input -> File(path).inputStream().buffered().use { input ->
// ── 1. header ────────────────────────────────────────────────────────── // ── 1. header ──────────────────────────────────────────────────────────
// throws on mismatch // throws on mismatch