@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.
- package/DVAICapacitorLlama.podspec +21 -21
- package/LICENSE +341 -34
- package/android/build.gradle +91 -91
- package/android/gradle.properties +4 -4
- package/android/settings.gradle +4 -4
- package/android/src/androidTest/java/co/deepvoiceai/bridge/llama/RealModelSmokeTest.kt +238 -238
- package/android/src/main/AndroidManifest.xml +7 -7
- package/android/src/main/java/co/deepvoiceai/bridge/llama/Plugin.kt +177 -177
- package/android/src/main/res/xml/dvai_network_security_config.xml +7 -7
- package/android/src/test/java/co/deepvoiceai/bridge/llama/SmokeTest.kt +11 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/ios/Package.swift +62 -57
- package/ios/Sources/DVAICapacitorLlama/Plugin.swift +154 -154
- package/ios/Sources/DVAICapacitorLlama/PluginProxy.m +15 -15
- package/ios/Tests/DVAICapacitorLlamaTests/RealModelSmokeTest.swift +429 -412
- package/ios/Tests/DVAICapacitorLlamaTests/SmokeTest.swift +15 -10
- package/package.json +6 -6
- package/README.md +0 -199
|
@@ -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>
|