@elizaos/capacitor-bun-runtime 2.0.11-beta.7
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/ElizaosCapacitorBunRuntime.podspec +54 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/esm/definitions.d.ts +136 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +14 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +11 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +19 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +44 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +63 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +66 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/ElizaBunRuntimePlugin/BridgeInstaller.swift +94 -0
- package/ios/Sources/ElizaBunRuntimePlugin/ElizaBunRuntime.swift +705 -0
- package/ios/Sources/ElizaBunRuntimePlugin/ElizaBunRuntimePlugin.swift +1109 -0
- package/ios/Sources/ElizaBunRuntimePlugin/FullBunEngineHost.swift +677 -0
- package/ios/Sources/ElizaBunRuntimePlugin/JSContextHelpers.swift +226 -0
- package/ios/Sources/ElizaBunRuntimePlugin/SandboxPaths.swift +46 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/CryptoBridge.swift +238 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/ElizaSqliteVecBridge.m +28 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/FSBridge.swift +270 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/HTTPBridge.swift +153 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/HTTPServerBridge.swift +32 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/LlamaBridge.swift +233 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/LlamaBridgeImpl.swift +1863 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/LogBridge.swift +36 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/PathsBridge.swift +41 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/ProcessBridge.swift +80 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteBridge.swift +406 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteBridgeInstaller.swift +17 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteVecLoader.swift +66 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/UIBridge.swift +72 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlChinesePhonemizer.swift +313 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlConfiguration.swift +28 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlEngine.swift +325 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlHindiPhonemizer.swift +150 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlJapanesePhonemizer.swift +209 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlLatinPhonemizer.swift +374 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlModel.swift +87 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlPhonemizer.swift +679 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlPronunciationDicts.swift +131 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlSupport.swift +24 -0
- package/ios/Tests/llama-bridge-smoke-main.swift +92 -0
- package/package.json +68 -0
- package/src/bridge-contract.test.ts +127 -0
- package/src/definitions.d.ts +136 -0
- package/src/definitions.d.ts.map +1 -0
- package/src/definitions.ts +152 -0
- package/src/index.d.ts +9 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.ts +16 -0
- package/src/web.d.ts +19 -0
- package/src/web.d.ts.map +1 -0
- package/src/web.ts +80 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Darwin
|
|
3
|
+
#if ELIZA_IOS_FULL_BUN_ENGINE
|
|
4
|
+
import ElizaBunEngine
|
|
5
|
+
#endif
|
|
6
|
+
|
|
7
|
+
private let fullBunHostCallCallback: @convention(c) (
|
|
8
|
+
UnsafePointer<CChar>?,
|
|
9
|
+
UnsafePointer<CChar>?,
|
|
10
|
+
Int32
|
|
11
|
+
) -> UnsafeMutablePointer<CChar>? = { methodPtr, payloadPtr, timeoutMs in
|
|
12
|
+
let method = methodPtr.map { String(cString: $0) } ?? ""
|
|
13
|
+
let payloadJson = payloadPtr.map { String(cString: $0) } ?? "null"
|
|
14
|
+
let response = FullBunEngineHost.shared.handleHostCall(
|
|
15
|
+
method: method,
|
|
16
|
+
payloadJson: payloadJson,
|
|
17
|
+
timeoutMs: timeoutMs
|
|
18
|
+
)
|
|
19
|
+
return strdup(response)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Host for the real Bun iOS engine framework.
|
|
23
|
+
///
|
|
24
|
+
/// Full-engine/App Store builds link `ElizaBunEngine.xcframework` directly so
|
|
25
|
+
/// the shipped app does not import dynamic loader APIs. Compatibility builds
|
|
26
|
+
/// keep the optional loader path only in DEBUG development builds without
|
|
27
|
+
/// embedding the full engine framework. Release builds without the direct-link
|
|
28
|
+
/// flag fail closed.
|
|
29
|
+
///
|
|
30
|
+
/// IPC security model (full-Bun path):
|
|
31
|
+
/// - Transport: NDJSON over anonymous stdio pipes. No TCP port is opened by the
|
|
32
|
+
/// native side. `bun_start(...)` receives the read end of the parent's stdin
|
|
33
|
+
/// pipe and the write end of its stdout pipe; no other network socket is
|
|
34
|
+
/// created by the shim.
|
|
35
|
+
/// - Input validation: the C shim (`eliza_bun_engine_shim.c`) validates all
|
|
36
|
+
/// NDJSON frames before dispatch: `id` must be numeric, `method` must be a
|
|
37
|
+
/// JSON string, `payload` is extracted as a bounded value field. The
|
|
38
|
+
/// `ELIZA_MAX_PROTOCOL_LINE_BYTES` (16 MiB) cap prevents unbounded reads.
|
|
39
|
+
/// - Host-call allowlist: only `llama_hardware_info`, `llama_load_model`,
|
|
40
|
+
/// `llama_generate`, `llama_free`, `llama_cancel`, `eliza_tts_synthesize`,
|
|
41
|
+
/// and `eliza_asr_transcribe` are dispatched by
|
|
42
|
+
/// `handleHostCall`. All other method names return `{"ok":false,"error":"..."}`.
|
|
43
|
+
/// - `http_fetch` (JSContext compat path): loopback/local-agent URLs are
|
|
44
|
+
/// rejected at `HTTPBridge.isLocalLoopback` before a URLRequest is created.
|
|
45
|
+
/// External fetches go through URLSession with the standard iOS ATS policy.
|
|
46
|
+
/// - `http_request` IPC (both paths): the path must begin with `/` and must not
|
|
47
|
+
/// contain `://`. Validated in `MobileAgentBridgePlugin.proxyHttpRequest` and
|
|
48
|
+
/// enforced by the Bun bridge contract at the agent layer.
|
|
49
|
+
/// - Filesystem (JSContext compat path): `FSBridge` does not restrict paths
|
|
50
|
+
/// beyond what the iOS app sandbox enforces. The agent bundle is signed and
|
|
51
|
+
/// staged inside the app bundle; paths visible to the JSContext are limited to
|
|
52
|
+
/// the app container by the OS.
|
|
53
|
+
/// - ABI version check: `load()` verifies the framework reports ABI version "3"
|
|
54
|
+
/// before accepting any other symbols. A version mismatch is a hard error.
|
|
55
|
+
final class FullBunEngineHost {
|
|
56
|
+
static let shared = FullBunEngineHost()
|
|
57
|
+
private static let expectedAbiVersion = "3"
|
|
58
|
+
|
|
59
|
+
private typealias AbiVersionFn = @convention(c) () -> UnsafePointer<CChar>?
|
|
60
|
+
private typealias LastErrorFn = @convention(c) () -> UnsafePointer<CChar>?
|
|
61
|
+
private typealias HostCallbackFn = @convention(c) (
|
|
62
|
+
UnsafePointer<CChar>?,
|
|
63
|
+
UnsafePointer<CChar>?,
|
|
64
|
+
Int32
|
|
65
|
+
) -> UnsafeMutablePointer<CChar>?
|
|
66
|
+
private typealias SetHostCallbackFn = @convention(c) (HostCallbackFn?) -> Int32
|
|
67
|
+
private typealias StartFn = @convention(c) (
|
|
68
|
+
UnsafePointer<CChar>,
|
|
69
|
+
UnsafePointer<CChar>,
|
|
70
|
+
UnsafePointer<CChar>,
|
|
71
|
+
UnsafePointer<CChar>
|
|
72
|
+
) -> Int32
|
|
73
|
+
private typealias StopFn = @convention(c) () -> Int32
|
|
74
|
+
private typealias IsRunningFn = @convention(c) () -> Int32
|
|
75
|
+
private typealias CallFn = @convention(c) (
|
|
76
|
+
UnsafePointer<CChar>,
|
|
77
|
+
UnsafePointer<CChar>
|
|
78
|
+
) -> UnsafeMutablePointer<CChar>?
|
|
79
|
+
private typealias FreeFn = @convention(c) (UnsafeMutableRawPointer?) -> Void
|
|
80
|
+
|
|
81
|
+
private var loaded = false
|
|
82
|
+
#if !ELIZA_IOS_FULL_BUN_ENGINE && DEBUG
|
|
83
|
+
private var handle: UnsafeMutableRawPointer?
|
|
84
|
+
#endif
|
|
85
|
+
private var abiVersionFn: AbiVersionFn?
|
|
86
|
+
private var lastErrorFn: LastErrorFn?
|
|
87
|
+
private var setHostCallbackFn: SetHostCallbackFn?
|
|
88
|
+
private var startFn: StartFn?
|
|
89
|
+
private var stopFn: StopFn?
|
|
90
|
+
private var isRunningFn: IsRunningFn?
|
|
91
|
+
private var callFn: CallFn?
|
|
92
|
+
private var freeFn: FreeFn?
|
|
93
|
+
private var running = false
|
|
94
|
+
|
|
95
|
+
private init() {}
|
|
96
|
+
|
|
97
|
+
var isAvailable: Bool {
|
|
98
|
+
do {
|
|
99
|
+
try load()
|
|
100
|
+
return true
|
|
101
|
+
} catch {
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
var abiVersion: String {
|
|
107
|
+
guard let abi = abiVersionFn?() else { return "unknown" }
|
|
108
|
+
return String(cString: abi)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
var isRunning: Bool {
|
|
112
|
+
do {
|
|
113
|
+
try load()
|
|
114
|
+
let engineRunning = isRunningFn?() == 1
|
|
115
|
+
if !engineRunning { running = false }
|
|
116
|
+
return engineRunning
|
|
117
|
+
} catch {
|
|
118
|
+
running = false
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func start(
|
|
124
|
+
bundlePath: String,
|
|
125
|
+
argv: [String],
|
|
126
|
+
env: [String: String],
|
|
127
|
+
appSupportDir: String
|
|
128
|
+
) throws {
|
|
129
|
+
let startedAt = Date()
|
|
130
|
+
NSLog("[FullBunEngineHost] start requested bundle=\(bundlePath) appSupport=\(appSupportDir) argv=\(argv) envKeys=\(env.keys.sorted())")
|
|
131
|
+
try load()
|
|
132
|
+
if running {
|
|
133
|
+
if isRunningFn?() == 1 { return }
|
|
134
|
+
running = false
|
|
135
|
+
}
|
|
136
|
+
guard let startFn else {
|
|
137
|
+
throw makeError("ElizaBunEngine missing start symbol")
|
|
138
|
+
}
|
|
139
|
+
let argvJson = try encodeJSON(argv)
|
|
140
|
+
let envJson = try encodeJSON(env)
|
|
141
|
+
let code = bundlePath.withCString { bundlePtr in
|
|
142
|
+
argvJson.withCString { argvPtr in
|
|
143
|
+
envJson.withCString { envPtr in
|
|
144
|
+
appSupportDir.withCString { supportPtr in
|
|
145
|
+
startFn(bundlePtr, argvPtr, envPtr, supportPtr)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
guard code == 0 else {
|
|
151
|
+
let detail = lastError()
|
|
152
|
+
let durationMs = Int(Date().timeIntervalSince(startedAt) * 1000)
|
|
153
|
+
NSLog("[FullBunEngineHost] start failed code=\(code) durationMs=\(durationMs) detail=\(detail)")
|
|
154
|
+
throw makeError(
|
|
155
|
+
"ElizaBunEngine start failed with code \(code)" +
|
|
156
|
+
(detail.isEmpty ? "" : ": \(detail)")
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
running = true
|
|
160
|
+
let durationMs = Int(Date().timeIntervalSince(startedAt) * 1000)
|
|
161
|
+
NSLog("[FullBunEngineHost] start succeeded abi=\(abiVersion) durationMs=\(durationMs)")
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func stop() {
|
|
165
|
+
_ = stopFn?()
|
|
166
|
+
running = false
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func call(method: String, payload: Any?) throws -> Any? {
|
|
170
|
+
try load()
|
|
171
|
+
guard let callFn else {
|
|
172
|
+
throw makeError("ElizaBunEngine missing call symbol")
|
|
173
|
+
}
|
|
174
|
+
let previousError = lastError()
|
|
175
|
+
let payloadJson = try encodeJSON(payload ?? NSNull())
|
|
176
|
+
let resultPtr = method.withCString { methodPtr in
|
|
177
|
+
payloadJson.withCString { payloadPtr in
|
|
178
|
+
callFn(methodPtr, payloadPtr)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
guard let resultPtr else {
|
|
182
|
+
NSLog("[FullBunEngineHost] call returned null method=\(method) lastError=\(lastError())")
|
|
183
|
+
throw makeError("ElizaBunEngine call returned null for \(method)")
|
|
184
|
+
}
|
|
185
|
+
defer { freeFn?(UnsafeMutableRawPointer(resultPtr)) }
|
|
186
|
+
let resultJson = String(cString: resultPtr)
|
|
187
|
+
guard let data = resultJson.data(using: .utf8) else {
|
|
188
|
+
throw makeError("ElizaBunEngine returned non-UTF8 payload")
|
|
189
|
+
}
|
|
190
|
+
let decoded = try JSONSerialization.jsonObject(with: data)
|
|
191
|
+
if let dict = decoded as? [String: Any],
|
|
192
|
+
let ok = dict["ok"] as? Bool,
|
|
193
|
+
ok == false {
|
|
194
|
+
let message = dict["error"] as? String ?? "unknown full Bun engine error"
|
|
195
|
+
let currentError = lastError()
|
|
196
|
+
let diagnostic = !currentError.isEmpty && currentError != message
|
|
197
|
+
? currentError
|
|
198
|
+
: previousError
|
|
199
|
+
let detail = diagnostic.isEmpty || diagnostic == message
|
|
200
|
+
? ""
|
|
201
|
+
: " (engine error: \(diagnostic))"
|
|
202
|
+
NSLog("[FullBunEngineHost] call failed method=\(method) error=\(message) diagnostic=\(diagnostic)")
|
|
203
|
+
throw makeError("\(message)\(detail)")
|
|
204
|
+
}
|
|
205
|
+
if let dict = decoded as? [String: Any],
|
|
206
|
+
let ok = dict["ok"] as? Bool,
|
|
207
|
+
ok == true {
|
|
208
|
+
return dict["result"] ?? NSNull()
|
|
209
|
+
}
|
|
210
|
+
return decoded
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private func load() throws {
|
|
214
|
+
if loaded { return }
|
|
215
|
+
NSLog("[FullBunEngineHost] loading ElizaBunEngine")
|
|
216
|
+
#if ELIZA_IOS_FULL_BUN_ENGINE
|
|
217
|
+
let loadedAbiVersionFn: AbiVersionFn = {
|
|
218
|
+
eliza_bun_engine_abi_version()
|
|
219
|
+
}
|
|
220
|
+
let loadedLastErrorFn: LastErrorFn = {
|
|
221
|
+
eliza_bun_engine_last_error()
|
|
222
|
+
}
|
|
223
|
+
let loadedSetHostCallbackFn: SetHostCallbackFn = { callback in
|
|
224
|
+
eliza_bun_engine_set_host_callback(callback)
|
|
225
|
+
}
|
|
226
|
+
let loadedStartFn: StartFn = { bundlePath, argvJson, envJson, appSupportDir in
|
|
227
|
+
eliza_bun_engine_start(bundlePath, argvJson, envJson, appSupportDir)
|
|
228
|
+
}
|
|
229
|
+
let loadedStopFn: StopFn = {
|
|
230
|
+
eliza_bun_engine_stop()
|
|
231
|
+
}
|
|
232
|
+
let loadedIsRunningFn: IsRunningFn = {
|
|
233
|
+
eliza_bun_engine_is_running()
|
|
234
|
+
}
|
|
235
|
+
let loadedCallFn: CallFn = { method, payload in
|
|
236
|
+
eliza_bun_engine_call(method, payload)
|
|
237
|
+
}
|
|
238
|
+
let loadedFreeFn: FreeFn = { ptr in
|
|
239
|
+
eliza_bun_engine_free(ptr)
|
|
240
|
+
}
|
|
241
|
+
try installLoadedEngineSymbols(
|
|
242
|
+
abiVersionFn: loadedAbiVersionFn,
|
|
243
|
+
lastErrorFn: loadedLastErrorFn,
|
|
244
|
+
setHostCallbackFn: loadedSetHostCallbackFn,
|
|
245
|
+
startFn: loadedStartFn,
|
|
246
|
+
stopFn: loadedStopFn,
|
|
247
|
+
isRunningFn: loadedIsRunningFn,
|
|
248
|
+
callFn: loadedCallFn,
|
|
249
|
+
freeFn: loadedFreeFn
|
|
250
|
+
)
|
|
251
|
+
#elseif DEBUG
|
|
252
|
+
let binaryPath = try locateFrameworkBinary()
|
|
253
|
+
guard let openedHandle = dlopen(binaryPath, RTLD_NOW | RTLD_LOCAL) else {
|
|
254
|
+
throw makeError(String(cString: dlerror()))
|
|
255
|
+
}
|
|
256
|
+
do {
|
|
257
|
+
let loadedAbiVersionFn: AbiVersionFn = try symbol(
|
|
258
|
+
"eliza_bun_engine_abi_version",
|
|
259
|
+
in: openedHandle
|
|
260
|
+
)
|
|
261
|
+
let loadedLastErrorFn: LastErrorFn = try symbol(
|
|
262
|
+
"eliza_bun_engine_last_error",
|
|
263
|
+
in: openedHandle
|
|
264
|
+
)
|
|
265
|
+
let loadedSetHostCallbackFn: SetHostCallbackFn = try symbol(
|
|
266
|
+
"eliza_bun_engine_set_host_callback",
|
|
267
|
+
in: openedHandle
|
|
268
|
+
)
|
|
269
|
+
let loadedStartFn: StartFn = try symbol("eliza_bun_engine_start", in: openedHandle)
|
|
270
|
+
let loadedStopFn: StopFn = try symbol("eliza_bun_engine_stop", in: openedHandle)
|
|
271
|
+
let loadedIsRunningFn: IsRunningFn = try symbol(
|
|
272
|
+
"eliza_bun_engine_is_running",
|
|
273
|
+
in: openedHandle
|
|
274
|
+
)
|
|
275
|
+
let loadedCallFn: CallFn = try symbol("eliza_bun_engine_call", in: openedHandle)
|
|
276
|
+
let loadedFreeFn: FreeFn = try symbol("eliza_bun_engine_free", in: openedHandle)
|
|
277
|
+
try installLoadedEngineSymbols(
|
|
278
|
+
abiVersionFn: loadedAbiVersionFn,
|
|
279
|
+
lastErrorFn: loadedLastErrorFn,
|
|
280
|
+
setHostCallbackFn: loadedSetHostCallbackFn,
|
|
281
|
+
startFn: loadedStartFn,
|
|
282
|
+
stopFn: loadedStopFn,
|
|
283
|
+
isRunningFn: loadedIsRunningFn,
|
|
284
|
+
callFn: loadedCallFn,
|
|
285
|
+
freeFn: loadedFreeFn
|
|
286
|
+
)
|
|
287
|
+
self.handle = openedHandle
|
|
288
|
+
} catch {
|
|
289
|
+
_ = dlclose(openedHandle)
|
|
290
|
+
throw error
|
|
291
|
+
}
|
|
292
|
+
#else
|
|
293
|
+
throw makeError(
|
|
294
|
+
"ElizaBunEngine direct-link symbols are not compiled into this release build"
|
|
295
|
+
)
|
|
296
|
+
#endif
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private func installLoadedEngineSymbols(
|
|
300
|
+
abiVersionFn loadedAbiVersionFn: AbiVersionFn,
|
|
301
|
+
lastErrorFn loadedLastErrorFn: LastErrorFn,
|
|
302
|
+
setHostCallbackFn loadedSetHostCallbackFn: SetHostCallbackFn,
|
|
303
|
+
startFn loadedStartFn: StartFn,
|
|
304
|
+
stopFn loadedStopFn: StopFn,
|
|
305
|
+
isRunningFn loadedIsRunningFn: IsRunningFn,
|
|
306
|
+
callFn loadedCallFn: CallFn,
|
|
307
|
+
freeFn loadedFreeFn: FreeFn
|
|
308
|
+
) throws {
|
|
309
|
+
guard let abiPointer = loadedAbiVersionFn() else {
|
|
310
|
+
throw makeError("ElizaBunEngine ABI version returned null")
|
|
311
|
+
}
|
|
312
|
+
let loadedAbiVersion = String(cString: abiPointer)
|
|
313
|
+
guard loadedAbiVersion == Self.expectedAbiVersion else {
|
|
314
|
+
throw makeError(
|
|
315
|
+
"ElizaBunEngine ABI mismatch: expected \(Self.expectedAbiVersion), got \(loadedAbiVersion)"
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
let callbackCode = loadedSetHostCallbackFn(fullBunHostCallCallback)
|
|
319
|
+
guard callbackCode == 0 else {
|
|
320
|
+
throw makeError("ElizaBunEngine failed to install host callback: \(callbackCode)")
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
self.abiVersionFn = loadedAbiVersionFn
|
|
324
|
+
self.lastErrorFn = loadedLastErrorFn
|
|
325
|
+
self.setHostCallbackFn = loadedSetHostCallbackFn
|
|
326
|
+
self.startFn = loadedStartFn
|
|
327
|
+
self.stopFn = loadedStopFn
|
|
328
|
+
self.isRunningFn = loadedIsRunningFn
|
|
329
|
+
self.callFn = loadedCallFn
|
|
330
|
+
self.freeFn = loadedFreeFn
|
|
331
|
+
self.loaded = true
|
|
332
|
+
NSLog("[FullBunEngineHost] loaded ElizaBunEngine abi=\(loadedAbiVersion)")
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
#if !ELIZA_IOS_FULL_BUN_ENGINE && DEBUG
|
|
336
|
+
private func locateFrameworkBinary() throws -> String {
|
|
337
|
+
let relative = "ElizaBunEngine.framework/ElizaBunEngine"
|
|
338
|
+
let candidates = [
|
|
339
|
+
Bundle.main.privateFrameworksURL?.appendingPathComponent(relative).path,
|
|
340
|
+
Bundle.main.bundleURL.appendingPathComponent("Frameworks").appendingPathComponent(relative).path,
|
|
341
|
+
Bundle.main.url(
|
|
342
|
+
forResource: "ElizaBunEngine",
|
|
343
|
+
withExtension: nil,
|
|
344
|
+
subdirectory: "Frameworks/ElizaBunEngine.framework"
|
|
345
|
+
)?.path,
|
|
346
|
+
].compactMap { $0 }
|
|
347
|
+
for candidate in candidates where FileManager.default.fileExists(atPath: candidate) {
|
|
348
|
+
return candidate
|
|
349
|
+
}
|
|
350
|
+
throw makeError("ElizaBunEngine.framework is not embedded in the app bundle")
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private func symbol<T>(_ name: String, in handle: UnsafeMutableRawPointer) throws -> T {
|
|
354
|
+
guard let pointer = dlsym(handle, name) else {
|
|
355
|
+
throw makeError("ElizaBunEngine missing symbol \(name)")
|
|
356
|
+
}
|
|
357
|
+
return unsafeBitCast(pointer, to: T.self)
|
|
358
|
+
}
|
|
359
|
+
#endif
|
|
360
|
+
|
|
361
|
+
private func encodeJSON(_ value: Any) throws -> String {
|
|
362
|
+
let data = try JSONSerialization.data(withJSONObject: value)
|
|
363
|
+
return String(data: data, encoding: .utf8) ?? "{}"
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private func lastError() -> String {
|
|
367
|
+
guard let pointer = lastErrorFn?() else { return "" }
|
|
368
|
+
return String(cString: pointer)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private func makeError(_ message: String) -> NSError {
|
|
372
|
+
NSError(
|
|
373
|
+
domain: "ElizaBunEngine",
|
|
374
|
+
code: -1,
|
|
375
|
+
userInfo: [NSLocalizedDescriptionKey: message]
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
fileprivate func handleHostCall(
|
|
380
|
+
method: String,
|
|
381
|
+
payloadJson: String,
|
|
382
|
+
timeoutMs: Int32
|
|
383
|
+
) -> String {
|
|
384
|
+
_ = timeoutMs
|
|
385
|
+
NSLog("[FullBunEngineHost] host call method=\(method) payloadBytes=\(payloadJson.lengthOfBytes(using: .utf8)) timeoutMs=\(timeoutMs)")
|
|
386
|
+
do {
|
|
387
|
+
let payload = try decodeHostPayload(payloadJson)
|
|
388
|
+
switch method {
|
|
389
|
+
case "llama_hardware_info":
|
|
390
|
+
return encodeHostEnvelope(ok: true, result: LlamaBridgeImpl.shared.hardwareInfo().asDict())
|
|
391
|
+
case "llama_load_model":
|
|
392
|
+
return try handleLoadModel(payload)
|
|
393
|
+
case "llama_generate":
|
|
394
|
+
return try handleGenerate(payload)
|
|
395
|
+
case "llama_free":
|
|
396
|
+
return handleFree(payload)
|
|
397
|
+
case "llama_cancel":
|
|
398
|
+
return handleCancel(payload)
|
|
399
|
+
case "eliza_tts_synthesize":
|
|
400
|
+
return handleTtsSynthesize(payload)
|
|
401
|
+
case "eliza_asr_transcribe":
|
|
402
|
+
return handleAsrTranscribe(payload)
|
|
403
|
+
default:
|
|
404
|
+
return encodeHostEnvelope(
|
|
405
|
+
ok: false,
|
|
406
|
+
error: "Unknown native host call method: \(method)"
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
return encodeHostEnvelope(ok: false, error: error.localizedDescription)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private func handleLoadModel(_ payload: [String: Any]) throws -> String {
|
|
415
|
+
guard let path = stringValue(payload, "path") ?? stringValue(payload, "modelPath"),
|
|
416
|
+
!path.isEmpty else {
|
|
417
|
+
return encodeHostEnvelope(ok: false, error: "llama_load_model requires path")
|
|
418
|
+
}
|
|
419
|
+
let contextSize = uint32Value(payload, "context_size")
|
|
420
|
+
?? uint32Value(payload, "contextSize")
|
|
421
|
+
?? 4096
|
|
422
|
+
let useGPU = boolValue(payload, "use_gpu")
|
|
423
|
+
?? boolValue(payload, "useGpu")
|
|
424
|
+
?? true
|
|
425
|
+
let threads = int32Value(payload, "threads")
|
|
426
|
+
?? int32Value(payload, "maxThreads")
|
|
427
|
+
let result = LlamaBridgeImpl.shared.loadModel(
|
|
428
|
+
path: path,
|
|
429
|
+
contextSize: contextSize,
|
|
430
|
+
useGPU: useGPU,
|
|
431
|
+
threads: threads
|
|
432
|
+
)
|
|
433
|
+
if let error = result.error {
|
|
434
|
+
return encodeHostEnvelope(ok: false, error: error)
|
|
435
|
+
}
|
|
436
|
+
guard let contextId = result.contextId else {
|
|
437
|
+
return encodeHostEnvelope(ok: false, error: "llama_load_model returned no context_id")
|
|
438
|
+
}
|
|
439
|
+
return encodeHostEnvelope(ok: true, result: [
|
|
440
|
+
"context_id": NSNumber(value: contextId),
|
|
441
|
+
"contextId": NSNumber(value: contextId),
|
|
442
|
+
"modelPath": path,
|
|
443
|
+
"contextSize": NSNumber(value: contextSize),
|
|
444
|
+
"useGpu": NSNumber(value: useGPU),
|
|
445
|
+
])
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private func handleGenerate(_ payload: [String: Any]) throws -> String {
|
|
449
|
+
guard let contextId = int64Value(payload, "context_id")
|
|
450
|
+
?? int64Value(payload, "contextId") else {
|
|
451
|
+
return encodeHostEnvelope(ok: false, error: "llama_generate requires context_id")
|
|
452
|
+
}
|
|
453
|
+
guard let prompt = stringValue(payload, "prompt"), !prompt.isEmpty else {
|
|
454
|
+
return encodeHostEnvelope(ok: false, error: "llama_generate requires prompt")
|
|
455
|
+
}
|
|
456
|
+
let maxTokens = int32Value(payload, "max_tokens")
|
|
457
|
+
?? int32Value(payload, "maxTokens")
|
|
458
|
+
?? 256
|
|
459
|
+
let temperature = floatValue(payload, "temperature") ?? 0.7
|
|
460
|
+
let topP = floatValue(payload, "top_p") ?? floatValue(payload, "topP") ?? 0.95
|
|
461
|
+
let topK = int32Value(payload, "top_k") ?? int32Value(payload, "topK") ?? 40
|
|
462
|
+
let stopSequences = stringArrayValue(payload, "stop")
|
|
463
|
+
?? stringArrayValue(payload, "stopSequences")
|
|
464
|
+
?? []
|
|
465
|
+
|
|
466
|
+
let generate = {
|
|
467
|
+
LlamaBridgeImpl.shared.generate(
|
|
468
|
+
contextId: contextId,
|
|
469
|
+
prompt: prompt,
|
|
470
|
+
maxTokens: maxTokens,
|
|
471
|
+
temperature: temperature,
|
|
472
|
+
topP: topP,
|
|
473
|
+
topK: topK,
|
|
474
|
+
stopSequences: stopSequences
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
let result: LlamaGenerateResult
|
|
478
|
+
if let queue = LlamaBridgeImpl.shared.workQueue(for: contextId) {
|
|
479
|
+
result = queue.sync(execute: generate)
|
|
480
|
+
} else {
|
|
481
|
+
result = generate()
|
|
482
|
+
}
|
|
483
|
+
if let error = result.error {
|
|
484
|
+
return encodeHostEnvelope(ok: false, error: error)
|
|
485
|
+
}
|
|
486
|
+
return encodeHostEnvelope(ok: true, result: [
|
|
487
|
+
"text": result.text,
|
|
488
|
+
"promptTokens": NSNumber(value: result.promptTokens),
|
|
489
|
+
"outputTokens": NSNumber(value: result.outputTokens),
|
|
490
|
+
"durationMs": NSNumber(value: result.durationMs),
|
|
491
|
+
])
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private func handleFree(_ payload: [String: Any]) -> String {
|
|
495
|
+
guard let contextId = int64Value(payload, "context_id")
|
|
496
|
+
?? int64Value(payload, "contextId") else {
|
|
497
|
+
return encodeHostEnvelope(ok: true, result: ["freed": false])
|
|
498
|
+
}
|
|
499
|
+
LlamaBridgeImpl.shared.free(contextId: contextId)
|
|
500
|
+
return encodeHostEnvelope(ok: true, result: [
|
|
501
|
+
"freed": true,
|
|
502
|
+
"context_id": NSNumber(value: contextId),
|
|
503
|
+
])
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private func handleCancel(_ payload: [String: Any]) -> String {
|
|
507
|
+
guard let contextId = int64Value(payload, "context_id")
|
|
508
|
+
?? int64Value(payload, "contextId") else {
|
|
509
|
+
return encodeHostEnvelope(ok: true, result: ["cancelled": false])
|
|
510
|
+
}
|
|
511
|
+
LlamaBridgeImpl.shared.cancel(contextId: contextId)
|
|
512
|
+
return encodeHostEnvelope(ok: true, result: [
|
|
513
|
+
"cancelled": true,
|
|
514
|
+
"context_id": NSNumber(value: contextId),
|
|
515
|
+
])
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private func handleTtsSynthesize(_ payload: [String: Any]) -> String {
|
|
519
|
+
guard let bundleDir = stringValue(payload, "bundleDir")
|
|
520
|
+
?? stringValue(payload, "bundle_dir"),
|
|
521
|
+
!bundleDir.isEmpty else {
|
|
522
|
+
return encodeHostEnvelope(ok: false, error: "eliza_tts_synthesize requires bundleDir")
|
|
523
|
+
}
|
|
524
|
+
guard let text = stringValue(payload, "text"), !text.isEmpty else {
|
|
525
|
+
return encodeHostEnvelope(ok: false, error: "eliza_tts_synthesize requires text")
|
|
526
|
+
}
|
|
527
|
+
let speakerPresetId = stringValue(payload, "speakerPresetId")
|
|
528
|
+
?? stringValue(payload, "speaker_preset_id")
|
|
529
|
+
?? stringValue(payload, "voice")
|
|
530
|
+
?? stringValue(payload, "voiceId")
|
|
531
|
+
let maxSamples = intValue(payload, "maxSamples")
|
|
532
|
+
?? intValue(payload, "max_samples")
|
|
533
|
+
?? 24_000 * 60
|
|
534
|
+
let result = LlamaBridgeImpl.shared.synthesizeSpeech(
|
|
535
|
+
bundleDir: bundleDir,
|
|
536
|
+
text: text,
|
|
537
|
+
speakerPresetId: speakerPresetId,
|
|
538
|
+
maxSamples: maxSamples
|
|
539
|
+
)
|
|
540
|
+
if let error = result.error {
|
|
541
|
+
return encodeHostEnvelope(ok: false, error: error)
|
|
542
|
+
}
|
|
543
|
+
var payload: [String: Any] = [
|
|
544
|
+
"contentType": result.contentType,
|
|
545
|
+
"sampleRate": NSNumber(value: result.sampleRate),
|
|
546
|
+
"samples": NSNumber(value: result.samples),
|
|
547
|
+
"durationMs": NSNumber(value: result.durationMs),
|
|
548
|
+
]
|
|
549
|
+
if let audioFilePath = result.audioFilePath, !audioFilePath.isEmpty {
|
|
550
|
+
payload["audioFilePath"] = audioFilePath
|
|
551
|
+
} else {
|
|
552
|
+
payload["audioBase64"] = result.audioBase64
|
|
553
|
+
}
|
|
554
|
+
return encodeHostEnvelope(ok: true, result: payload)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/// Wire format: `pcm` is mono fp32 audio in [-1, 1] carried as a JSON number
|
|
558
|
+
/// array (no base64). `bridge.ts` encodes the same way. `sampleRate` is the
|
|
559
|
+
/// source rate in Hz; the inference slice resamples internally as needed.
|
|
560
|
+
private func handleAsrTranscribe(_ payload: [String: Any]) -> String {
|
|
561
|
+
guard let bundleDir = stringValue(payload, "bundleDir")
|
|
562
|
+
?? stringValue(payload, "bundle_dir"),
|
|
563
|
+
!bundleDir.isEmpty else {
|
|
564
|
+
return encodeHostEnvelope(ok: false, error: "eliza_asr_transcribe requires bundleDir")
|
|
565
|
+
}
|
|
566
|
+
guard let pcm = floatArrayValue(payload, "pcm"), !pcm.isEmpty else {
|
|
567
|
+
return encodeHostEnvelope(ok: false, error: "eliza_asr_transcribe requires pcm")
|
|
568
|
+
}
|
|
569
|
+
let sampleRate = intValue(payload, "sampleRate")
|
|
570
|
+
?? intValue(payload, "sample_rate")
|
|
571
|
+
?? 16_000
|
|
572
|
+
let result = LlamaBridgeImpl.shared.transcribeSpeech(
|
|
573
|
+
bundleDir: bundleDir,
|
|
574
|
+
pcm: pcm,
|
|
575
|
+
sampleRate: sampleRate
|
|
576
|
+
)
|
|
577
|
+
if let error = result.error {
|
|
578
|
+
return encodeHostEnvelope(ok: false, error: error)
|
|
579
|
+
}
|
|
580
|
+
return encodeHostEnvelope(ok: true, result: [
|
|
581
|
+
"text": result.text,
|
|
582
|
+
"durationMs": NSNumber(value: result.durationMs),
|
|
583
|
+
])
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private func decodeHostPayload(_ json: String) throws -> [String: Any] {
|
|
587
|
+
guard let data = json.data(using: .utf8) else { return [:] }
|
|
588
|
+
let value = try JSONSerialization.jsonObject(with: data)
|
|
589
|
+
return value as? [String: Any] ?? [:]
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private func encodeHostEnvelope(
|
|
593
|
+
ok: Bool,
|
|
594
|
+
result: Any? = nil,
|
|
595
|
+
error: String? = nil
|
|
596
|
+
) -> String {
|
|
597
|
+
var object: [String: Any] = ["ok": ok]
|
|
598
|
+
if let result {
|
|
599
|
+
object["result"] = result
|
|
600
|
+
} else if ok {
|
|
601
|
+
object["result"] = NSNull()
|
|
602
|
+
}
|
|
603
|
+
if let error {
|
|
604
|
+
object["error"] = error
|
|
605
|
+
}
|
|
606
|
+
guard JSONSerialization.isValidJSONObject(object),
|
|
607
|
+
let data = try? JSONSerialization.data(withJSONObject: object),
|
|
608
|
+
let json = String(data: data, encoding: .utf8) else {
|
|
609
|
+
let fallback = (error ?? "Failed to encode native host response")
|
|
610
|
+
.replacingOccurrences(of: "\"", with: "\\\"")
|
|
611
|
+
return "{\"ok\":false,\"error\":\"\(fallback)\"}"
|
|
612
|
+
}
|
|
613
|
+
return json
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private func stringValue(_ payload: [String: Any], _ key: String) -> String? {
|
|
617
|
+
payload[key] as? String
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private func boolValue(_ payload: [String: Any], _ key: String) -> Bool? {
|
|
621
|
+
if let value = payload[key] as? Bool { return value }
|
|
622
|
+
if let value = payload[key] as? NSNumber { return value.boolValue }
|
|
623
|
+
if let value = payload[key] as? String {
|
|
624
|
+
if value == "true" || value == "1" { return true }
|
|
625
|
+
if value == "false" || value == "0" { return false }
|
|
626
|
+
}
|
|
627
|
+
return nil
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
private func int32Value(_ payload: [String: Any], _ key: String) -> Int32? {
|
|
631
|
+
if let value = payload[key] as? NSNumber { return value.int32Value }
|
|
632
|
+
if let value = payload[key] as? String, let parsed = Int32(value) { return parsed }
|
|
633
|
+
return nil
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private func intValue(_ payload: [String: Any], _ key: String) -> Int? {
|
|
637
|
+
if let value = payload[key] as? NSNumber { return value.intValue }
|
|
638
|
+
if let value = payload[key] as? String, let parsed = Int(value) { return parsed }
|
|
639
|
+
return nil
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
private func int64Value(_ payload: [String: Any], _ key: String) -> Int64? {
|
|
643
|
+
if let value = payload[key] as? NSNumber { return value.int64Value }
|
|
644
|
+
if let value = payload[key] as? String, let parsed = Int64(value) { return parsed }
|
|
645
|
+
return nil
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private func uint32Value(_ payload: [String: Any], _ key: String) -> UInt32? {
|
|
649
|
+
if let value = payload[key] as? NSNumber { return value.uint32Value }
|
|
650
|
+
if let value = payload[key] as? String, let parsed = UInt32(value) { return parsed }
|
|
651
|
+
return nil
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private func floatValue(_ payload: [String: Any], _ key: String) -> Float? {
|
|
655
|
+
if let value = payload[key] as? NSNumber { return value.floatValue }
|
|
656
|
+
if let value = payload[key] as? String, let parsed = Float(value) { return parsed }
|
|
657
|
+
return nil
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private func stringArrayValue(_ payload: [String: Any], _ key: String) -> [String]? {
|
|
661
|
+
if let values = payload[key] as? [String] { return values }
|
|
662
|
+
if let values = payload[key] as? [Any] {
|
|
663
|
+
return values.compactMap { $0 as? String }
|
|
664
|
+
}
|
|
665
|
+
return nil
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
private func floatArrayValue(_ payload: [String: Any], _ key: String) -> [Float]? {
|
|
669
|
+
if let values = payload[key] as? [NSNumber] {
|
|
670
|
+
return values.map { $0.floatValue }
|
|
671
|
+
}
|
|
672
|
+
if let values = payload[key] as? [Any] {
|
|
673
|
+
return values.compactMap { ($0 as? NSNumber)?.floatValue }
|
|
674
|
+
}
|
|
675
|
+
return nil
|
|
676
|
+
}
|
|
677
|
+
}
|