@dvai-bridge/capacitor-llama 4.0.0 → 4.0.2

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,177 +1,177 @@
1
- package co.deepvoiceai.bridge.llama
2
-
3
- import com.getcapacitor.JSArray
4
- import com.getcapacitor.JSObject
5
- import com.getcapacitor.Plugin
6
- import com.getcapacitor.PluginCall
7
- import com.getcapacitor.PluginMethod
8
- import com.getcapacitor.annotation.CapacitorPlugin
9
- import co.deepvoiceai.bridge.llama.core.ModelDownloader
10
- import co.deepvoiceai.bridge.llama.core.PluginState
11
- import kotlinx.coroutines.CoroutineScope
12
- import kotlinx.coroutines.Dispatchers
13
- import kotlinx.coroutines.SupervisorJob
14
- import kotlinx.coroutines.cancel
15
- import kotlinx.coroutines.launch
16
-
17
- // ---------------------------------------------------------------------------
18
- // JSObject ↔ Map translation helpers — needed because PluginState's API is
19
- // Capacitor-neutral (Map<String, Any?>) while the JS-bridge layer works with
20
- // Capacitor's JSObject (which extends JSONObject and has no .toMap()).
21
- // ---------------------------------------------------------------------------
22
-
23
- private fun JSObject.toAnyMap(): Map<String, Any?> {
24
- val out = mutableMapOf<String, Any?>()
25
- val it = keys()
26
- while (it.hasNext()) {
27
- val k = it.next()
28
- out[k] = this.opt(k) // returns Any (or JSONObject.NULL); fine for our usage
29
- }
30
- return out
31
- }
32
-
33
- private fun Map<String, Any?>.toJSObject(): JSObject {
34
- val obj = JSObject()
35
- for ((k, v) in this) {
36
- if (v != null) obj.put(k, v)
37
- }
38
- return obj
39
- }
40
-
41
- @CapacitorPlugin(name = "DVAIBridgeLlama")
42
- class DVAIBridgeLlamaPlugin : Plugin() {
43
- private val state = PluginState()
44
- private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
45
- private val downloader: ModelDownloader by lazy { ModelDownloader(context) }
46
-
47
- override fun handleOnDestroy() {
48
- super.handleOnDestroy()
49
- scope.cancel()
50
- }
51
-
52
- @PluginMethod
53
- fun start(call: PluginCall) {
54
- scope.launch {
55
- notifyListeners("progress", JSObject().apply { put("phase", "load") })
56
- try {
57
- val resultMap = state.start((call.data ?: JSObject()).toAnyMap())
58
- notifyListeners("progress", JSObject().apply { put("phase", "ready") })
59
- call.resolve(resultMap.toJSObject())
60
- } catch (e: Exception) {
61
- notifyListeners("progress", JSObject().apply {
62
- put("phase", "error")
63
- put("message", e.message ?: "Start failed")
64
- })
65
- call.reject(e.message ?: "Start failed", e)
66
- }
67
- }
68
- }
69
-
70
- @PluginMethod
71
- fun stop(call: PluginCall) {
72
- scope.launch {
73
- try {
74
- state.stop()
75
- call.resolve()
76
- } catch (e: Exception) {
77
- call.reject(e.message ?: "Stop failed", e)
78
- }
79
- }
80
- }
81
-
82
- @PluginMethod
83
- fun status(call: PluginCall) {
84
- scope.launch {
85
- call.resolve(state.statusInfo().toJSObject())
86
- }
87
- }
88
-
89
- @PluginMethod
90
- fun downloadModel(call: PluginCall) {
91
- val url = call.getString("url") ?: return call.reject("url is required")
92
- val sha = call.getString("sha256")?.takeIf { it.isNotEmpty() }?.lowercase()
93
- ?: return call.reject("sha256 is required")
94
- val destFilename = call.getString("destFilename") ?: url.substringAfterLast('/')
95
- val headers: Map<String, String> = call.getObject("headers")?.let { obj ->
96
- val map = mutableMapOf<String, String>()
97
- obj.keys().forEachRemaining { k -> obj.getString(k)?.let { map[k] = it } }
98
- map
99
- } ?: emptyMap()
100
-
101
- scope.launch {
102
- try {
103
- val (path, cached) = downloader.downloadModel(
104
- url = url,
105
- expectedSha256 = sha,
106
- destFilename = destFilename,
107
- headers = headers,
108
- ) { bytesDone, bytesTotal ->
109
- val payload = JSObject().apply {
110
- put("phase", "download")
111
- put("bytesReceived", bytesDone)
112
- if (bytesTotal != null) {
113
- put("bytesTotal", bytesTotal)
114
- if (bytesTotal > 0) {
115
- put("percent", bytesDone.toDouble() / bytesTotal.toDouble() * 100.0)
116
- }
117
- }
118
- }
119
- notifyListeners("progress", payload)
120
- }
121
- call.resolve(JSObject().apply {
122
- put("path", path)
123
- put("cached", cached)
124
- })
125
- } catch (e: Exception) {
126
- call.reject(e.message ?: "Download failed", e)
127
- }
128
- }
129
- }
130
-
131
- @PluginMethod
132
- fun listCachedModels(call: PluginCall) {
133
- scope.launch {
134
- try {
135
- val infos = downloader.listCached()
136
- val arr = JSArray()
137
- infos.forEach { info ->
138
- arr.put(JSObject().apply {
139
- put("filename", info.filename)
140
- put("path", info.path)
141
- put("bytes", info.bytes)
142
- put("sha256", info.sha256)
143
- })
144
- }
145
- call.resolve(JSObject().apply { put("models", arr) })
146
- } catch (e: Exception) {
147
- call.reject(e.message ?: "List failed", e)
148
- }
149
- }
150
- }
151
-
152
- @PluginMethod
153
- fun deleteCachedModel(call: PluginCall) {
154
- val filename = call.getString("filename")?.takeIf { it.isNotEmpty() }
155
- ?: return call.reject("filename is required")
156
- scope.launch {
157
- try {
158
- downloader.deleteCached(filename)
159
- call.resolve()
160
- } catch (e: Exception) {
161
- call.reject(e.message ?: "Delete failed", e)
162
- }
163
- }
164
- }
165
-
166
- @PluginMethod
167
- fun cacheDir(call: PluginCall) {
168
- scope.launch {
169
- try {
170
- val path = downloader.cacheDir().absolutePath
171
- call.resolve(JSObject().apply { put("path", path) })
172
- } catch (e: Exception) {
173
- call.reject(e.message ?: "cacheDir failed", e)
174
- }
175
- }
176
- }
177
- }
1
+ package co.deepvoiceai.bridge.llama
2
+
3
+ import com.getcapacitor.JSArray
4
+ import com.getcapacitor.JSObject
5
+ import com.getcapacitor.Plugin
6
+ import com.getcapacitor.PluginCall
7
+ import com.getcapacitor.PluginMethod
8
+ import com.getcapacitor.annotation.CapacitorPlugin
9
+ import co.deepvoiceai.bridge.llama.core.ModelDownloader
10
+ import co.deepvoiceai.bridge.llama.core.PluginState
11
+ import kotlinx.coroutines.CoroutineScope
12
+ import kotlinx.coroutines.Dispatchers
13
+ import kotlinx.coroutines.SupervisorJob
14
+ import kotlinx.coroutines.cancel
15
+ import kotlinx.coroutines.launch
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // JSObject ↔ Map translation helpers — needed because PluginState's API is
19
+ // Capacitor-neutral (Map<String, Any?>) while the JS-bridge layer works with
20
+ // Capacitor's JSObject (which extends JSONObject and has no .toMap()).
21
+ // ---------------------------------------------------------------------------
22
+
23
+ private fun JSObject.toAnyMap(): Map<String, Any?> {
24
+ val out = mutableMapOf<String, Any?>()
25
+ val it = keys()
26
+ while (it.hasNext()) {
27
+ val k = it.next()
28
+ out[k] = this.opt(k) // returns Any (or JSONObject.NULL); fine for our usage
29
+ }
30
+ return out
31
+ }
32
+
33
+ private fun Map<String, Any?>.toJSObject(): JSObject {
34
+ val obj = JSObject()
35
+ for ((k, v) in this) {
36
+ if (v != null) obj.put(k, v)
37
+ }
38
+ return obj
39
+ }
40
+
41
+ @CapacitorPlugin(name = "DVAIBridgeLlama")
42
+ class DVAIBridgeLlamaPlugin : Plugin() {
43
+ private val state = PluginState()
44
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
45
+ private val downloader: ModelDownloader by lazy { ModelDownloader(context) }
46
+
47
+ override fun handleOnDestroy() {
48
+ super.handleOnDestroy()
49
+ scope.cancel()
50
+ }
51
+
52
+ @PluginMethod
53
+ fun start(call: PluginCall) {
54
+ scope.launch {
55
+ notifyListeners("progress", JSObject().apply { put("phase", "load") })
56
+ try {
57
+ val resultMap = state.start((call.data ?: JSObject()).toAnyMap())
58
+ notifyListeners("progress", JSObject().apply { put("phase", "ready") })
59
+ call.resolve(resultMap.toJSObject())
60
+ } catch (e: Exception) {
61
+ notifyListeners("progress", JSObject().apply {
62
+ put("phase", "error")
63
+ put("message", e.message ?: "Start failed")
64
+ })
65
+ call.reject(e.message ?: "Start failed", e)
66
+ }
67
+ }
68
+ }
69
+
70
+ @PluginMethod
71
+ fun stop(call: PluginCall) {
72
+ scope.launch {
73
+ try {
74
+ state.stop()
75
+ call.resolve()
76
+ } catch (e: Exception) {
77
+ call.reject(e.message ?: "Stop failed", e)
78
+ }
79
+ }
80
+ }
81
+
82
+ @PluginMethod
83
+ fun status(call: PluginCall) {
84
+ scope.launch {
85
+ call.resolve(state.statusInfo().toJSObject())
86
+ }
87
+ }
88
+
89
+ @PluginMethod
90
+ fun downloadModel(call: PluginCall) {
91
+ val url = call.getString("url") ?: return call.reject("url is required")
92
+ val sha = call.getString("sha256")?.takeIf { it.isNotEmpty() }?.lowercase()
93
+ ?: return call.reject("sha256 is required")
94
+ val destFilename = call.getString("destFilename") ?: url.substringAfterLast('/')
95
+ val headers: Map<String, String> = call.getObject("headers")?.let { obj ->
96
+ val map = mutableMapOf<String, String>()
97
+ obj.keys().forEachRemaining { k -> obj.getString(k)?.let { map[k] = it } }
98
+ map
99
+ } ?: emptyMap()
100
+
101
+ scope.launch {
102
+ try {
103
+ val (path, cached) = downloader.downloadModel(
104
+ url = url,
105
+ expectedSha256 = sha,
106
+ destFilename = destFilename,
107
+ headers = headers,
108
+ ) { bytesDone, bytesTotal ->
109
+ val payload = JSObject().apply {
110
+ put("phase", "download")
111
+ put("bytesReceived", bytesDone)
112
+ if (bytesTotal != null) {
113
+ put("bytesTotal", bytesTotal)
114
+ if (bytesTotal > 0) {
115
+ put("percent", bytesDone.toDouble() / bytesTotal.toDouble() * 100.0)
116
+ }
117
+ }
118
+ }
119
+ notifyListeners("progress", payload)
120
+ }
121
+ call.resolve(JSObject().apply {
122
+ put("path", path)
123
+ put("cached", cached)
124
+ })
125
+ } catch (e: Exception) {
126
+ call.reject(e.message ?: "Download failed", e)
127
+ }
128
+ }
129
+ }
130
+
131
+ @PluginMethod
132
+ fun listCachedModels(call: PluginCall) {
133
+ scope.launch {
134
+ try {
135
+ val infos = downloader.listCached()
136
+ val arr = JSArray()
137
+ infos.forEach { info ->
138
+ arr.put(JSObject().apply {
139
+ put("filename", info.filename)
140
+ put("path", info.path)
141
+ put("bytes", info.bytes)
142
+ put("sha256", info.sha256)
143
+ })
144
+ }
145
+ call.resolve(JSObject().apply { put("models", arr) })
146
+ } catch (e: Exception) {
147
+ call.reject(e.message ?: "List failed", e)
148
+ }
149
+ }
150
+ }
151
+
152
+ @PluginMethod
153
+ fun deleteCachedModel(call: PluginCall) {
154
+ val filename = call.getString("filename")?.takeIf { it.isNotEmpty() }
155
+ ?: return call.reject("filename is required")
156
+ scope.launch {
157
+ try {
158
+ downloader.deleteCached(filename)
159
+ call.resolve()
160
+ } catch (e: Exception) {
161
+ call.reject(e.message ?: "Delete failed", e)
162
+ }
163
+ }
164
+ }
165
+
166
+ @PluginMethod
167
+ fun cacheDir(call: PluginCall) {
168
+ scope.launch {
169
+ try {
170
+ val path = downloader.cacheDir().absolutePath
171
+ call.resolve(JSObject().apply { put("path", path) })
172
+ } catch (e: Exception) {
173
+ call.reject(e.message ?: "cacheDir failed", e)
174
+ }
175
+ }
176
+ }
177
+ }
@@ -1,7 +1,7 @@
1
- <?xml version="1.0" encoding="utf-8"?>
2
- <network-security-config>
3
- <domain-config cleartextTrafficPermitted="true">
4
- <domain includeSubdomains="false">localhost</domain>
5
- <domain includeSubdomains="false">127.0.0.1</domain>
6
- </domain-config>
7
- </network-security-config>
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <network-security-config>
3
+ <domain-config cleartextTrafficPermitted="true">
4
+ <domain includeSubdomains="false">localhost</domain>
5
+ <domain includeSubdomains="false">127.0.0.1</domain>
6
+ </domain-config>
7
+ </network-security-config>
@@ -1,11 +1,11 @@
1
- package co.deepvoiceai.bridge.llama
2
-
3
- import org.junit.Assert.assertNotNull
4
- import org.junit.Test
5
-
6
- class SmokeTest {
7
- @Test
8
- fun pluginClassExists() {
9
- assertNotNull(DVAIBridgeLlamaPlugin::class.java)
10
- }
11
- }
1
+ package co.deepvoiceai.bridge.llama
2
+
3
+ import org.junit.Assert.assertNotNull
4
+ import org.junit.Test
5
+
6
+ class SmokeTest {
7
+ @Test
8
+ fun pluginClassExists() {
9
+ assertNotNull(DVAIBridgeLlamaPlugin::class.java)
10
+ }
11
+ }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { registerPlugin } from \"@capacitor/core\";\r\nimport type { NativePluginInterface } from \"@dvai-bridge/capacitor\";\r\n\r\nconst DVAIBridgeLlama = registerPlugin<NativePluginInterface>(\"DVAIBridgeLlama\");\r\n\r\nexport default DVAIBridgeLlama;\r\nexport { DVAIBridgeLlama };\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAA+B;AAG/B,IAAM,sBAAkB,4BAAsC,iBAAiB;AAE/E,IAAO,gBAAQ;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { registerPlugin } from \"@capacitor/core\";\nimport type { NativePluginInterface } from \"@dvai-bridge/capacitor\";\n\nconst DVAIBridgeLlama = registerPlugin<NativePluginInterface>(\"DVAIBridgeLlama\");\n\nexport default DVAIBridgeLlama;\nexport { DVAIBridgeLlama };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAA+B;AAG/B,IAAM,sBAAkB,4BAAsC,iBAAiB;AAE/E,IAAO,gBAAQ;","names":[]}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { registerPlugin } from \"@capacitor/core\";\r\nimport type { NativePluginInterface } from \"@dvai-bridge/capacitor\";\r\n\r\nconst DVAIBridgeLlama = registerPlugin<NativePluginInterface>(\"DVAIBridgeLlama\");\r\n\r\nexport default DVAIBridgeLlama;\r\nexport { DVAIBridgeLlama };\r\n"],"mappings":";AAAA,SAAS,sBAAsB;AAG/B,IAAM,kBAAkB,eAAsC,iBAAiB;AAE/E,IAAO,gBAAQ;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { registerPlugin } from \"@capacitor/core\";\nimport type { NativePluginInterface } from \"@dvai-bridge/capacitor\";\n\nconst DVAIBridgeLlama = registerPlugin<NativePluginInterface>(\"DVAIBridgeLlama\");\n\nexport default DVAIBridgeLlama;\nexport { DVAIBridgeLlama };\n"],"mappings":";AAAA,SAAS,sBAAsB;AAG/B,IAAM,kBAAkB,eAAsC,iBAAiB;AAE/E,IAAO,gBAAQ;","names":[]}
package/ios/Package.swift CHANGED
@@ -1,57 +1,62 @@
1
- // swift-tools-version: 5.9
2
- import PackageDescription
3
-
4
- let package = Package(
5
- name: "DVAICapacitorLlama",
6
- platforms: [.iOS(.v14), .macOS(.v12)],
7
- products: [
8
- .library(name: "DVAICapacitorLlama", targets: ["DVAICapacitorLlama"]),
9
- ],
10
- dependencies: [
11
- // Capacitor SPM artifact (existing)
12
- .package(url: "https://github.com/ionic-team/capacitor-swift-pm", branch: "main"),
13
- // Core package — relative path during dev, replaced with version-pin
14
- // or git URL at publish time. The path is relative to *this* Package.swift's
15
- // location (`packages/dvai-bridge-capacitor-llama/ios/`), so two `..` get
16
- // us to the `packages/` parent and `dvai-bridge-ios-llama-core/ios` is the
17
- // sibling package's SPM root.
18
- //
19
- // The core's Package.swift lives at the PACKAGE ROOT (not under `ios/`)
20
- // so that SPM derives identity "dvai-bridge-ios-llama-core" rather than
21
- // "ios" for this dependency. With both Package.swifts at `ios/`, SPM's
22
- // path-dep identity-from-last-dir-name rule aliased them and triggered
23
- // a false `cyclic dependency between packages DVAICapacitorLlama ->
24
- // DVAICapacitorLlama` resolution error. The bare product reference
25
- // ("DVAILlamaCore") in the target dependency list below works because
26
- // SPM auto-resolves unambiguous product names across the dep graph.
27
- .package(path: "../../dvai-bridge-ios-llama-core"),
28
- ],
29
- targets: [
30
- .target(
31
- name: "DVAICapacitorLlama",
32
- dependencies: [
33
- .product(name: "Capacitor", package: "capacitor-swift-pm"),
34
- .product(name: "Cordova", package: "capacitor-swift-pm"),
35
- // The package's identity is derived from the last dir of the
36
- // path dep (`dvai-bridge-ios-llama-core`), not from the
37
- // manifest's `name:` field. Bare-name product references don't
38
- // cross package boundaries reliably; explicit form is required.
39
- .product(name: "DVAILlamaCore", package: "dvai-bridge-ios-llama-core"),
40
- ],
41
- path: "Sources/DVAICapacitorLlama",
42
- exclude: ["PluginProxy.m"]
43
- ),
44
- .testTarget(
45
- name: "DVAICapacitorLlamaTests",
46
- dependencies: [
47
- "DVAICapacitorLlama",
48
- // RealModelSmokeTest reaches the core directly (LlamaCppBridge,
49
- // ModelDownloader, MTMD_MEDIA_MARKER) — declare both core products
50
- // explicitly so Xcode resolves them at link time.
51
- .product(name: "DVAILlamaCore", package: "dvai-bridge-ios-llama-core"),
52
- .product(name: "DVAILlamaCoreObjC", package: "dvai-bridge-ios-llama-core"),
53
- ],
54
- path: "Tests/DVAICapacitorLlamaTests"
55
- ),
56
- ]
57
- )
1
+ // swift-tools-version: 5.9
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "DVAICapacitorLlama",
6
+ // iOS 17 / macOS 14 — must match dvai-bridge-ios-llama-core
7
+ // (`platforms: [.iOS(.v17), .macOS(.v14)]`), which we depend on
8
+ // transitively via `DVAILlamaCore`/`DVAILlamaCoreObjC` below.
9
+ // Earlier .v14 / .v12 declaration was a vestige from before the
10
+ // Hummingbird migration (v3.2.0) bumped the shared-core floor.
11
+ platforms: [.iOS(.v17), .macOS(.v14)],
12
+ products: [
13
+ .library(name: "DVAICapacitorLlama", targets: ["DVAICapacitorLlama"]),
14
+ ],
15
+ dependencies: [
16
+ // Capacitor SPM artifact (existing)
17
+ .package(url: "https://github.com/ionic-team/capacitor-swift-pm", branch: "main"),
18
+ // Core package — relative path during dev, replaced with version-pin
19
+ // or git URL at publish time. The path is relative to *this* Package.swift's
20
+ // location (`packages/dvai-bridge-capacitor-llama/ios/`), so two `..` get
21
+ // us to the `packages/` parent and `dvai-bridge-ios-llama-core/ios` is the
22
+ // sibling package's SPM root.
23
+ //
24
+ // The core's Package.swift lives at the PACKAGE ROOT (not under `ios/`)
25
+ // so that SPM derives identity "dvai-bridge-ios-llama-core" rather than
26
+ // "ios" for this dependency. With both Package.swifts at `ios/`, SPM's
27
+ // path-dep identity-from-last-dir-name rule aliased them and triggered
28
+ // a false `cyclic dependency between packages DVAICapacitorLlama ->
29
+ // DVAICapacitorLlama` resolution error. The bare product reference
30
+ // ("DVAILlamaCore") in the target dependency list below works because
31
+ // SPM auto-resolves unambiguous product names across the dep graph.
32
+ .package(path: "../../dvai-bridge-ios-llama-core"),
33
+ ],
34
+ targets: [
35
+ .target(
36
+ name: "DVAICapacitorLlama",
37
+ dependencies: [
38
+ .product(name: "Capacitor", package: "capacitor-swift-pm"),
39
+ .product(name: "Cordova", package: "capacitor-swift-pm"),
40
+ // The package's identity is derived from the last dir of the
41
+ // path dep (`dvai-bridge-ios-llama-core`), not from the
42
+ // manifest's `name:` field. Bare-name product references don't
43
+ // cross package boundaries reliably; explicit form is required.
44
+ .product(name: "DVAILlamaCore", package: "dvai-bridge-ios-llama-core"),
45
+ ],
46
+ path: "Sources/DVAICapacitorLlama",
47
+ exclude: ["PluginProxy.m"]
48
+ ),
49
+ .testTarget(
50
+ name: "DVAICapacitorLlamaTests",
51
+ dependencies: [
52
+ "DVAICapacitorLlama",
53
+ // RealModelSmokeTest reaches the core directly (LlamaCppBridge,
54
+ // ModelDownloader, MTMD_MEDIA_MARKER) — declare both core products
55
+ // explicitly so Xcode resolves them at link time.
56
+ .product(name: "DVAILlamaCore", package: "dvai-bridge-ios-llama-core"),
57
+ .product(name: "DVAILlamaCoreObjC", package: "dvai-bridge-ios-llama-core"),
58
+ ],
59
+ path: "Tests/DVAICapacitorLlamaTests"
60
+ ),
61
+ ]
62
+ )