diff --git a/examples/llama.android/app/src/main/java/com/example/llama/data/repository/ModelRepository.kt b/examples/llama.android/app/src/main/java/com/example/llama/data/repository/ModelRepository.kt index bcc3721e36..f205eb8240 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/data/repository/ModelRepository.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/data/repository/ModelRepository.kt @@ -107,6 +107,7 @@ class ModelRepositoryImpl @Inject constructor( @ApplicationContext private val context: Context, private val modelDao: ModelDao, private val huggingFaceRemoteDataSource: HuggingFaceRemoteDataSource, + private val ggufMetadataReader: GgufMetadataReader, ) : ModelRepository { private val modelsDir = File(context.filesDir, INTERNAL_STORAGE_PATH) @@ -224,7 +225,7 @@ class ModelRepositoryImpl @Inject constructor( val metadata = try { val filePath = modelFile.absolutePath Log.i(TAG, "Extracting GGUF Metadata from $filePath") - GgufMetadata.fromDomain(GgufMetadataReader().readStructuredMetadata(filePath)) + GgufMetadata.fromDomain(ggufMetadataReader.readStructuredMetadata(filePath)) } catch (e: Exception) { Log.e(TAG, "Cannot extract GGUF metadata: ${e.message}", e) throw e diff --git a/examples/llama.android/app/src/main/java/com/example/llama/di/AppModule.kt b/examples/llama.android/app/src/main/java/com/example/llama/di/AppModule.kt index 24f6549afb..3b58b6ca5d 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/di/AppModule.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/di/AppModule.kt @@ -3,6 +3,7 @@ package com.example.llama.di import android.content.Context import android.llama.cpp.InferenceEngine import android.llama.cpp.InferenceEngineLoader +import android.llama.cpp.gguf.GgufMetadataReader import com.example.llama.data.local.AppDatabase import com.example.llama.data.remote.HuggingFaceApiService import com.example.llama.data.remote.HuggingFaceRemoteDataSource @@ -82,6 +83,10 @@ internal abstract class AppModule { @Provides fun providesSystemPromptDao(appDatabase: AppDatabase) = appDatabase.systemPromptDao() + @Provides + @Singleton + fun providesGgufMetadataReader(): GgufMetadataReader = GgufMetadataReader.create() + @Provides @Singleton fun provideOkhttpClient() = OkHttpClient.Builder() diff --git a/examples/llama.android/llama/src/main/java/android/llama/cpp/gguf/GgufMetadataReader.kt b/examples/llama.android/llama/src/main/java/android/llama/cpp/gguf/GgufMetadataReader.kt index 6c1652e788..777cb8ec47 100644 --- a/examples/llama.android/llama/src/main/java/android/llama/cpp/gguf/GgufMetadataReader.kt +++ b/examples/llama.android/llama/src/main/java/android/llama/cpp/gguf/GgufMetadataReader.kt @@ -4,23 +4,62 @@ import java.io.File import java.io.IOException import java.io.InputStream -private val DEFAULT_SKIP_KEYS = setOf( - "tokenizer.chat_template", - "tokenizer.ggml.scores", - "tokenizer.ggml.tokens", - "tokenizer.ggml.token_type" -) +/** + * Interface for reading GGUF metadata from model files. + * Use `GgufMetadataReader.create()` to get an instance. + */ +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 = 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. * This parser reads the header and metadata of a GGUF v3 file (little-endian) and skips tensor data. */ -class GgufMetadataReader( - /** Keys whose value should be skipped entirely (not kept in the resulting map). */ - private val skipKeys: Set = DEFAULT_SKIP_KEYS, - /** If ≥0, arrays longer than this get summarised instead of materialised. -1 ⇒ never summarise. */ - private val arraySummariseThreshold: Int = 1_000 -) { +private class GgufMetadataReaderImpl( + private val skipKeys: Set, + private val arraySummariseThreshold: Int, +) : GgufMetadataReader { companion object { private const val ARCH_LLAMA = "llama" } @@ -92,7 +131,7 @@ class GgufMetadataReader( * @throws IOException if the file is not GGUF, the version is unsupported, * 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 -> // ── 1. header ────────────────────────────────────────────────────────── // throws on mismatch