@dvai-bridge/capacitor-llama 4.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,238 +1,238 @@
1
- package co.deepvoiceai.bridge.llama
2
-
3
- import androidx.test.ext.junit.runners.AndroidJUnit4
4
- import androidx.test.platform.app.InstrumentationRegistry
5
- import co.deepvoiceai.bridge.llama.core.LlamaCppBridge
6
- import co.deepvoiceai.bridge.llama.core.ModelDownloader
7
- import co.deepvoiceai.bridge.llama.core.MTMD_MEDIA_MARKER
8
- import org.junit.After
9
- import org.junit.Assert.assertFalse
10
- import org.junit.Assert.assertNotNull
11
- import org.junit.Assert.assertTrue
12
- import org.junit.Assume.assumeTrue
13
- import org.junit.Test
14
- import org.junit.runner.RunWith
15
- import java.io.File
16
-
17
- /**
18
- * End-to-end smoke test against a small public GGUF model. Verifies
19
- * mechanics (download → load → respond → free) only, not output quality.
20
- *
21
- * Reads `smoke_model_url` / `smoke_model_sha256` from the instrumentation
22
- * arguments — the workflow forwards them via:
23
- *
24
- * ./gradlew connectedAndroidTest \
25
- * -Pandroid.testInstrumentationRunnerArguments.smoke_model_url=$URL \
26
- * -Pandroid.testInstrumentationRunnerArguments.smoke_model_sha256=$SHA
27
- *
28
- * When either is missing the test is skipped via `Assume.assumeTrue`,
29
- * so it stays safe to run locally without those args.
30
- */
31
- @RunWith(AndroidJUnit4::class)
32
- class RealModelSmokeTest {
33
- private val ctx = InstrumentationRegistry.getInstrumentation().targetContext
34
- private val args = InstrumentationRegistry.getArguments()
35
- private var bridge: LlamaCppBridge? = null
36
- private var tempDir: File? = null
37
-
38
- @After
39
- fun tearDown() {
40
- bridge?.unload()
41
- bridge = null
42
- tempDir?.deleteRecursively()
43
- tempDir = null
44
- }
45
-
46
- /**
47
- * Vision smoke: download model + mmproj, load both, run a multimodal
48
- * completion against the tiny test image asset. Skips cleanly if any of
49
- * smoke_vision_model_url / smoke_vision_model_sha256 /
50
- * smoke_vision_mmproj_url / smoke_vision_mmproj_sha256 are missing.
51
- */
52
- @Test
53
- fun smokeVisionEndToEnd() {
54
- val modelUrl = args.getString("smoke_vision_model_url")
55
- val modelSha = args.getString("smoke_vision_model_sha256")
56
- val mmprojUrl = args.getString("smoke_vision_mmproj_url")
57
- val mmprojSha = args.getString("smoke_vision_mmproj_sha256")
58
- assumeTrue(
59
- "smoke_vision_* not all provided as instrumentation args; skipping",
60
- !modelUrl.isNullOrEmpty() && !modelSha.isNullOrEmpty() &&
61
- !mmprojUrl.isNullOrEmpty() && !mmprojSha.isNullOrEmpty()
62
- )
63
-
64
- val cacheRoot = File(ctx.cacheDir, "dvai-vision-${System.nanoTime()}")
65
- cacheRoot.mkdirs()
66
- tempDir = cacheRoot
67
-
68
- val downloader = ModelDownloader(ctx, cacheDirOverride = cacheRoot)
69
- val (modelPath, _) = downloader.downloadModel(
70
- url = modelUrl!!,
71
- expectedSha256 = modelSha!!.lowercase(),
72
- destFilename = "smoke-vision-model.gguf",
73
- headers = emptyMap(),
74
- onProgress = { _, _ -> },
75
- )
76
- val (mmprojPath, _) = downloader.downloadModel(
77
- url = mmprojUrl!!,
78
- expectedSha256 = mmprojSha!!.lowercase(),
79
- destFilename = "smoke-vision-mmproj.gguf",
80
- headers = emptyMap(),
81
- onProgress = { _, _ -> },
82
- )
83
-
84
- val bridge = LlamaCppBridge()
85
- this.bridge = bridge
86
- val loaded = bridge.loadModel(
87
- path = modelPath,
88
- mmprojPath = null,
89
- gpuLayers = 99,
90
- contextSize = 4096,
91
- threads = 4,
92
- embeddingMode = false,
93
- )
94
- assertTrue("model load should succeed", loaded)
95
- val mmOk = bridge.loadMmproj(mmprojPath)
96
- assertTrue("mmproj load should succeed", mmOk)
97
- assertTrue("bridge should report mmproj loaded", bridge.isMmprojLoaded())
98
-
99
- // Read the tiny PNG from assets (1x1 transparent pixel).
100
- val imageBytes = ctx.assets.open("images/tiny-test.png").use { it.readBytes() }
101
-
102
- val messages = listOf(mapOf("role" to "user", "content" to "Describe this image: $MTMD_MEDIA_MARKER"))
103
- val chatPrompt = bridge.applyChatTemplate(
104
- templateOverride = null,
105
- messages = messages,
106
- addAssistant = true,
107
- )
108
- assertNotNull("chat template should render", chatPrompt)
109
-
110
- val completion = bridge.completeMultimodalPrompt(
111
- prompt = chatPrompt!!,
112
- media = listOf(imageBytes),
113
- maxTokens = 32,
114
- temperature = 0.0f,
115
- topP = 1.0f,
116
- )
117
- assertNotNull("vision completion should not be null", completion)
118
- assertFalse("vision completion should not be empty", completion!!.isEmpty())
119
- }
120
-
121
- /**
122
- * Audio smoke: same as vision, but with the WAV fixture. Skipped if the
123
- * loaded mmproj has no audio encoder.
124
- */
125
- @Test
126
- fun smokeAudioEndToEnd() {
127
- val modelUrl = args.getString("smoke_vision_model_url")
128
- val modelSha = args.getString("smoke_vision_model_sha256")
129
- val mmprojUrl = args.getString("smoke_vision_mmproj_url")
130
- val mmprojSha = args.getString("smoke_vision_mmproj_sha256")
131
- assumeTrue(
132
- "smoke_vision_* not all provided as instrumentation args; skipping",
133
- !modelUrl.isNullOrEmpty() && !modelSha.isNullOrEmpty() &&
134
- !mmprojUrl.isNullOrEmpty() && !mmprojSha.isNullOrEmpty()
135
- )
136
-
137
- val cacheRoot = File(ctx.cacheDir, "dvai-audio-${System.nanoTime()}")
138
- cacheRoot.mkdirs()
139
- tempDir = cacheRoot
140
-
141
- val downloader = ModelDownloader(ctx, cacheDirOverride = cacheRoot)
142
- val (modelPath, _) = downloader.downloadModel(
143
- url = modelUrl!!,
144
- expectedSha256 = modelSha!!.lowercase(),
145
- destFilename = "smoke-audio-model.gguf",
146
- headers = emptyMap(),
147
- onProgress = { _, _ -> },
148
- )
149
- val (mmprojPath, _) = downloader.downloadModel(
150
- url = mmprojUrl!!,
151
- expectedSha256 = mmprojSha!!.lowercase(),
152
- destFilename = "smoke-audio-mmproj.gguf",
153
- headers = emptyMap(),
154
- onProgress = { _, _ -> },
155
- )
156
-
157
- val bridge = LlamaCppBridge()
158
- this.bridge = bridge
159
- bridge.loadModel(
160
- path = modelPath,
161
- mmprojPath = null,
162
- gpuLayers = 99,
163
- contextSize = 4096,
164
- threads = 4,
165
- embeddingMode = false,
166
- )
167
- bridge.loadMmproj(mmprojPath)
168
- assumeTrue("Loaded mmproj reports no audio encoder; skipping audio smoke", bridge.hasAudioEncoder())
169
-
170
- // mtmd accepts wav/mp3/flac for audio. Use the WAV fixture from assets.
171
- val audioBytes = ctx.assets.open("audio/wav-1s-16khz-mono.wav").use { it.readBytes() }
172
-
173
- val messages = listOf(mapOf("role" to "user", "content" to "Transcribe this: $MTMD_MEDIA_MARKER"))
174
- val chatPrompt = bridge.applyChatTemplate(
175
- templateOverride = null,
176
- messages = messages,
177
- addAssistant = true,
178
- )!!
179
-
180
- val completion = bridge.completeMultimodalPrompt(
181
- prompt = chatPrompt,
182
- media = listOf(audioBytes),
183
- maxTokens = 32,
184
- temperature = 0.0f,
185
- topP = 1.0f,
186
- )
187
- assertNotNull("audio completion should not be null", completion)
188
- assertFalse("audio completion should not be empty", completion!!.isEmpty())
189
- }
190
-
191
- @Test
192
- fun smokeRealModelEndToEnd() {
193
- val url = args.getString("smoke_model_url")
194
- val sha = args.getString("smoke_model_sha256")
195
- assumeTrue(
196
- "smoke_model_url/smoke_model_sha256 not provided as instrumentation args; skipping",
197
- !url.isNullOrEmpty() && !sha.isNullOrEmpty()
198
- )
199
-
200
- val cacheRoot = File(ctx.cacheDir, "dvai-smoke-${System.nanoTime()}")
201
- cacheRoot.mkdirs()
202
- tempDir = cacheRoot
203
-
204
- val downloader = ModelDownloader(ctx, cacheDirOverride = cacheRoot)
205
- val (path, cached) = downloader.downloadModel(
206
- url = url!!,
207
- expectedSha256 = sha!!.lowercase(),
208
- destFilename = "smoke-model.gguf",
209
- headers = emptyMap(),
210
- onProgress = { _, _ -> /* no-op for smoke */ },
211
- )
212
- assertFalse("first download into a fresh temp dir should not be cached", cached)
213
- assertTrue("downloaded file should exist at $path", File(path).exists())
214
-
215
- val bridge = LlamaCppBridge()
216
- this.bridge = bridge
217
- val loaded = bridge.loadModel(
218
- path = path,
219
- mmprojPath = null,
220
- gpuLayers = 99,
221
- contextSize = 2048,
222
- threads = 4,
223
- embeddingMode = false,
224
- )
225
- assertTrue("model load should succeed", loaded)
226
- assertTrue("bridge should report loaded", bridge.isLoaded())
227
-
228
- val completion = bridge.completePrompt(
229
- prompt = "<|begin_of_text|>What is 2+2?",
230
- maxTokens = 32,
231
- temperature = 0.0f,
232
- topP = 1.0f,
233
- )
234
- // Don't assert specific content — that's quality testing, not smoke.
235
- assertNotNull("completion should not be null", completion)
236
- assertFalse("completion should not be empty", completion!!.isEmpty())
237
- }
238
- }
1
+ package co.deepvoiceai.bridge.llama
2
+
3
+ import androidx.test.ext.junit.runners.AndroidJUnit4
4
+ import androidx.test.platform.app.InstrumentationRegistry
5
+ import co.deepvoiceai.bridge.llama.core.LlamaCppBridge
6
+ import co.deepvoiceai.bridge.llama.core.ModelDownloader
7
+ import co.deepvoiceai.bridge.llama.core.MTMD_MEDIA_MARKER
8
+ import org.junit.After
9
+ import org.junit.Assert.assertFalse
10
+ import org.junit.Assert.assertNotNull
11
+ import org.junit.Assert.assertTrue
12
+ import org.junit.Assume.assumeTrue
13
+ import org.junit.Test
14
+ import org.junit.runner.RunWith
15
+ import java.io.File
16
+
17
+ /**
18
+ * End-to-end smoke test against a small public GGUF model. Verifies
19
+ * mechanics (download → load → respond → free) only, not output quality.
20
+ *
21
+ * Reads `smoke_model_url` / `smoke_model_sha256` from the instrumentation
22
+ * arguments — the workflow forwards them via:
23
+ *
24
+ * ./gradlew connectedAndroidTest \
25
+ * -Pandroid.testInstrumentationRunnerArguments.smoke_model_url=$URL \
26
+ * -Pandroid.testInstrumentationRunnerArguments.smoke_model_sha256=$SHA
27
+ *
28
+ * When either is missing the test is skipped via `Assume.assumeTrue`,
29
+ * so it stays safe to run locally without those args.
30
+ */
31
+ @RunWith(AndroidJUnit4::class)
32
+ class RealModelSmokeTest {
33
+ private val ctx = InstrumentationRegistry.getInstrumentation().targetContext
34
+ private val args = InstrumentationRegistry.getArguments()
35
+ private var bridge: LlamaCppBridge? = null
36
+ private var tempDir: File? = null
37
+
38
+ @After
39
+ fun tearDown() {
40
+ bridge?.unload()
41
+ bridge = null
42
+ tempDir?.deleteRecursively()
43
+ tempDir = null
44
+ }
45
+
46
+ /**
47
+ * Vision smoke: download model + mmproj, load both, run a multimodal
48
+ * completion against the tiny test image asset. Skips cleanly if any of
49
+ * smoke_vision_model_url / smoke_vision_model_sha256 /
50
+ * smoke_vision_mmproj_url / smoke_vision_mmproj_sha256 are missing.
51
+ */
52
+ @Test
53
+ fun smokeVisionEndToEnd() {
54
+ val modelUrl = args.getString("smoke_vision_model_url")
55
+ val modelSha = args.getString("smoke_vision_model_sha256")
56
+ val mmprojUrl = args.getString("smoke_vision_mmproj_url")
57
+ val mmprojSha = args.getString("smoke_vision_mmproj_sha256")
58
+ assumeTrue(
59
+ "smoke_vision_* not all provided as instrumentation args; skipping",
60
+ !modelUrl.isNullOrEmpty() && !modelSha.isNullOrEmpty() &&
61
+ !mmprojUrl.isNullOrEmpty() && !mmprojSha.isNullOrEmpty()
62
+ )
63
+
64
+ val cacheRoot = File(ctx.cacheDir, "dvai-vision-${System.nanoTime()}")
65
+ cacheRoot.mkdirs()
66
+ tempDir = cacheRoot
67
+
68
+ val downloader = ModelDownloader(ctx, cacheDirOverride = cacheRoot)
69
+ val (modelPath, _) = downloader.downloadModel(
70
+ url = modelUrl!!,
71
+ expectedSha256 = modelSha!!.lowercase(),
72
+ destFilename = "smoke-vision-model.gguf",
73
+ headers = emptyMap(),
74
+ onProgress = { _, _ -> },
75
+ )
76
+ val (mmprojPath, _) = downloader.downloadModel(
77
+ url = mmprojUrl!!,
78
+ expectedSha256 = mmprojSha!!.lowercase(),
79
+ destFilename = "smoke-vision-mmproj.gguf",
80
+ headers = emptyMap(),
81
+ onProgress = { _, _ -> },
82
+ )
83
+
84
+ val bridge = LlamaCppBridge()
85
+ this.bridge = bridge
86
+ val loaded = bridge.loadModel(
87
+ path = modelPath,
88
+ mmprojPath = null,
89
+ gpuLayers = 99,
90
+ contextSize = 4096,
91
+ threads = 4,
92
+ embeddingMode = false,
93
+ )
94
+ assertTrue("model load should succeed", loaded)
95
+ val mmOk = bridge.loadMmproj(mmprojPath)
96
+ assertTrue("mmproj load should succeed", mmOk)
97
+ assertTrue("bridge should report mmproj loaded", bridge.isMmprojLoaded())
98
+
99
+ // Read the tiny PNG from assets (1x1 transparent pixel).
100
+ val imageBytes = ctx.assets.open("images/tiny-test.png").use { it.readBytes() }
101
+
102
+ val messages = listOf(mapOf("role" to "user", "content" to "Describe this image: $MTMD_MEDIA_MARKER"))
103
+ val chatPrompt = bridge.applyChatTemplate(
104
+ templateOverride = null,
105
+ messages = messages,
106
+ addAssistant = true,
107
+ )
108
+ assertNotNull("chat template should render", chatPrompt)
109
+
110
+ val completion = bridge.completeMultimodalPrompt(
111
+ prompt = chatPrompt!!,
112
+ media = listOf(imageBytes),
113
+ maxTokens = 32,
114
+ temperature = 0.0f,
115
+ topP = 1.0f,
116
+ )
117
+ assertNotNull("vision completion should not be null", completion)
118
+ assertFalse("vision completion should not be empty", completion!!.isEmpty())
119
+ }
120
+
121
+ /**
122
+ * Audio smoke: same as vision, but with the WAV fixture. Skipped if the
123
+ * loaded mmproj has no audio encoder.
124
+ */
125
+ @Test
126
+ fun smokeAudioEndToEnd() {
127
+ val modelUrl = args.getString("smoke_vision_model_url")
128
+ val modelSha = args.getString("smoke_vision_model_sha256")
129
+ val mmprojUrl = args.getString("smoke_vision_mmproj_url")
130
+ val mmprojSha = args.getString("smoke_vision_mmproj_sha256")
131
+ assumeTrue(
132
+ "smoke_vision_* not all provided as instrumentation args; skipping",
133
+ !modelUrl.isNullOrEmpty() && !modelSha.isNullOrEmpty() &&
134
+ !mmprojUrl.isNullOrEmpty() && !mmprojSha.isNullOrEmpty()
135
+ )
136
+
137
+ val cacheRoot = File(ctx.cacheDir, "dvai-audio-${System.nanoTime()}")
138
+ cacheRoot.mkdirs()
139
+ tempDir = cacheRoot
140
+
141
+ val downloader = ModelDownloader(ctx, cacheDirOverride = cacheRoot)
142
+ val (modelPath, _) = downloader.downloadModel(
143
+ url = modelUrl!!,
144
+ expectedSha256 = modelSha!!.lowercase(),
145
+ destFilename = "smoke-audio-model.gguf",
146
+ headers = emptyMap(),
147
+ onProgress = { _, _ -> },
148
+ )
149
+ val (mmprojPath, _) = downloader.downloadModel(
150
+ url = mmprojUrl!!,
151
+ expectedSha256 = mmprojSha!!.lowercase(),
152
+ destFilename = "smoke-audio-mmproj.gguf",
153
+ headers = emptyMap(),
154
+ onProgress = { _, _ -> },
155
+ )
156
+
157
+ val bridge = LlamaCppBridge()
158
+ this.bridge = bridge
159
+ bridge.loadModel(
160
+ path = modelPath,
161
+ mmprojPath = null,
162
+ gpuLayers = 99,
163
+ contextSize = 4096,
164
+ threads = 4,
165
+ embeddingMode = false,
166
+ )
167
+ bridge.loadMmproj(mmprojPath)
168
+ assumeTrue("Loaded mmproj reports no audio encoder; skipping audio smoke", bridge.hasAudioEncoder())
169
+
170
+ // mtmd accepts wav/mp3/flac for audio. Use the WAV fixture from assets.
171
+ val audioBytes = ctx.assets.open("audio/wav-1s-16khz-mono.wav").use { it.readBytes() }
172
+
173
+ val messages = listOf(mapOf("role" to "user", "content" to "Transcribe this: $MTMD_MEDIA_MARKER"))
174
+ val chatPrompt = bridge.applyChatTemplate(
175
+ templateOverride = null,
176
+ messages = messages,
177
+ addAssistant = true,
178
+ )!!
179
+
180
+ val completion = bridge.completeMultimodalPrompt(
181
+ prompt = chatPrompt,
182
+ media = listOf(audioBytes),
183
+ maxTokens = 32,
184
+ temperature = 0.0f,
185
+ topP = 1.0f,
186
+ )
187
+ assertNotNull("audio completion should not be null", completion)
188
+ assertFalse("audio completion should not be empty", completion!!.isEmpty())
189
+ }
190
+
191
+ @Test
192
+ fun smokeRealModelEndToEnd() {
193
+ val url = args.getString("smoke_model_url")
194
+ val sha = args.getString("smoke_model_sha256")
195
+ assumeTrue(
196
+ "smoke_model_url/smoke_model_sha256 not provided as instrumentation args; skipping",
197
+ !url.isNullOrEmpty() && !sha.isNullOrEmpty()
198
+ )
199
+
200
+ val cacheRoot = File(ctx.cacheDir, "dvai-smoke-${System.nanoTime()}")
201
+ cacheRoot.mkdirs()
202
+ tempDir = cacheRoot
203
+
204
+ val downloader = ModelDownloader(ctx, cacheDirOverride = cacheRoot)
205
+ val (path, cached) = downloader.downloadModel(
206
+ url = url!!,
207
+ expectedSha256 = sha!!.lowercase(),
208
+ destFilename = "smoke-model.gguf",
209
+ headers = emptyMap(),
210
+ onProgress = { _, _ -> /* no-op for smoke */ },
211
+ )
212
+ assertFalse("first download into a fresh temp dir should not be cached", cached)
213
+ assertTrue("downloaded file should exist at $path", File(path).exists())
214
+
215
+ val bridge = LlamaCppBridge()
216
+ this.bridge = bridge
217
+ val loaded = bridge.loadModel(
218
+ path = path,
219
+ mmprojPath = null,
220
+ gpuLayers = 99,
221
+ contextSize = 2048,
222
+ threads = 4,
223
+ embeddingMode = false,
224
+ )
225
+ assertTrue("model load should succeed", loaded)
226
+ assertTrue("bridge should report loaded", bridge.isLoaded())
227
+
228
+ val completion = bridge.completePrompt(
229
+ prompt = "<|begin_of_text|>What is 2+2?",
230
+ maxTokens = 32,
231
+ temperature = 0.0f,
232
+ topP = 1.0f,
233
+ )
234
+ // Don't assert specific content — that's quality testing, not smoke.
235
+ assertNotNull("completion should not be null", completion)
236
+ assertFalse("completion should not be empty", completion!!.isEmpty())
237
+ }
238
+ }
@@ -1,7 +1,7 @@
1
- <?xml version="1.0" encoding="utf-8"?>
2
- <manifest xmlns:android="http://schemas.android.com/apk/res/android"
3
- xmlns:tools="http://schemas.android.com/tools">
4
- <application
5
- android:networkSecurityConfig="@xml/dvai_network_security_config"
6
- tools:replace="android:networkSecurityConfig" />
7
- </manifest>
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
3
+ xmlns:tools="http://schemas.android.com/tools">
4
+ <application
5
+ android:networkSecurityConfig="@xml/dvai_network_security_config"
6
+ tools:replace="android:networkSecurityConfig" />
7
+ </manifest>