@glassly/bluetooth-sdk 0.3.1-dev.496 → 0.3.1-dev.497
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/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +3 -3
- package/build/BluetoothSdk.types.d.ts +10 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +5 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +18 -0
- package/ios/GlasslyBluetoothSDK.podspec +2 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +3 -3
- package/ios/Source/translation/OfflineTranslator.swift +224 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +10 -0
- package/src/_private/BluetoothSdkModule.ts +8 -0
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/index.ts +4 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
#if canImport(Translation) && canImport(UIKit)
|
|
4
|
+
import SwiftUI
|
|
5
|
+
import Translation
|
|
6
|
+
import UIKit
|
|
7
|
+
#endif
|
|
8
|
+
|
|
9
|
+
enum OfflineTranslationError: LocalizedError {
|
|
10
|
+
case unavailable
|
|
11
|
+
case notInstalled(String)
|
|
12
|
+
case noPresenter
|
|
13
|
+
case failed(String)
|
|
14
|
+
|
|
15
|
+
/// Leading token is the stable code the JS side matches on.
|
|
16
|
+
var errorDescription: String? {
|
|
17
|
+
switch self {
|
|
18
|
+
case .unavailable: "unavailable: on-device translation requires iOS 26"
|
|
19
|
+
case let .notInstalled(pair): "not_installed: \(pair) is not downloaded"
|
|
20
|
+
case .noPresenter: "no_presenter: no visible window to present the download prompt"
|
|
21
|
+
case let .failed(message): "failed: \(message)"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// On-device text translation backed by Apple's Translation framework.
|
|
27
|
+
/// Sessions are created without UI (`TranslationSession(installedSource:target:)`,
|
|
28
|
+
/// iOS 26), so a pair must be installed before it can translate; installing
|
|
29
|
+
/// goes through the system download prompt in `download`.
|
|
30
|
+
enum OfflineTranslator {
|
|
31
|
+
static var isSupported: Bool {
|
|
32
|
+
#if canImport(Translation) && canImport(UIKit)
|
|
33
|
+
if #available(iOS 26.0, *) {
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
#endif
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Every language the system can translate `source` into, with its install state.
|
|
41
|
+
static func languages(source: String) async -> [[String: String]] {
|
|
42
|
+
#if canImport(Translation) && canImport(UIKit)
|
|
43
|
+
if #available(iOS 26.0, *) {
|
|
44
|
+
let availability = makeAvailability()
|
|
45
|
+
let from = Locale.Language(identifier: source)
|
|
46
|
+
var rows: [[String: String]] = []
|
|
47
|
+
for language in await availability.supportedLanguages {
|
|
48
|
+
let code = language.minimalIdentifier
|
|
49
|
+
let status = await availability.status(from: from, to: language)
|
|
50
|
+
if status == .unsupported {
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
rows.append([
|
|
54
|
+
"code": code,
|
|
55
|
+
"name": Locale.current.localizedString(forIdentifier: code) ?? code,
|
|
56
|
+
"status": statusName(status),
|
|
57
|
+
])
|
|
58
|
+
}
|
|
59
|
+
return rows.sorted { ($0["name"] ?? "") < ($1["name"] ?? "") }
|
|
60
|
+
}
|
|
61
|
+
#endif
|
|
62
|
+
return []
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static func status(source: String, target: String) async -> String {
|
|
66
|
+
#if canImport(Translation) && canImport(UIKit)
|
|
67
|
+
if #available(iOS 26.0, *) {
|
|
68
|
+
let status = await makeAvailability().status(
|
|
69
|
+
from: Locale.Language(identifier: source),
|
|
70
|
+
to: Locale.Language(identifier: target)
|
|
71
|
+
)
|
|
72
|
+
return statusName(status)
|
|
73
|
+
}
|
|
74
|
+
#endif
|
|
75
|
+
return "unavailable"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Shows the system prompt that downloads the pair, then reports its status.
|
|
79
|
+
static func download(source: String, target: String) async throws -> String {
|
|
80
|
+
#if canImport(Translation) && canImport(UIKit)
|
|
81
|
+
if #available(iOS 26.0, *) {
|
|
82
|
+
try await presentDownload(
|
|
83
|
+
source: Locale.Language(identifier: source),
|
|
84
|
+
target: Locale.Language(identifier: target)
|
|
85
|
+
)
|
|
86
|
+
await SessionStore.shared.drop(source: source, target: target)
|
|
87
|
+
return await status(source: source, target: target)
|
|
88
|
+
}
|
|
89
|
+
#endif
|
|
90
|
+
throw OfflineTranslationError.unavailable
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static func translate(text: String, source: String, target: String) async throws -> String {
|
|
94
|
+
#if canImport(Translation) && canImport(UIKit)
|
|
95
|
+
if #available(iOS 26.0, *) {
|
|
96
|
+
return try await SessionStore.shared.translate(text: text, source: source, target: target)
|
|
97
|
+
}
|
|
98
|
+
#endif
|
|
99
|
+
throw OfflineTranslationError.unavailable
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#if canImport(Translation) && canImport(UIKit)
|
|
104
|
+
@available(iOS 26.0, *)
|
|
105
|
+
private extension OfflineTranslator {
|
|
106
|
+
static func makeAvailability() -> LanguageAvailability {
|
|
107
|
+
// Low latency keeps the downloadable language-pack flow, which is
|
|
108
|
+
// what works with no network.
|
|
109
|
+
if #available(iOS 26.4, *) {
|
|
110
|
+
return LanguageAvailability(preferredStrategy: .lowLatency)
|
|
111
|
+
}
|
|
112
|
+
return LanguageAvailability()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
static func statusName(_ status: LanguageAvailability.Status) -> String {
|
|
116
|
+
switch status {
|
|
117
|
+
case .installed: return "installed"
|
|
118
|
+
case .supported: return "supported"
|
|
119
|
+
case .unsupported: return "unsupported"
|
|
120
|
+
@unknown default: return "unsupported"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@MainActor
|
|
125
|
+
static func presentDownload(source: Locale.Language, target: Locale.Language) async throws {
|
|
126
|
+
guard let presenter = topViewController() else { throw OfflineTranslationError.noPresenter }
|
|
127
|
+
let configuration: TranslationSession.Configuration = if #available(iOS 26.4, *) {
|
|
128
|
+
.init(source: source, target: target, preferredStrategy: .lowLatency)
|
|
129
|
+
} else {
|
|
130
|
+
.init(source: source, target: target)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
|
134
|
+
var host: UIViewController?
|
|
135
|
+
var finished = false
|
|
136
|
+
let view = DownloadHost(configuration: configuration) { result in
|
|
137
|
+
if finished {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
finished = true
|
|
141
|
+
host?.willMove(toParent: nil)
|
|
142
|
+
host?.view.removeFromSuperview()
|
|
143
|
+
host?.removeFromParent()
|
|
144
|
+
continuation.resume(with: result)
|
|
145
|
+
}
|
|
146
|
+
let controller = UIHostingController(rootView: view)
|
|
147
|
+
controller.view.backgroundColor = .clear
|
|
148
|
+
controller.view.isUserInteractionEnabled = false
|
|
149
|
+
controller.view.frame = CGRect(x: 0, y: 0, width: 1, height: 1)
|
|
150
|
+
presenter.addChild(controller)
|
|
151
|
+
presenter.view.addSubview(controller.view)
|
|
152
|
+
controller.didMove(toParent: presenter)
|
|
153
|
+
host = controller
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@MainActor
|
|
158
|
+
static func topViewController() -> UIViewController? {
|
|
159
|
+
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
|
|
160
|
+
let scene = scenes.first { $0.activationState == .foregroundActive } ?? scenes.first
|
|
161
|
+
let window = scene?.windows.first { $0.isKeyWindow } ?? scene?.windows.first
|
|
162
|
+
var top = window?.rootViewController
|
|
163
|
+
while let presented = top?.presentedViewController {
|
|
164
|
+
top = presented
|
|
165
|
+
}
|
|
166
|
+
return top
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// Invisible host: `.translationTask` is the only API that hands out a
|
|
171
|
+
/// session allowed to request downloads.
|
|
172
|
+
@available(iOS 26.0, *)
|
|
173
|
+
private struct DownloadHost: View {
|
|
174
|
+
let configuration: TranslationSession.Configuration
|
|
175
|
+
let onFinish: @MainActor (Result<Void, Error>) -> Void
|
|
176
|
+
|
|
177
|
+
var body: some View {
|
|
178
|
+
Color.clear.translationTask(configuration) { @MainActor session in
|
|
179
|
+
do {
|
|
180
|
+
try await session.prepareTranslation()
|
|
181
|
+
onFinish(.success(()))
|
|
182
|
+
} catch {
|
|
183
|
+
onFinish(.failure(OfflineTranslationError.failed(error.localizedDescription)))
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@available(iOS 26.0, *)
|
|
190
|
+
private actor SessionStore {
|
|
191
|
+
static let shared = SessionStore()
|
|
192
|
+
|
|
193
|
+
private var sessions: [String: TranslationSession] = [:]
|
|
194
|
+
|
|
195
|
+
func drop(source: String, target: String) {
|
|
196
|
+
sessions.removeValue(forKey: "\(source)>\(target)")?.cancel()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
func translate(text: String, source: String, target: String) async throws -> String {
|
|
200
|
+
let key = "\(source)>\(target)"
|
|
201
|
+
let session = sessions[key] ?? makeSession(source: source, target: target)
|
|
202
|
+
sessions[key] = session
|
|
203
|
+
do {
|
|
204
|
+
return try await session.translate(text).targetText
|
|
205
|
+
} catch {
|
|
206
|
+
// A failed session may be cancelled or stale; rebuild on the next call.
|
|
207
|
+
sessions.removeValue(forKey: key)
|
|
208
|
+
if TranslationError.notInstalled ~= error {
|
|
209
|
+
throw OfflineTranslationError.notInstalled(key)
|
|
210
|
+
}
|
|
211
|
+
throw OfflineTranslationError.failed(error.localizedDescription)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private func makeSession(source: String, target: String) -> TranslationSession {
|
|
216
|
+
let from = Locale.Language(identifier: source)
|
|
217
|
+
let to = Locale.Language(identifier: target)
|
|
218
|
+
if #available(iOS 26.4, *) {
|
|
219
|
+
return TranslationSession(installedSource: from, target: to, preferredStrategy: .lowLatency)
|
|
220
|
+
}
|
|
221
|
+
return TranslationSession(installedSource: from, target: to)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
#endif
|
package/package.json
CHANGED
|
@@ -257,6 +257,10 @@ export type GlassesMediaVolumeSetResult = {
|
|
|
257
257
|
statusCode: number
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
export type OfflineTranslationStatus = "installed" | "supported" | "unsupported" | "unavailable"
|
|
261
|
+
|
|
262
|
+
export type OfflineTranslationLanguage = {code: string; name: string; status: OfflineTranslationStatus}
|
|
263
|
+
|
|
260
264
|
export type LocalTranscriptionEvent = {
|
|
261
265
|
text: string
|
|
262
266
|
isFinal?: boolean
|
|
@@ -1562,6 +1566,12 @@ export interface BluetoothSdkPublicModule {
|
|
|
1562
1566
|
extractTarBz2(sourcePath: string, destinationPath: string): Promise<boolean>
|
|
1563
1567
|
restartTranscriber(): Promise<void>
|
|
1564
1568
|
|
|
1569
|
+
// Offline translation (iOS 26+; rejects elsewhere)
|
|
1570
|
+
getOfflineTranslationLanguages(source: string): Promise<OfflineTranslationLanguage[]>
|
|
1571
|
+
getOfflineTranslationStatus(source: string, target: string): Promise<OfflineTranslationStatus>
|
|
1572
|
+
downloadOfflineTranslation(source: string, target: string): Promise<OfflineTranslationStatus>
|
|
1573
|
+
translateOffline(text: string, source: string, target: string): Promise<string>
|
|
1574
|
+
|
|
1565
1575
|
// TTS Commands (TODO: MOVE TO CRUST)
|
|
1566
1576
|
setTtsModelDetails(path: string, languageCode: string): Promise<void>
|
|
1567
1577
|
getTtsModelPath(): Promise<string>
|
|
@@ -2,6 +2,8 @@ import {NativeModule, requireNativeModule} from "expo"
|
|
|
2
2
|
|
|
3
3
|
import {CAMERA_FOV_DEFAULT, CAMERA_FOV_MAX, CAMERA_FOV_MIN} from "../BluetoothSdk.types"
|
|
4
4
|
import type {
|
|
5
|
+
OfflineTranslationLanguage,
|
|
6
|
+
OfflineTranslationStatus,
|
|
5
7
|
BluetoothSettingsUpdate,
|
|
6
8
|
BluetoothSdkModuleEvents,
|
|
7
9
|
BluetoothStatus,
|
|
@@ -280,6 +282,12 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
|
|
|
280
282
|
validateSttModel(path: string): Promise<boolean>
|
|
281
283
|
extractTarBz2(sourcePath: string, destinationPath: string): Promise<boolean>
|
|
282
284
|
|
|
285
|
+
// Offline translation (iOS 26+; rejects elsewhere)
|
|
286
|
+
getOfflineTranslationLanguages(source: string): Promise<OfflineTranslationLanguage[]>
|
|
287
|
+
getOfflineTranslationStatus(source: string, target: string): Promise<OfflineTranslationStatus>
|
|
288
|
+
downloadOfflineTranslation(source: string, target: string): Promise<OfflineTranslationStatus>
|
|
289
|
+
translateOffline(text: string, source: string, target: string): Promise<string>
|
|
290
|
+
|
|
283
291
|
// Speaker Identification (on-device; see SpeakerIdEngine)
|
|
284
292
|
setSpeakerIdEnabled(enabled: boolean): Promise<void>
|
|
285
293
|
setSpeakerIdModelPath(path: string): Promise<void>
|
|
@@ -12,9 +12,9 @@ export interface BluetoothSdkReleaseMetadata {
|
|
|
12
12
|
export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
14
|
"familyBaseVersion": "0.3.1",
|
|
15
|
-
"releaseIdentity": "0.3.1-dev.
|
|
16
|
-
"releaseSetId": "glassly-0.3.1-dev.
|
|
17
|
-
"sourceCommit": "
|
|
15
|
+
"releaseIdentity": "0.3.1-dev.497",
|
|
16
|
+
"releaseSetId": "glassly-0.3.1-dev.497",
|
|
17
|
+
"sourceCommit": "e8375fec1441ab00893f2bc1e7c29530e35fb419",
|
|
18
18
|
"otaManifestUrl": null,
|
|
19
19
|
"otaManifestSha256": null
|
|
20
20
|
})
|
package/src/index.ts
CHANGED
|
@@ -192,6 +192,10 @@ export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
|
|
|
192
192
|
validateSttModel: bindPublicMethod("validateSttModel"),
|
|
193
193
|
extractTarBz2: bindPublicMethod("extractTarBz2"),
|
|
194
194
|
restartTranscriber: bindPublicMethod("restartTranscriber"),
|
|
195
|
+
getOfflineTranslationLanguages: bindPublicMethod("getOfflineTranslationLanguages"),
|
|
196
|
+
getOfflineTranslationStatus: bindPublicMethod("getOfflineTranslationStatus"),
|
|
197
|
+
downloadOfflineTranslation: bindPublicMethod("downloadOfflineTranslation"),
|
|
198
|
+
translateOffline: bindPublicMethod("translateOffline"),
|
|
195
199
|
setTtsModelDetails: bindPublicMethod("setTtsModelDetails"),
|
|
196
200
|
getTtsModelPath: bindPublicMethod("getTtsModelPath"),
|
|
197
201
|
getTtsModelLanguage: bindPublicMethod("getTtsModelLanguage"),
|