@dr33m/react-native-litert-lm 0.5.0
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/LICENSE +21 -0
- package/README.md +627 -0
- package/android/CMakeLists.txt +32 -0
- package/android/build.gradle +104 -0
- package/android/src/main/AndroidManifest.xml +19 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLM.kt +888 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridModelStore.kt +189 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/LiteRTLMRegistry.kt +32 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/MultimodalPartFactories.kt +15 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/StreamingCallbackListener.kt +89 -0
- package/android/src/main/java/dev/litert/litertlm/LiteRTLMInitProvider.kt +57 -0
- package/android/src/main/java/dev/litert/litertlm/LiteRTLMPackage.kt +43 -0
- package/android/src/test/java/com/margelo/nitro/core/Promise.kt +46 -0
- package/android/src/test/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMTest.kt +122 -0
- package/app.plugin.js +58 -0
- package/cpp/cpp-adapter.cpp +21 -0
- package/cpp/include/README.md +32 -0
- package/cpp/include/litert_lm_engine.h +516 -0
- package/cpp/include/stb_image.h +7988 -0
- package/ios/HybridLiteRTLM+Execute.swift +287 -0
- package/ios/HybridLiteRTLM+Streaming.swift +153 -0
- package/ios/HybridLiteRTLM.swift +541 -0
- package/ios/HybridModelStore.swift +206 -0
- package/ios/MultimodalPart+Factories.swift +22 -0
- package/ios/Tests/HybridLiteRTLMTests.swift +187 -0
- package/lib/__mocks__/react-native-nitro-modules.d.ts +81 -0
- package/lib/__mocks__/react-native-nitro-modules.js +98 -0
- package/lib/__tests__/hooks.test.d.ts +1 -0
- package/lib/__tests__/hooks.test.js +175 -0
- package/lib/__tests__/inferenceRouting.test.d.ts +1 -0
- package/lib/__tests__/inferenceRouting.test.js +28 -0
- package/lib/__tests__/memoryTracker.test.d.ts +1 -0
- package/lib/__tests__/memoryTracker.test.js +74 -0
- package/lib/__tests__/modelFactory.test.d.ts +1 -0
- package/lib/__tests__/modelFactory.test.js +107 -0
- package/lib/__tests__/modelPath.test.d.ts +1 -0
- package/lib/__tests__/modelPath.test.js +12 -0
- package/lib/__tests__/modelRegistry.test.d.ts +1 -0
- package/lib/__tests__/modelRegistry.test.js +61 -0
- package/lib/hooks.d.ts +46 -0
- package/lib/hooks.js +159 -0
- package/lib/index.d.ts +142 -0
- package/lib/index.js +184 -0
- package/lib/inferenceRouting.d.ts +18 -0
- package/lib/inferenceRouting.js +63 -0
- package/lib/memoryTracker.d.ts +128 -0
- package/lib/memoryTracker.js +155 -0
- package/lib/modelFactory.d.ts +24 -0
- package/lib/modelFactory.js +128 -0
- package/lib/modelPath.d.ts +8 -0
- package/lib/modelPath.js +18 -0
- package/lib/modelRegistry.d.ts +55 -0
- package/lib/modelRegistry.js +82 -0
- package/lib/specs/LiteRTLM.nitro.d.ts +380 -0
- package/lib/specs/LiteRTLM.nitro.js +2 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/LiteRTLM+autolinking.cmake +83 -0
- package/nitrogen/generated/android/LiteRTLM+autolinking.gradle +27 -0
- package/nitrogen/generated/android/LiteRTLMOnLoad.cpp +74 -0
- package/nitrogen/generated/android/LiteRTLMOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JBackend.hpp +61 -0
- package/nitrogen/generated/android/c++/JExecuteResult.hpp +86 -0
- package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_bool.hpp +76 -0
- package/nitrogen/generated/android/c++/JGenerationStats.hpp +77 -0
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.cpp +371 -0
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.hpp +83 -0
- package/nitrogen/generated/android/c++/JHybridModelStoreSpec.cpp +101 -0
- package/nitrogen/generated/android/c++/JHybridModelStoreSpec.hpp +67 -0
- package/nitrogen/generated/android/c++/JLLMConfig.hpp +125 -0
- package/nitrogen/generated/android/c++/JMemoryUsage.hpp +69 -0
- package/nitrogen/generated/android/c++/JMessage.hpp +63 -0
- package/nitrogen/generated/android/c++/JModelFile.hpp +69 -0
- package/nitrogen/generated/android/c++/JMultimodalPart.hpp +78 -0
- package/nitrogen/generated/android/c++/JPartType.hpp +61 -0
- package/nitrogen/generated/android/c++/JRole.hpp +61 -0
- package/nitrogen/generated/android/c++/JToolCall.hpp +61 -0
- package/nitrogen/generated/android/c++/JToolDefinition.hpp +65 -0
- package/nitrogen/generated/android/c++/JToolResponse.hpp +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Backend.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ExecuteResult.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_std__string_bool.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/GenerationStats.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMSpec.kt +165 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridModelStoreSpec.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LLMConfig.kt +106 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LiteRTLMOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MemoryUsage.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Message.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ModelFile.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MultimodalPart.kt +71 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/PartType.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Role.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolCall.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolDefinition.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolResponse.kt +56 -0
- package/nitrogen/generated/ios/LiteRTLM+autolinking.rb +62 -0
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.cpp +98 -0
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.hpp +585 -0
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Umbrella.hpp +93 -0
- package/nitrogen/generated/ios/LiteRTLMAutolinking.mm +41 -0
- package/nitrogen/generated/ios/LiteRTLMAutolinking.swift +38 -0
- package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.hpp +279 -0
- package/nitrogen/generated/ios/c++/HybridModelStoreSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridModelStoreSpecSwift.hpp +117 -0
- package/nitrogen/generated/ios/swift/Backend.swift +44 -0
- package/nitrogen/generated/ios/swift/ExecuteResult.swift +45 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_ExecuteResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/GenerationStats.swift +54 -0
- package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec.swift +75 -0
- package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec_cxx.swift +516 -0
- package/nitrogen/generated/ios/swift/HybridModelStoreSpec.swift +59 -0
- package/nitrogen/generated/ios/swift/HybridModelStoreSpec_cxx.swift +203 -0
- package/nitrogen/generated/ios/swift/LLMConfig.swift +239 -0
- package/nitrogen/generated/ios/swift/MemoryUsage.swift +44 -0
- package/nitrogen/generated/ios/swift/Message.swift +34 -0
- package/nitrogen/generated/ios/swift/ModelFile.swift +44 -0
- package/nitrogen/generated/ios/swift/MultimodalPart.swift +101 -0
- package/nitrogen/generated/ios/swift/PartType.swift +44 -0
- package/nitrogen/generated/ios/swift/Role.swift +44 -0
- package/nitrogen/generated/ios/swift/ToolCall.swift +34 -0
- package/nitrogen/generated/ios/swift/ToolDefinition.swift +39 -0
- package/nitrogen/generated/ios/swift/ToolResponse.swift +34 -0
- package/nitrogen/generated/shared/c++/Backend.hpp +80 -0
- package/nitrogen/generated/shared/c++/ExecuteResult.hpp +94 -0
- package/nitrogen/generated/shared/c++/GenerationStats.hpp +103 -0
- package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.cpp +41 -0
- package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.hpp +109 -0
- package/nitrogen/generated/shared/c++/HybridModelStoreSpec.cpp +25 -0
- package/nitrogen/generated/shared/c++/HybridModelStoreSpec.hpp +71 -0
- package/nitrogen/generated/shared/c++/LLMConfig.hpp +134 -0
- package/nitrogen/generated/shared/c++/MemoryUsage.hpp +95 -0
- package/nitrogen/generated/shared/c++/Message.hpp +89 -0
- package/nitrogen/generated/shared/c++/ModelFile.hpp +95 -0
- package/nitrogen/generated/shared/c++/MultimodalPart.hpp +103 -0
- package/nitrogen/generated/shared/c++/PartType.hpp +80 -0
- package/nitrogen/generated/shared/c++/Role.hpp +80 -0
- package/nitrogen/generated/shared/c++/ToolCall.hpp +87 -0
- package/nitrogen/generated/shared/c++/ToolDefinition.hpp +91 -0
- package/nitrogen/generated/shared/c++/ToolResponse.hpp +87 -0
- package/package.json +115 -0
- package/react-native-litert-lm.podspec +62 -0
- package/react-native.config.js +16 -0
- package/scripts/download-ios-frameworks.sh +39 -0
- package/scripts/framework-source.js +46 -0
- package/scripts/postinstall.js +145 -0
- package/src/__mocks__/react-native-nitro-modules.ts +132 -0
- package/src/__tests__/hooks.test.ts +216 -0
- package/src/__tests__/inferenceRouting.test.ts +36 -0
- package/src/__tests__/memoryTracker.test.ts +87 -0
- package/src/__tests__/modelFactory.test.ts +153 -0
- package/src/__tests__/modelPath.test.ts +14 -0
- package/src/__tests__/modelRegistry.test.ts +73 -0
- package/src/hooks.ts +234 -0
- package/src/index.ts +214 -0
- package/src/inferenceRouting.ts +80 -0
- package/src/memoryTracker.ts +268 -0
- package/src/modelFactory.ts +163 -0
- package/src/modelPath.ts +16 -0
- package/src/modelRegistry.ts +104 -0
- package/src/specs/LiteRTLM.nitro.ts +451 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
//
|
|
2
|
+
// HybridLiteRTLM+Execute.swift
|
|
3
|
+
// react-native-litert-lm
|
|
4
|
+
//
|
|
5
|
+
// Unified inference pipeline (execute). JS routes via src/inferenceRouting.ts;
|
|
6
|
+
// legacy send* methods below delegate here for direct native hybrid access.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import Foundation
|
|
10
|
+
import UIKit
|
|
11
|
+
import NitroModules
|
|
12
|
+
import CLiteRTLM
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// MARK: - Preprocessed parts for thread-safety
|
|
17
|
+
|
|
18
|
+
struct PreprocessedPart {
|
|
19
|
+
enum Kind {
|
|
20
|
+
case text(String)
|
|
21
|
+
case imagePath(String)
|
|
22
|
+
case imageData(Data)
|
|
23
|
+
case audioPath(String)
|
|
24
|
+
case audioData(Data)
|
|
25
|
+
}
|
|
26
|
+
let kind: Kind
|
|
27
|
+
let label: String
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// MARK: - Execute pipeline
|
|
31
|
+
|
|
32
|
+
extension HybridLiteRTLM {
|
|
33
|
+
|
|
34
|
+
/// Only used when legacy native *Async methods are invoked directly (not via createLLM JS proxy).
|
|
35
|
+
func executeVoid(
|
|
36
|
+
parts: [MultimodalPart],
|
|
37
|
+
onToken: @escaping (_ token: String, _ done: Bool) -> Void
|
|
38
|
+
) throws -> Promise<Void> {
|
|
39
|
+
let voidPromise = Promise<Void>()
|
|
40
|
+
let resultPromise = try execute(parts: parts, onToken: onToken)
|
|
41
|
+
resultPromise
|
|
42
|
+
.then { _ in voidPromise.resolve() }
|
|
43
|
+
.catch { voidPromise.reject(withError: $0) }
|
|
44
|
+
return voidPromise
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public func execute(parts: [MultimodalPart], onToken: ((_ token: String, _ done: Bool) -> Void)?) throws -> Promise<ExecuteResult> {
|
|
48
|
+
let promise = Promise<ExecuteResult>()
|
|
49
|
+
|
|
50
|
+
// Preprocess all JSI-bound data on the caller thread synchronously
|
|
51
|
+
var preprocessed: [PreprocessedPart] = []
|
|
52
|
+
for part in parts {
|
|
53
|
+
switch part.type {
|
|
54
|
+
case .text:
|
|
55
|
+
let txt = part.text ?? ""
|
|
56
|
+
preprocessed.append(PreprocessedPart(kind: .text(txt), label: txt))
|
|
57
|
+
case .image:
|
|
58
|
+
if let path = part.path {
|
|
59
|
+
preprocessed.append(PreprocessedPart(kind: .imagePath(path), label: "[Image]"))
|
|
60
|
+
} else if let buf = part.imageBuffer {
|
|
61
|
+
let data = buf.toData(copyIfNeeded: true)
|
|
62
|
+
preprocessed.append(PreprocessedPart(kind: .imageData(data), label: "[Image]"))
|
|
63
|
+
}
|
|
64
|
+
case .audio:
|
|
65
|
+
if let path = part.path {
|
|
66
|
+
preprocessed.append(PreprocessedPart(kind: .audioPath(path), label: "[Audio]"))
|
|
67
|
+
} else if let buf = part.audioBuffer {
|
|
68
|
+
let data = buf.toData(copyIfNeeded: true)
|
|
69
|
+
preprocessed.append(PreprocessedPart(kind: .audioData(data), label: "[Audio]"))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let userLabel = preprocessed.map { $0.label }.joined(separator: " ").trimmingCharacters(in: .whitespaces)
|
|
75
|
+
|
|
76
|
+
queue.async {
|
|
77
|
+
guard let conversation = self.conversation else {
|
|
78
|
+
promise.reject(withError: NSError(domain: "LiteRTLM", code: 400,
|
|
79
|
+
userInfo: [NSLocalizedDescriptionKey: "LiteRTLM: No model loaded. Call loadModel() first."]))
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let payload: (json: String, tempFiles: [String])
|
|
84
|
+
do { payload = try self.buildExecutePayload(preprocessed) }
|
|
85
|
+
catch { promise.reject(withError: error); return }
|
|
86
|
+
|
|
87
|
+
let msgJson = payload.json
|
|
88
|
+
let tempFiles = payload.tempFiles
|
|
89
|
+
let cleanup = { tempFiles.forEach { try? FileManager.default.removeItem(atPath: $0) } }
|
|
90
|
+
|
|
91
|
+
if let onToken = onToken {
|
|
92
|
+
self.runExecuteStreaming(
|
|
93
|
+
conversation: conversation,
|
|
94
|
+
msgJson: msgJson,
|
|
95
|
+
userLabel: userLabel,
|
|
96
|
+
onToken: onToken,
|
|
97
|
+
promise: promise,
|
|
98
|
+
cleanup: cleanup
|
|
99
|
+
)
|
|
100
|
+
} else {
|
|
101
|
+
self.runExecuteBlocking(
|
|
102
|
+
conversation: conversation,
|
|
103
|
+
msgJson: msgJson,
|
|
104
|
+
userLabel: userLabel,
|
|
105
|
+
promise: promise,
|
|
106
|
+
cleanup: cleanup
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return promise
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// MARK: - Payload / media
|
|
115
|
+
|
|
116
|
+
private func validateMediaPath(_ path: String, label: String) throws {
|
|
117
|
+
if !FileManager.default.fileExists(atPath: path) {
|
|
118
|
+
throw NSError(
|
|
119
|
+
domain: "LiteRTLM", code: 404,
|
|
120
|
+
userInfo: [NSLocalizedDescriptionKey: "\(label) file not found: \(path)"]
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private func buildExecutePayload(_ preprocessed: [PreprocessedPart]) throws -> (json: String, tempFiles: [String]) {
|
|
126
|
+
struct Desc { let kind: String; let text: String?; let file: String? }
|
|
127
|
+
var descs: [Desc] = []
|
|
128
|
+
var temps: [String] = []
|
|
129
|
+
|
|
130
|
+
do {
|
|
131
|
+
for part in preprocessed {
|
|
132
|
+
switch part.kind {
|
|
133
|
+
case .text(let text):
|
|
134
|
+
descs.append(Desc(kind: "text", text: text, file: nil))
|
|
135
|
+
case .imagePath(let path):
|
|
136
|
+
try validateMediaPath(path, label: "Image")
|
|
137
|
+
let scaled = scaleImageIfNeeded(path)
|
|
138
|
+
if scaled != path { temps.append(scaled) }
|
|
139
|
+
descs.append(Desc(kind: "image", text: nil, file: scaled))
|
|
140
|
+
case .imageData(let data):
|
|
141
|
+
let raw = try saveDataToTempFile(data, ext: "jpg")
|
|
142
|
+
temps.append(raw)
|
|
143
|
+
let scaled = scaleImageIfNeeded(raw)
|
|
144
|
+
if scaled != raw { temps.append(scaled) }
|
|
145
|
+
descs.append(Desc(kind: "image", text: nil, file: scaled))
|
|
146
|
+
case .audioPath(let path):
|
|
147
|
+
try validateMediaPath(path, label: "Audio")
|
|
148
|
+
descs.append(Desc(kind: "audio", text: nil, file: path))
|
|
149
|
+
case .audioData(let data):
|
|
150
|
+
let raw = try saveDataToTempFile(data, ext: "wav")
|
|
151
|
+
temps.append(raw)
|
|
152
|
+
descs.append(Desc(kind: "audio", text: nil, file: raw))
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
temps.forEach { try? FileManager.default.removeItem(atPath: $0) }
|
|
157
|
+
throw error
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if descs.count == 1 && descs[0].kind == "text" {
|
|
161
|
+
let payload: [String: Any] = ["role": "user", "content": descs[0].text ?? ""]
|
|
162
|
+
let data = try JSONSerialization.data(withJSONObject: payload, options: [])
|
|
163
|
+
if let jsonString = String(data: data, encoding: .utf8) {
|
|
164
|
+
return (jsonString, temps)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
var contents: [[String: String]] = []
|
|
169
|
+
for d in descs {
|
|
170
|
+
if d.kind == "text" {
|
|
171
|
+
contents.append(["type": "text", "text": d.text ?? ""])
|
|
172
|
+
} else if d.kind == "image" {
|
|
173
|
+
contents.append(["type": "image", "path": d.file ?? ""])
|
|
174
|
+
} else {
|
|
175
|
+
contents.append(["type": "audio", "path": d.file ?? ""])
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let payload: [String: Any] = ["role": "user", "content": contents]
|
|
180
|
+
let data = try JSONSerialization.data(withJSONObject: payload, options: [])
|
|
181
|
+
guard let jsonString = String(data: data, encoding: .utf8) else {
|
|
182
|
+
throw NSError(domain: "LiteRTLM", code: 500, userInfo: [NSLocalizedDescriptionKey: "Failed to encode JSON payload"])
|
|
183
|
+
}
|
|
184
|
+
return (jsonString, temps)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private func scaleImageIfNeeded(_ imagePath: String, maxDimension: Int = 1024) -> String {
|
|
188
|
+
guard let image = UIImage(contentsOfFile: imagePath) else { return imagePath }
|
|
189
|
+
let w = Int(image.size.width), h = Int(image.size.height)
|
|
190
|
+
guard max(w, h) > maxDimension else { return imagePath }
|
|
191
|
+
let scale = CGFloat(maxDimension) / CGFloat(max(w, h))
|
|
192
|
+
let newSize = CGSize(width: CGFloat(w) * scale, height: CGFloat(h) * scale)
|
|
193
|
+
let renderer = UIGraphicsImageRenderer(size: newSize)
|
|
194
|
+
let scaled = renderer.image { _ in image.draw(in: CGRect(origin: .zero, size: newSize)) }
|
|
195
|
+
let tmp = (NSTemporaryDirectory() as NSString)
|
|
196
|
+
.appendingPathComponent("litert_scaled_\(UUID().uuidString).jpg")
|
|
197
|
+
if let data = scaled.jpegData(compressionQuality: 0.9) {
|
|
198
|
+
do {
|
|
199
|
+
try data.write(to: URL(fileURLWithPath: tmp))
|
|
200
|
+
return tmp
|
|
201
|
+
} catch {
|
|
202
|
+
NSLog("[LiteRTLM] Failed to write scaled image to temp file: \(error.localizedDescription)")
|
|
203
|
+
return imagePath
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return imagePath
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private func saveDataToTempFile(_ data: Data, ext: String) throws -> String {
|
|
210
|
+
let tmp = (NSTemporaryDirectory() as NSString)
|
|
211
|
+
.appendingPathComponent("litert_buf_\(UUID().uuidString).\(ext)")
|
|
212
|
+
try data.write(to: URL(fileURLWithPath: tmp))
|
|
213
|
+
return tmp
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// MARK: - Streaming / blocking runners
|
|
217
|
+
|
|
218
|
+
private func runExecuteBlocking(
|
|
219
|
+
conversation: OpaquePointer,
|
|
220
|
+
msgJson: String,
|
|
221
|
+
userLabel: String,
|
|
222
|
+
promise: Promise<ExecuteResult>,
|
|
223
|
+
cleanup: @escaping () -> Void
|
|
224
|
+
) {
|
|
225
|
+
let startTime = Date()
|
|
226
|
+
guard let response = litert_lm_conversation_send_message(conversation, msgJson, nil, nil) else {
|
|
227
|
+
cleanup()
|
|
228
|
+
promise.reject(withError: NSError(domain: "LiteRTLM", code: 500,
|
|
229
|
+
userInfo: [NSLocalizedDescriptionKey: "LiteRTLM: execute failed."]))
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
defer { litert_lm_json_response_delete(response) }
|
|
233
|
+
|
|
234
|
+
var result = ""
|
|
235
|
+
if let rs = litert_lm_json_response_get_string(response) {
|
|
236
|
+
result = extractTextFromResponse(String(cString: rs))
|
|
237
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
commitExecuteTurn(
|
|
241
|
+
userLabel: userLabel,
|
|
242
|
+
modelResponse: result,
|
|
243
|
+
startTime: startTime,
|
|
244
|
+
conversation: conversation,
|
|
245
|
+
tokenCount: 0
|
|
246
|
+
)
|
|
247
|
+
cleanup()
|
|
248
|
+
// iOS: tool calls not yet captured from C API — return empty array
|
|
249
|
+
promise.resolve(withResult: ExecuteResult(text: result, toolCalls: [], thinkingText: ""))
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
func commitExecuteTurn(
|
|
255
|
+
userLabel: String,
|
|
256
|
+
modelResponse: String,
|
|
257
|
+
startTime: Date,
|
|
258
|
+
conversation: OpaquePointer?,
|
|
259
|
+
tokenCount: Int
|
|
260
|
+
) {
|
|
261
|
+
let totalTime = Date().timeIntervalSince(startTime)
|
|
262
|
+
var compTok = Double(tokenCount)
|
|
263
|
+
var tps = 0.0, ttft = 0.0
|
|
264
|
+
if let conversation = conversation,
|
|
265
|
+
let bi = litert_lm_conversation_get_benchmark_info(conversation) {
|
|
266
|
+
let turns = litert_lm_benchmark_info_get_num_decode_turns(bi)
|
|
267
|
+
if turns > 0 {
|
|
268
|
+
let li = turns - 1
|
|
269
|
+
tps = litert_lm_benchmark_info_get_decode_tokens_per_sec_at(bi, li)
|
|
270
|
+
compTok = Double(litert_lm_benchmark_info_get_decode_token_count_at(bi, li))
|
|
271
|
+
}
|
|
272
|
+
ttft = litert_lm_benchmark_info_get_time_to_first_token(bi)
|
|
273
|
+
litert_lm_benchmark_info_delete(bi)
|
|
274
|
+
}
|
|
275
|
+
let pt = Double(userLabel.count) / 4.0
|
|
276
|
+
if compTok == 0.0 { compTok = Double(modelResponse.count) / 4.0 }
|
|
277
|
+
lastStats = GenerationStats(
|
|
278
|
+
promptTokens: pt, completionTokens: compTok,
|
|
279
|
+
totalTokens: pt + compTok, timeToFirstToken: ttft,
|
|
280
|
+
totalTime: totalTime,
|
|
281
|
+
tokensPerSecond: tps > 0 ? tps : (compTok / totalTime)
|
|
282
|
+
)
|
|
283
|
+
history.append(Message(role: .user, content: userLabel))
|
|
284
|
+
history.append(Message(role: .model, content: modelResponse))
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
//
|
|
2
|
+
// HybridLiteRTLM+Streaming.swift
|
|
3
|
+
// react-native-litert-lm
|
|
4
|
+
//
|
|
5
|
+
// Streaming context and callback runners for the unified execute pipeline.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
import NitroModules
|
|
10
|
+
import CLiteRTLM
|
|
11
|
+
|
|
12
|
+
class ExecuteStreamContext {
|
|
13
|
+
let userLabel: String
|
|
14
|
+
let startTime: Date
|
|
15
|
+
let onToken: (_ token: String, _ done: Bool) -> Void
|
|
16
|
+
let promise: Promise<ExecuteResult>
|
|
17
|
+
let parent: HybridLiteRTLM
|
|
18
|
+
let cleanup: () -> Void
|
|
19
|
+
var rawResponse: String = ""
|
|
20
|
+
var fullResponse: String = ""
|
|
21
|
+
var lastEmittedLength: Int = 0
|
|
22
|
+
var tokenCount: Int = 0
|
|
23
|
+
|
|
24
|
+
init(
|
|
25
|
+
userLabel: String,
|
|
26
|
+
startTime: Date,
|
|
27
|
+
onToken: @escaping (_ token: String, _ done: Bool) -> Void,
|
|
28
|
+
promise: Promise<ExecuteResult>,
|
|
29
|
+
parent: HybridLiteRTLM,
|
|
30
|
+
cleanup: @escaping () -> Void
|
|
31
|
+
) {
|
|
32
|
+
self.userLabel = userLabel
|
|
33
|
+
self.startTime = startTime
|
|
34
|
+
self.onToken = onToken
|
|
35
|
+
self.promise = promise
|
|
36
|
+
self.parent = parent
|
|
37
|
+
self.cleanup = cleanup
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
extension HybridLiteRTLM {
|
|
42
|
+
|
|
43
|
+
func runExecuteStreaming(
|
|
44
|
+
conversation: OpaquePointer,
|
|
45
|
+
msgJson: String,
|
|
46
|
+
userLabel: String,
|
|
47
|
+
onToken: @escaping (_ token: String, _ done: Bool) -> Void,
|
|
48
|
+
promise: Promise<ExecuteResult>,
|
|
49
|
+
cleanup: @escaping () -> Void
|
|
50
|
+
) {
|
|
51
|
+
let ctx = ExecuteStreamContext(
|
|
52
|
+
userLabel: userLabel, startTime: Date(),
|
|
53
|
+
onToken: onToken, promise: promise, parent: self,
|
|
54
|
+
cleanup: cleanup
|
|
55
|
+
)
|
|
56
|
+
let ptr = Unmanaged.passRetained(ctx).toOpaque()
|
|
57
|
+
|
|
58
|
+
let cb: LiteRtLmStreamCallback = { ptr, chunk, isFinal, errorMsg in
|
|
59
|
+
guard let ptr = ptr else { return }
|
|
60
|
+
let ctx = Unmanaged<ExecuteStreamContext>.fromOpaque(ptr).takeUnretainedValue()
|
|
61
|
+
|
|
62
|
+
if let errorMsg = errorMsg {
|
|
63
|
+
let msg = String(cString: errorMsg)
|
|
64
|
+
ctx.onToken("Error: \(msg)", true)
|
|
65
|
+
ctx.cleanup()
|
|
66
|
+
ctx.promise.reject(withError: NSError(domain: "LiteRTLM", code: 500,
|
|
67
|
+
userInfo: [NSLocalizedDescriptionKey: msg]))
|
|
68
|
+
Unmanaged<ExecuteStreamContext>.fromOpaque(ptr).release()
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if isFinal {
|
|
73
|
+
ctx.parent.finalizeExecuteStream(ctx: ctx, streamPtr: ptr)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if let chunk = chunk {
|
|
78
|
+
ctx.parent.emitExecuteStreamChunk(ctx: ctx, chunk: chunk)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let status = litert_lm_conversation_send_message_stream(
|
|
83
|
+
conversation, msgJson, nil, nil, cb, ptr)
|
|
84
|
+
if status != 0 {
|
|
85
|
+
Unmanaged<ExecuteStreamContext>.fromOpaque(ptr).release()
|
|
86
|
+
cleanup()
|
|
87
|
+
promise.reject(withError: NSError(domain: "LiteRTLM", code: Int(status),
|
|
88
|
+
userInfo: [NSLocalizedDescriptionKey: "LiteRTLM: execute streaming failed."]))
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func finalizeExecuteStream(ctx: ExecuteStreamContext, streamPtr: UnsafeMutableRawPointer) {
|
|
93
|
+
let cleaned = stripControlTokens(ctx.rawResponse)
|
|
94
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
95
|
+
var finalText = cleaned
|
|
96
|
+
if !ctx.userLabel.isEmpty && finalText.hasPrefix(ctx.userLabel) {
|
|
97
|
+
finalText = String(finalText.dropFirst(ctx.userLabel.count))
|
|
98
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
99
|
+
}
|
|
100
|
+
if finalText.count > ctx.lastEmittedLength {
|
|
101
|
+
let si = finalText.index(finalText.startIndex, offsetBy: ctx.lastEmittedLength)
|
|
102
|
+
ctx.onToken(String(finalText[si...]), false)
|
|
103
|
+
}
|
|
104
|
+
ctx.fullResponse = finalText
|
|
105
|
+
|
|
106
|
+
queue.async {
|
|
107
|
+
self.commitExecuteTurn(
|
|
108
|
+
userLabel: ctx.userLabel,
|
|
109
|
+
modelResponse: ctx.fullResponse,
|
|
110
|
+
startTime: ctx.startTime,
|
|
111
|
+
conversation: self.conversation,
|
|
112
|
+
tokenCount: ctx.tokenCount
|
|
113
|
+
)
|
|
114
|
+
ctx.onToken("", true)
|
|
115
|
+
ctx.cleanup()
|
|
116
|
+
// iOS: tool calls not yet captured from C API — return empty array
|
|
117
|
+
ctx.promise.resolve(withResult: ExecuteResult(text: ctx.fullResponse, toolCalls: [], thinkingText: ""))
|
|
118
|
+
Unmanaged<ExecuteStreamContext>.fromOpaque(streamPtr).release()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func emitExecuteStreamChunk(ctx: ExecuteStreamContext, chunk: UnsafePointer<CChar>) {
|
|
123
|
+
let token = String(cString: chunk)
|
|
124
|
+
let raw = token.hasPrefix("{") && token.contains("\"role\"")
|
|
125
|
+
? extractTextFromResponse(token) : token
|
|
126
|
+
ctx.rawResponse += raw
|
|
127
|
+
let cleaned = stripControlTokens(ctx.rawResponse)
|
|
128
|
+
.trimmingLeadingCharacters(in: .whitespacesAndNewlines)
|
|
129
|
+
var processed = cleaned
|
|
130
|
+
if !ctx.userLabel.isEmpty && processed.hasPrefix(ctx.userLabel) {
|
|
131
|
+
processed = String(processed.dropFirst(ctx.userLabel.count))
|
|
132
|
+
.trimmingLeadingCharacters(in: .whitespacesAndNewlines)
|
|
133
|
+
}
|
|
134
|
+
let safe = safeEmitLength(processed)
|
|
135
|
+
if safe > ctx.lastEmittedLength {
|
|
136
|
+
let chars = Array(processed)
|
|
137
|
+
ctx.onToken(String(chars[ctx.lastEmittedLength..<safe]), false)
|
|
138
|
+
ctx.lastEmittedLength = safe
|
|
139
|
+
ctx.tokenCount += 1
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private extension String {
|
|
145
|
+
func trimmingLeadingCharacters(in characterSet: CharacterSet) -> String {
|
|
146
|
+
guard let index = firstIndex(where: { char in
|
|
147
|
+
!char.unicodeScalars.allSatisfy { characterSet.contains($0) }
|
|
148
|
+
}) else {
|
|
149
|
+
return ""
|
|
150
|
+
}
|
|
151
|
+
return String(self[index...])
|
|
152
|
+
}
|
|
153
|
+
}
|