@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.
Files changed (61) hide show
  1. package/ElizaosCapacitorBunRuntime.podspec +54 -0
  2. package/LICENSE +21 -0
  3. package/README.md +127 -0
  4. package/dist/esm/definitions.d.ts +136 -0
  5. package/dist/esm/definitions.d.ts.map +1 -0
  6. package/dist/esm/definitions.js +14 -0
  7. package/dist/esm/definitions.js.map +1 -0
  8. package/dist/esm/index.d.ts +9 -0
  9. package/dist/esm/index.d.ts.map +1 -0
  10. package/dist/esm/index.js +11 -0
  11. package/dist/esm/index.js.map +1 -0
  12. package/dist/esm/web.d.ts +19 -0
  13. package/dist/esm/web.d.ts.map +1 -0
  14. package/dist/esm/web.js +44 -0
  15. package/dist/esm/web.js.map +1 -0
  16. package/dist/plugin.cjs.js +63 -0
  17. package/dist/plugin.cjs.js.map +1 -0
  18. package/dist/plugin.js +66 -0
  19. package/dist/plugin.js.map +1 -0
  20. package/ios/Sources/ElizaBunRuntimePlugin/BridgeInstaller.swift +94 -0
  21. package/ios/Sources/ElizaBunRuntimePlugin/ElizaBunRuntime.swift +705 -0
  22. package/ios/Sources/ElizaBunRuntimePlugin/ElizaBunRuntimePlugin.swift +1109 -0
  23. package/ios/Sources/ElizaBunRuntimePlugin/FullBunEngineHost.swift +677 -0
  24. package/ios/Sources/ElizaBunRuntimePlugin/JSContextHelpers.swift +226 -0
  25. package/ios/Sources/ElizaBunRuntimePlugin/SandboxPaths.swift +46 -0
  26. package/ios/Sources/ElizaBunRuntimePlugin/bridge/CryptoBridge.swift +238 -0
  27. package/ios/Sources/ElizaBunRuntimePlugin/bridge/ElizaSqliteVecBridge.m +28 -0
  28. package/ios/Sources/ElizaBunRuntimePlugin/bridge/FSBridge.swift +270 -0
  29. package/ios/Sources/ElizaBunRuntimePlugin/bridge/HTTPBridge.swift +153 -0
  30. package/ios/Sources/ElizaBunRuntimePlugin/bridge/HTTPServerBridge.swift +32 -0
  31. package/ios/Sources/ElizaBunRuntimePlugin/bridge/LlamaBridge.swift +233 -0
  32. package/ios/Sources/ElizaBunRuntimePlugin/bridge/LlamaBridgeImpl.swift +1863 -0
  33. package/ios/Sources/ElizaBunRuntimePlugin/bridge/LogBridge.swift +36 -0
  34. package/ios/Sources/ElizaBunRuntimePlugin/bridge/PathsBridge.swift +41 -0
  35. package/ios/Sources/ElizaBunRuntimePlugin/bridge/ProcessBridge.swift +80 -0
  36. package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteBridge.swift +406 -0
  37. package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteBridgeInstaller.swift +17 -0
  38. package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteVecLoader.swift +66 -0
  39. package/ios/Sources/ElizaBunRuntimePlugin/bridge/UIBridge.swift +72 -0
  40. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlChinesePhonemizer.swift +313 -0
  41. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlConfiguration.swift +28 -0
  42. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlEngine.swift +325 -0
  43. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlHindiPhonemizer.swift +150 -0
  44. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlJapanesePhonemizer.swift +209 -0
  45. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlLatinPhonemizer.swift +374 -0
  46. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlModel.swift +87 -0
  47. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlPhonemizer.swift +679 -0
  48. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlPronunciationDicts.swift +131 -0
  49. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlSupport.swift +24 -0
  50. package/ios/Tests/llama-bridge-smoke-main.swift +92 -0
  51. package/package.json +68 -0
  52. package/src/bridge-contract.test.ts +127 -0
  53. package/src/definitions.d.ts +136 -0
  54. package/src/definitions.d.ts.map +1 -0
  55. package/src/definitions.ts +152 -0
  56. package/src/index.d.ts +9 -0
  57. package/src/index.d.ts.map +1 -0
  58. package/src/index.ts +16 -0
  59. package/src/web.d.ts +19 -0
  60. package/src/web.d.ts.map +1 -0
  61. package/src/web.ts +80 -0
@@ -0,0 +1,72 @@
1
+ import Foundation
2
+ import JavaScriptCore
3
+ import Capacitor
4
+
5
+ /// Implements `ui_post_message` and `ui_register_handler` from
6
+ /// `BRIDGE_CONTRACT.md`.
7
+ ///
8
+ /// `ui_post_message` is a Capacitor event that flows from the agent → WebView.
9
+ /// The Capacitor plugin shell forwards the events via `notifyListeners` so
10
+ /// the React UI can subscribe with `addListener`.
11
+ ///
12
+ /// `ui_register_handler` registers a JS callback under a string name. The
13
+ /// plugin's `call(method, args)` Capacitor method dispatches to the matching
14
+ /// handler and returns the result.
15
+ public final class UIBridge {
16
+ private weak var context: JSContext?
17
+ private weak var plugin: CAPPlugin?
18
+ private var handlers: [String: ManagedCallback] = [:]
19
+ private let lock = NSLock()
20
+
21
+ public init(plugin: CAPPlugin?) {
22
+ self.plugin = plugin
23
+ }
24
+
25
+ public func install(into ctx: JSContext) {
26
+ self.context = ctx
27
+
28
+ ctx.installBridgeFunction(name: "ui_post_message") { args in
29
+ guard args.count >= 1,
30
+ let channel = args[0].toString() else {
31
+ return NSNull()
32
+ }
33
+ let payload: Any? = args.count >= 2 ? args[1].toObject() : nil
34
+ let event: [String: Any] = [
35
+ "channel": channel,
36
+ "payload": payload ?? NSNull(),
37
+ ]
38
+ DispatchQueue.main.async {
39
+ self.plugin?.notifyListeners("eliza:ui", data: event)
40
+ }
41
+ return NSNull()
42
+ }
43
+
44
+ ctx.installBridgeFunction(name: "ui_register_handler") { args in
45
+ guard args.count >= 2,
46
+ let method = args[0].toString() else {
47
+ return NSNull()
48
+ }
49
+ let value = args[1]
50
+ guard let mc = ManagedCallback(value: value) else { return NSNull() }
51
+ self.lock.lock()
52
+ self.handlers[method] = mc
53
+ self.lock.unlock()
54
+ return NSNull()
55
+ }
56
+ }
57
+
58
+ /// Looks up a registered handler. Returns nil when nothing is registered
59
+ /// under `method`. The caller is responsible for invoking it on the
60
+ /// JSContext queue.
61
+ public func handler(for method: String) -> ManagedCallback? {
62
+ lock.lock()
63
+ defer { lock.unlock() }
64
+ return handlers[method]
65
+ }
66
+
67
+ public func clear() {
68
+ lock.lock()
69
+ handlers.removeAll()
70
+ lock.unlock()
71
+ }
72
+ }
@@ -0,0 +1,313 @@
1
+ import Foundation
2
+
3
+ /// Chinese text-to-phoneme conversion for Kokoro TTS.
4
+ ///
5
+ /// Pipeline: Chinese text → CFStringTokenizer (word segmentation + pinyin) → IPA
6
+ /// Uses Apple's built-in Mandarin Latin transcription — no external dependencies.
7
+ ///
8
+ /// Pinyin-to-IPA mapping adapted from stefantaubert/pinyin-to-ipa (MIT license).
9
+ /// Tone marks simplified to arrow notation to match Kokoro's vocab.
10
+ final class ChinesePhonemizer {
11
+
12
+ // MARK: - Pinyin Initial → IPA
13
+
14
+ /// Mandarin initials mapped to IPA. Longest-match order matters (zh before z).
15
+ private static let initials: [(pinyin: String, ipa: String)] = [
16
+ ("zh", "ʈʂ"), ("ch", "ʈʂʰ"), ("sh", "ʂ"),
17
+ ("b", "p"), ("p", "pʰ"), ("m", "m"), ("f", "f"),
18
+ ("d", "t"), ("t", "tʰ"), ("n", "n"), ("l", "l"),
19
+ ("g", "k"), ("k", "kʰ"), ("h", "x"),
20
+ ("j", "tɕ"), ("q", "tɕʰ"), ("x", "ɕ"),
21
+ ("z", "ts"), ("c", "tsʰ"), ("s", "s"),
22
+ ("r", "ɻ"),
23
+ ]
24
+
25
+ // MARK: - Pinyin Final → IPA
26
+
27
+ /// Mandarin finals mapped to IPA (tone marker "0" replaced later).
28
+ /// Ordered longest-first to ensure correct greedy matching.
29
+ ///
30
+ /// Note: combining diacritics (◌̯ non-syllabic, ◌̩ syllabic) are omitted because
31
+ /// Kokoro's vocab_index.json doesn't contain them — they'd be silently dropped
32
+ /// during tokenization, corrupting the phoneme sequence.
33
+ private static let finals: [(pinyin: String, ipa: String)] = [
34
+ ("iang", "ja0ŋ"), ("iong", "jʊ0ŋ"), ("uang", "wa0ŋ"), ("ueng", "wə0ŋ"),
35
+ ("iao", "jau0"), ("ian", "jɛ0n"), ("iou", "jou0"),
36
+ ("uai", "wai0"), ("uan", "wa0n"), ("uei", "wei0"), ("uen", "wə0n"),
37
+ ("üan", "ɥɛ0n"), ("üe", "ɥe0"),
38
+ ("ang", "a0ŋ"), ("eng", "ə0ŋ"), ("ing", "i0ŋ"), ("ong", "ʊ0ŋ"),
39
+ ("ai", "ai0"), ("ei", "ei0"), ("ao", "au0"), ("ou", "ou0"),
40
+ ("an", "a0n"), ("en", "ə0n"), ("in", "i0n"), ("ün", "y0n"),
41
+ ("ia", "ja0"), ("ie", "je0"), ("uo", "wo0"), ("ua", "wa0"),
42
+ ("a", "a0"), ("e", "ɤ0"), ("i", "i0"), ("o", "wo0"), ("u", "u0"), ("ü", "y0"),
43
+ ]
44
+
45
+ /// Context-dependent final for "i" after zh/ch/sh/r.
46
+ /// Uses ɨ (close central unrounded) which is in Kokoro's vocab,
47
+ /// instead of ɻ̩ (combining syllabic mark not in vocab).
48
+ private static let retroflexI = "ɨ0"
49
+ /// Context-dependent final for "i" after z/c/s.
50
+ private static let alveolarI = "ɨ0"
51
+
52
+ // MARK: - Interjections & Syllabic Consonants
53
+
54
+ private static let interjections: [String: String] = [
55
+ "er": "ɚ0", "io": "jɔ0", "ê": "ɛ0",
56
+ ]
57
+
58
+ private static let syllabicConsonants: [String: String] = [
59
+ "hng": "hŋ0", "hm": "hm0", "ng": "ŋ0", "m": "m0", "n": "n0",
60
+ ]
61
+
62
+ // MARK: - Tone Contours
63
+
64
+ private static let toneContours: [Character: String] = [
65
+ "1": "˥", // high level
66
+ "2": "˧˥", // rising
67
+ "3": "˧˩˧", // dipping
68
+ "4": "˥˩", // falling
69
+ "5": "", // neutral
70
+ "0": "", // no tone
71
+ ]
72
+
73
+ /// Simplified tone marks (arrow notation matching Kokoro vocab).
74
+ private static let retoneMap: [(from: String, to: String)] = [
75
+ ("˧˩˧", "↓"), // 3rd tone
76
+ ("˧˥", "↗"), // 2nd tone
77
+ ("˥˩", "↘"), // 4th tone
78
+ ("˥", "→"), // 1st tone
79
+ ]
80
+
81
+ // MARK: - Chinese Punctuation
82
+
83
+ private static let punctuationMap: [Character: String] = [
84
+ ",": ",", "。": ".", "!": "!", "?": "?", ";": ";", ":": ":",
85
+ "、": ",", "—": "-",
86
+ "「": "\"", "」": "\"", "『": "\"", "』": "\"",
87
+ "《": "\"", "》": "\"", "【": "\"", "】": "\"",
88
+ "(": "(", ")": ")",
89
+ ]
90
+
91
+ // MARK: - Public API
92
+
93
+ /// Convert Chinese text to IPA phoneme string.
94
+ func phonemize(_ text: String) -> String {
95
+ var result = ""
96
+ var lastWasWord = false
97
+
98
+ // Process character by character to get individual pinyin syllables.
99
+ // CFStringTokenizer per-word concatenates multi-char pinyin (e.g. "nǐhǎo"),
100
+ // so we tokenize each Chinese character individually for correct syllable boundaries.
101
+ for ch in text {
102
+ if let punct = Self.punctuationMap[ch] {
103
+ result += punct
104
+ lastWasWord = false
105
+ } else if ch.isPunctuation || ch.isSymbol {
106
+ lastWasWord = false
107
+ } else if ch.isWhitespace {
108
+ if lastWasWord { result += " " }
109
+ lastWasWord = false
110
+ } else if ch.isASCII && ch.isLetter {
111
+ // English letter passthrough
112
+ if !lastWasWord { result += " " }
113
+ result += String(ch).lowercased()
114
+ lastWasWord = true
115
+ } else {
116
+ // Chinese character — get pinyin via CFStringTransform
117
+ let mutable = NSMutableString(string: String(ch))
118
+ CFStringTransform(mutable, nil, kCFStringTransformMandarinLatin, false)
119
+ let pinyin = mutable as String
120
+
121
+ // Skip if transform returned the same character (not Chinese)
122
+ if pinyin != String(ch) {
123
+ if lastWasWord { result += " " }
124
+ result += Self.pinyinToIPA(pinyin)
125
+ lastWasWord = true
126
+ }
127
+ }
128
+ }
129
+
130
+ return result
131
+ }
132
+
133
+ // MARK: - Pinyin → IPA Conversion
134
+
135
+ /// Convert a tone-marked pinyin string (e.g. "nǐ hǎo") to IPA.
136
+ static func pinyinToIPA(_ pinyin: String) -> String {
137
+ // Split on whitespace/hyphens to get individual syllables
138
+ let syllables = pinyin.components(separatedBy: CharacterSet.whitespaces)
139
+ .flatMap { $0.components(separatedBy: "-") }
140
+ .filter { !$0.isEmpty }
141
+
142
+ return syllables.map { syllableToIPA($0) }.joined()
143
+ }
144
+
145
+ /// Convert a single pinyin syllable to IPA.
146
+ static func syllableToIPA(_ syllable: String) -> String {
147
+ // Normalize: extract tone from diacritics
148
+ let (base, tone) = extractTone(syllable)
149
+ var normalized = normalizeFinalsNotation(base)
150
+
151
+ // Check interjections
152
+ if let ipa = interjections[normalized] {
153
+ return applyTone(ipa, tone: tone)
154
+ }
155
+
156
+ // Check syllabic consonants
157
+ if let ipa = syllabicConsonants[normalized] {
158
+ return applyTone(ipa, tone: tone)
159
+ }
160
+
161
+ // Handle zero-initial syllables: y→i/ü, w→u mappings
162
+ // "yi" → final "i", "wu" → final "u", "yu" → final "ü"
163
+ // "ya" → final "ia", "ye" → final "ie", "yao" → final "iao", etc.
164
+ // "wa" → final "ua", "wo" → final "uo", "wai" → final "uai", etc.
165
+ if normalized.hasPrefix("y") {
166
+ let afterY = String(normalized.dropFirst())
167
+ if afterY == "i" || afterY.isEmpty {
168
+ normalized = "i"
169
+ } else if afterY == "u" || afterY == "ü" {
170
+ normalized = "ü"
171
+ } else if afterY == "uan" || afterY == "ue" || afterY == "un" {
172
+ // yuan→üan, yue→üe, yun→ün
173
+ normalized = "ü" + afterY.dropFirst()
174
+ } else {
175
+ // ya→ia, ye→ie, yao→iao, you→iou, etc.
176
+ normalized = "i" + afterY
177
+ }
178
+ } else if normalized.hasPrefix("w") {
179
+ let afterW = String(normalized.dropFirst())
180
+ if afterW == "u" || afterW.isEmpty {
181
+ normalized = "u"
182
+ } else {
183
+ // wa→ua, wo→uo, wai→uai, wei→uei, wen→uen, wang→uang
184
+ normalized = "u" + afterW
185
+ }
186
+ }
187
+
188
+ // Split into initial + final
189
+ var initial = ""
190
+ var initialIPA = ""
191
+ var remainder = normalized
192
+
193
+ for (py, ipa) in initials {
194
+ if normalized.hasPrefix(py) {
195
+ initial = py
196
+ initialIPA = ipa
197
+ remainder = String(normalized.dropFirst(py.count))
198
+ break
199
+ }
200
+ }
201
+
202
+ // Handle empty remainder (standalone initial — shouldn't happen for valid pinyin)
203
+ guard !remainder.isEmpty else {
204
+ return initialIPA
205
+ }
206
+
207
+ // Context-dependent "i" after retroflex/alveolar initials
208
+ if remainder == "i" {
209
+ if ["zh", "ch", "sh", "r"].contains(initial) {
210
+ return initialIPA + applyTone(retroflexI, tone: tone)
211
+ }
212
+ if ["z", "c", "s"].contains(initial) {
213
+ return initialIPA + applyTone(alveolarI, tone: tone)
214
+ }
215
+ }
216
+
217
+ // Match final
218
+ for (py, ipa) in finals {
219
+ if remainder == py {
220
+ return initialIPA + applyTone(ipa, tone: tone)
221
+ }
222
+ }
223
+
224
+ // Fallback: return raw
225
+ return initialIPA + remainder
226
+ }
227
+
228
+ /// Replace tone marker "0" with actual tone contour.
229
+ private static func applyTone(_ ipa: String, tone: Character) -> String {
230
+ let contour = toneContours[tone] ?? ""
231
+ // Simplify tone contours to arrow notation
232
+ var toned = ipa.replacingOccurrences(of: "0", with: contour)
233
+ for (from, to) in retoneMap {
234
+ toned = toned.replacingOccurrences(of: from, with: to)
235
+ }
236
+ return toned
237
+ }
238
+
239
+ /// Extract tone number from diacritic-marked pinyin.
240
+ /// Returns (base pinyin without diacritics, tone character '1'-'5').
241
+ static func extractTone(_ syllable: String) -> (String, Character) {
242
+ var base = ""
243
+ var tone: Character = "5" // default neutral
244
+
245
+ for scalar in syllable.unicodeScalars {
246
+ switch scalar.value {
247
+ // Tone 1: macron (ā, ē, ī, ō, ū, ǖ)
248
+ case 0x0101: base += "a"; tone = "1"
249
+ case 0x0113: base += "e"; tone = "1"
250
+ case 0x012B: base += "i"; tone = "1"
251
+ case 0x014D: base += "o"; tone = "1"
252
+ case 0x016B: base += "u"; tone = "1"
253
+ case 0x01D6: base += "ü"; tone = "1"
254
+ // Tone 2: acute (á, é, í, ó, ú, ǘ)
255
+ case 0x00E1: base += "a"; tone = "2"
256
+ case 0x00E9: base += "e"; tone = "2"
257
+ case 0x00ED: base += "i"; tone = "2"
258
+ case 0x00F3: base += "o"; tone = "2"
259
+ case 0x00FA: base += "u"; tone = "2"
260
+ case 0x01D8: base += "ü"; tone = "2"
261
+ // Tone 3: caron (ǎ, ě, ǐ, ǒ, ǔ, ǚ)
262
+ case 0x01CE: base += "a"; tone = "3"
263
+ case 0x011B: base += "e"; tone = "3"
264
+ case 0x01D0: base += "i"; tone = "3"
265
+ case 0x01D2: base += "o"; tone = "3"
266
+ case 0x01D4: base += "u"; tone = "3"
267
+ case 0x01DA: base += "ü"; tone = "3"
268
+ // Tone 4: grave (à, è, ì, ò, ù, ǜ)
269
+ case 0x00E0: base += "a"; tone = "4"
270
+ case 0x00E8: base += "e"; tone = "4"
271
+ case 0x00EC: base += "i"; tone = "4"
272
+ case 0x00F2: base += "o"; tone = "4"
273
+ case 0x00F9: base += "u"; tone = "4"
274
+ case 0x01DC: base += "ü"; tone = "4"
275
+ default:
276
+ base += String(scalar)
277
+ }
278
+ }
279
+
280
+ return (base.lowercased(), tone)
281
+ }
282
+
283
+ /// Normalize pinyin final notation to match our lookup tables.
284
+ /// Handles: iu→iou, ui→uei, un→uen, v/ü normalization.
285
+ static func normalizeFinalsNotation(_ pinyin: String) -> String {
286
+ var s = pinyin.replacingOccurrences(of: "v", with: "ü")
287
+ .replacingOccurrences(of: "yu", with: "ü")
288
+
289
+ // Common abbreviations in standard pinyin
290
+ // iu → iou (e.g., liu → liou)
291
+ if s.hasSuffix("iu") && s.count > 2 {
292
+ s = String(s.dropLast(2)) + "iou"
293
+ }
294
+ // ui → uei (e.g., gui → guei)
295
+ if s.hasSuffix("ui") && s.count > 2 {
296
+ s = String(s.dropLast(2)) + "uei"
297
+ }
298
+ // un → uen (e.g., gun → guen), but not ün
299
+ if s.hasSuffix("un") && !s.hasSuffix("ün") && s.count > 2 {
300
+ s = String(s.dropLast(2)) + "uen"
301
+ }
302
+
303
+ // After j/q/x, u → ü
304
+ if s.count >= 2 {
305
+ let first = String(s.prefix(1))
306
+ if ["j", "q", "x"].contains(first) {
307
+ s = first + String(s.dropFirst()).replacingOccurrences(of: "u", with: "ü")
308
+ }
309
+ }
310
+
311
+ return s
312
+ }
313
+ }
@@ -0,0 +1,28 @@
1
+ import Foundation
2
+
3
+ /// Configuration for Kokoro-82M TTS model.
4
+ public struct KokoroConfig: Codable, Sendable {
5
+ /// Output audio sample rate in Hz.
6
+ public let sampleRate: Int
7
+ /// Maximum phoneme input length (E2E model uses fixed 128).
8
+ public let maxPhonemeLength: Int
9
+ /// Style embedding dimension (ref_s input to CoreML model).
10
+ public let styleDim: Int
11
+ /// Supported languages.
12
+ public let languages: [String]
13
+
14
+ public init(
15
+ sampleRate: Int = 24000,
16
+ maxPhonemeLength: Int = 128,
17
+ styleDim: Int = 256,
18
+ languages: [String] = ["en", "fr", "es", "ja", "zh", "hi", "pt", "it"]
19
+ ) {
20
+ self.sampleRate = sampleRate
21
+ self.maxPhonemeLength = maxPhonemeLength
22
+ self.styleDim = styleDim
23
+ self.languages = languages
24
+ }
25
+
26
+ /// Default configuration matching Kokoro-82M.
27
+ public static let `default` = KokoroConfig()
28
+ }