@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,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
|
+
}
|