bleam 0.0.7 → 0.0.9
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/dist/cli.cjs +23 -4
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +23 -4
- package/dist/{ui-234Plg7Z.d.cts → ui-Bg11tvlc.d.ts} +7 -7
- package/dist/ui.d.ts +1 -1
- package/dist/window.d.ts +1 -1
- package/package.json +4 -1
- package/templates/image-generation/app/index.tsx +1 -1
- package/templates/native/.gitattributes +1 -0
- package/templates/native/App.tsx +26 -0
- package/templates/native/app.json +12 -0
- package/templates/native/index.ts +8 -0
- package/templates/native/ios/.xcode.env +11 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/Flux2Config.swift +288 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/MemoryConfig.swift +233 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/MemoryOptimizationConfig.swift +157 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +425 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/QuantizationConfig.swift +138 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/TransformerRepoProfile.swift +59 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/VAEConfig.swift +134 -0
- package/templates/native/ios/Bleam/AI/Flux2/Flux2Core.swift +55 -0
- package/templates/native/ios/Bleam/AI/Flux2/Loading/WeightLoader.swift +1192 -0
- package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +1282 -0
- package/templates/native/ios/Bleam/AI/Flux2/Pipeline/LatentUtils.swift +191 -0
- package/templates/native/ios/Bleam/AI/Flux2/Scheduler/FlowMatchEulerScheduler.swift +279 -0
- package/templates/native/ios/Bleam/AI/Flux2/Text/KleinTextEncoder.swift +380 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Attention.swift +379 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Embeddings.swift +159 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2FeedForward.swift +154 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2FusedKernels.swift +198 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2KVCache.swift +36 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Modulation.swift +193 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2ParallelAttention.swift +389 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2RoPE.swift +304 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2SingleBlock.swift +290 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Transformer.swift +530 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2TransformerBlock.swift +362 -0
- package/templates/native/ios/Bleam/AI/Flux2/Utils/Flux2Debug.swift +101 -0
- package/templates/native/ios/Bleam/AI/Flux2/Utils/MLXCheckpoint.swift +118 -0
- package/templates/native/ios/Bleam/AI/Flux2/Utils/MemoryManager.swift +201 -0
- package/templates/native/ios/Bleam/AI/Flux2/VAE/AutoencoderKL.swift +476 -0
- package/templates/native/ios/Bleam/AI/Flux2/VAE/ResnetBlock.swift +316 -0
- package/templates/native/ios/Bleam/AI/Flux2/VAE/VAEDecoder.swift +120 -0
- package/templates/native/ios/Bleam/AI/Flux2/VAE/VAEEncoder.swift +136 -0
- package/templates/native/ios/Bleam/AppDelegate.swift +373 -0
- package/templates/native/ios/Bleam/Appearance.swift +62 -0
- package/templates/native/ios/Bleam/Bridging-Header.h +3 -0
- package/templates/native/ios/Bleam/Images.xcassets/AppIcon.appiconset/Contents.json +13 -0
- package/templates/native/ios/Bleam/Images.xcassets/Contents.json +6 -0
- package/templates/native/ios/Bleam/Info.plist +76 -0
- package/templates/native/ios/Bleam/PrivacyInfo.xcprivacy +48 -0
- package/templates/native/ios/Bleam/SceneDelegate.swift +58 -0
- package/templates/native/ios/Bleam/SplashScreen.storyboard +47 -0
- package/templates/native/ios/Bleam/Supporting/Expo.plist +6 -0
- package/templates/native/ios/Bleam/bleam.entitlements +10 -0
- package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +1146 -0
- package/templates/native/ios/Bleam.xcodeproj/xcshareddata/xcschemes/Bleam.xcscheme +88 -0
- package/templates/native/ios/GenerationService/GenerationService.entitlements +10 -0
- package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +591 -0
- package/templates/native/ios/GenerationService/Info.plist +31 -0
- package/templates/native/ios/GenerationService/main.swift +165 -0
- package/templates/native/ios/PlatformHelper/Info.plist +29 -0
- package/templates/native/ios/PlatformHelper/main.swift +335 -0
- package/templates/native/ios/Podfile +198 -0
- package/templates/native/ios/Podfile.lock +2284 -0
- package/templates/native/ios/Podfile.properties.json +5 -0
- package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +12 -0
- package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +124 -0
- package/templates/native/metro.config.js +6 -0
- package/templates/native/modules/bleam-runtime/BleamRuntime.podspec +14 -0
- package/templates/native/modules/bleam-runtime/expo-module.config.json +8 -0
- package/templates/native/modules/bleam-runtime/ios/AIModule.swift +1258 -0
- package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +135 -0
- package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +313 -0
- package/templates/native/modules/bleam-runtime/package.json +5 -0
- package/templates/native/package.json +39 -0
- package/templates/native/scripts/build/mlx-frameworks.ts +421 -0
- package/templates/native/scripts/start/index.ts +474 -0
- package/templates/native/scripts/start/known-failures.ts +20 -0
- package/templates/native/scripts/start/loader.ts +76 -0
- package/templates/native/scripts/start/native-state.ts +242 -0
- package/templates/native/scripts/start/package.json +4 -0
- package/templates/native/scripts/start/swift-packages.ts +414 -0
- package/templates/native/scripts/start/xcode-formatter.ts +106 -0
- package/templates/native/tsconfig.json +14 -0
- package/templates/native/yarn.lock +3749 -0
- package/dist/ui-BJmXhz9Q.d.ts +0 -92
- package/dist/ui-Dd7SXdbg.d.cts +0 -92
- package/dist/ui-Ds88eETu.d.ts +0 -92
- package/dist/ui-TaqnB5SP.d.ts +0 -92
- package/dist/ui-WQvnXL0W.d.ts +0 -92
|
@@ -0,0 +1,1258 @@
|
|
|
1
|
+
import CoreImage
|
|
2
|
+
import CoreImage.CIFilterBuiltins
|
|
3
|
+
import ExpoModulesCore
|
|
4
|
+
import Foundation
|
|
5
|
+
|
|
6
|
+
#if canImport(FoundationModels)
|
|
7
|
+
import FoundationModels
|
|
8
|
+
#endif
|
|
9
|
+
|
|
10
|
+
public final class AIModule: Module {
|
|
11
|
+
@MainActor private let textGeneration = TextGenerationCoordinator()
|
|
12
|
+
private static var appGroupIdentifier: String {
|
|
13
|
+
Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String ?? "group.dev.bleam.app"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public func definition() -> ModuleDefinition {
|
|
17
|
+
Name("AI")
|
|
18
|
+
Events("imagegenProgress", "textGenerationUpdate", "toolCall")
|
|
19
|
+
|
|
20
|
+
Function("isAvailable") { () -> Bool in
|
|
21
|
+
#if canImport(FoundationModels)
|
|
22
|
+
if #available(iOS 26, *) {
|
|
23
|
+
return SystemLanguageModel.default.availability == .available
|
|
24
|
+
}
|
|
25
|
+
#endif
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Function("availability") { () -> [String: Any] in
|
|
30
|
+
#if canImport(FoundationModels)
|
|
31
|
+
if #available(iOS 26, *) {
|
|
32
|
+
switch SystemLanguageModel.default.availability {
|
|
33
|
+
case .available:
|
|
34
|
+
return ["available": true]
|
|
35
|
+
case .unavailable(.deviceNotEligible):
|
|
36
|
+
return ["available": false, "reason": "device_not_eligible"]
|
|
37
|
+
case .unavailable(.appleIntelligenceNotEnabled):
|
|
38
|
+
return ["available": false, "reason": "apple_intelligence_not_enabled"]
|
|
39
|
+
case .unavailable(.modelNotReady):
|
|
40
|
+
return ["available": false, "reason": "model_not_ready"]
|
|
41
|
+
case .unavailable:
|
|
42
|
+
return ["available": false, "reason": "unknown"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
#endif
|
|
46
|
+
return ["available": false, "reason": "unknown"]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
AsyncFunction("generate") { [weak self] (request: TextGenerationRequest, promise: Promise) in
|
|
50
|
+
#if canImport(FoundationModels)
|
|
51
|
+
if #available(iOS 26, *) {
|
|
52
|
+
guard SystemLanguageModel.default.availability == .available else {
|
|
53
|
+
promise.reject("MODEL_UNAVAILABLE", "Apple Intelligence model is not available")
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
guard !request.prompt.isEmpty else {
|
|
58
|
+
promise.reject("INVALID_MESSAGE", "Expected prompt")
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let generationID = request.id ?? UUID().uuidString
|
|
63
|
+
let task = Task { @MainActor [weak self] in
|
|
64
|
+
guard let self else { return }
|
|
65
|
+
do {
|
|
66
|
+
let tools = try Self.tools(
|
|
67
|
+
from: request.tools,
|
|
68
|
+
generationID: generationID,
|
|
69
|
+
coordinator: self.textGeneration,
|
|
70
|
+
emit: { [weak self] event in self?.sendEvent("toolCall", event) }
|
|
71
|
+
)
|
|
72
|
+
let session = Self.session(system: request.system, tools: tools)
|
|
73
|
+
let options = try Self.generationOptions(from: request)
|
|
74
|
+
|
|
75
|
+
if let schema = request.outputSchema {
|
|
76
|
+
let generationSchema = try Self.generationSchema(from: schema)
|
|
77
|
+
let response = try await session.respond(
|
|
78
|
+
to: request.prompt,
|
|
79
|
+
schema: generationSchema,
|
|
80
|
+
includeSchemaInPrompt: true,
|
|
81
|
+
options: options
|
|
82
|
+
)
|
|
83
|
+
promise.resolve(try Self.value(from: response.content, schema: schema))
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let response = try await session.respond(to: request.prompt, options: options)
|
|
88
|
+
promise.resolve(response.content)
|
|
89
|
+
} catch {
|
|
90
|
+
Self.reject(error, promise: promise)
|
|
91
|
+
}
|
|
92
|
+
self.textGeneration.finish(generationID)
|
|
93
|
+
}
|
|
94
|
+
Task { @MainActor [weak self] in self?.textGeneration.start(generationID, task: task) }
|
|
95
|
+
} else {
|
|
96
|
+
promise.reject("UNSUPPORTED_OS", "Apple Foundation Models require iOS 26 or newer")
|
|
97
|
+
}
|
|
98
|
+
#else
|
|
99
|
+
promise.reject("UNSUPPORTED_OS", "Apple Foundation Models are unavailable in this build")
|
|
100
|
+
#endif
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
AsyncFunction("streamGenerate") { [weak self] (request: TextGenerationRequest, promise: Promise) in
|
|
104
|
+
#if canImport(FoundationModels)
|
|
105
|
+
if #available(iOS 26, *) {
|
|
106
|
+
guard SystemLanguageModel.default.availability == .available else {
|
|
107
|
+
promise.reject("MODEL_UNAVAILABLE", "Apple Intelligence model is not available")
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
guard !request.prompt.isEmpty else {
|
|
111
|
+
promise.reject("INVALID_MESSAGE", "Expected prompt")
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let generationID = request.id ?? UUID().uuidString
|
|
116
|
+
let task = Task { @MainActor [weak self] in
|
|
117
|
+
guard let self else { return }
|
|
118
|
+
do {
|
|
119
|
+
let tools = try Self.tools(
|
|
120
|
+
from: request.tools,
|
|
121
|
+
generationID: generationID,
|
|
122
|
+
coordinator: self.textGeneration,
|
|
123
|
+
emit: { [weak self] event in self?.sendEvent("toolCall", event) }
|
|
124
|
+
)
|
|
125
|
+
let session = Self.session(system: request.system, tools: tools)
|
|
126
|
+
let options = try Self.generationOptions(from: request)
|
|
127
|
+
let stream = session.streamResponse(to: request.prompt, options: options)
|
|
128
|
+
var content = ""
|
|
129
|
+
for try await snapshot in stream {
|
|
130
|
+
try Task.checkCancellation()
|
|
131
|
+
content = snapshot.content
|
|
132
|
+
self.sendEvent("textGenerationUpdate", ["id": generationID, "content": content])
|
|
133
|
+
}
|
|
134
|
+
promise.resolve(content)
|
|
135
|
+
} catch {
|
|
136
|
+
Self.reject(error, promise: promise)
|
|
137
|
+
}
|
|
138
|
+
self.textGeneration.finish(generationID)
|
|
139
|
+
}
|
|
140
|
+
Task { @MainActor [weak self] in self?.textGeneration.start(generationID, task: task) }
|
|
141
|
+
} else {
|
|
142
|
+
promise.reject("UNSUPPORTED_OS", "Apple Foundation Models require iOS 26 or newer")
|
|
143
|
+
}
|
|
144
|
+
#else
|
|
145
|
+
promise.reject("UNSUPPORTED_OS", "Apple Foundation Models are unavailable in this build")
|
|
146
|
+
#endif
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
AsyncFunction("cancelTextGeneration") { [weak self] (id: String) -> Bool in
|
|
150
|
+
await self?.textGeneration.cancel(id) ?? false
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
AsyncFunction("resolveToolCall") { [weak self] (request: ToolResultRequest) -> Bool in
|
|
154
|
+
await self?.textGeneration.resolve(request) ?? false
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
AsyncFunction("generateImage") { (request: ImageGenerationRequest, promise: Promise) in
|
|
158
|
+
do {
|
|
159
|
+
let imageRequest = try Self.imageRequest(from: request)
|
|
160
|
+
try Self.validateImageRequest(imageRequest)
|
|
161
|
+
let workerRequest = try Self.workerRequest(from: imageRequest)
|
|
162
|
+
Task { @MainActor in
|
|
163
|
+
do {
|
|
164
|
+
let event = try await ImageGenerationServiceClient.shared.run(
|
|
165
|
+
request: workerRequest,
|
|
166
|
+
onProgress: { [weak self] event in
|
|
167
|
+
guard let currentStep = event.currentStep, let totalSteps = event.totalSteps else { return }
|
|
168
|
+
self?.sendEvent("imagegenProgress", [
|
|
169
|
+
"currentStep": currentStep,
|
|
170
|
+
"totalSteps": totalSteps,
|
|
171
|
+
])
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
guard let path = event.path, let width = event.width, let height = event.height, let model = event.model else {
|
|
175
|
+
throw ModuleError.imageGeneration("GenerationService returned an incomplete result")
|
|
176
|
+
}
|
|
177
|
+
var response: [String: Any] = [
|
|
178
|
+
"uri": URL(fileURLWithPath: path).absoluteString,
|
|
179
|
+
"width": width,
|
|
180
|
+
"height": height,
|
|
181
|
+
"model": model,
|
|
182
|
+
]
|
|
183
|
+
if let metadata = Self.metadataDictionary(event.metadata) {
|
|
184
|
+
response["metadata"] = metadata
|
|
185
|
+
}
|
|
186
|
+
promise.resolve(response)
|
|
187
|
+
} catch {
|
|
188
|
+
Self.rejectImageGeneration(error, promise: promise)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
promise.reject(error)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
AsyncFunction("cancelImagegen") { (promise: Promise) in
|
|
197
|
+
Task { @MainActor in
|
|
198
|
+
do {
|
|
199
|
+
promise.resolve(try await ImageGenerationServiceClient.shared.cancel())
|
|
200
|
+
} catch {
|
|
201
|
+
promise.reject(error)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
AsyncFunction("imageMemoryTelemetry") { (promise: Promise) in
|
|
207
|
+
Task { @MainActor in
|
|
208
|
+
do {
|
|
209
|
+
promise.resolve(try await ImageGenerationServiceClient.shared.memoryTelemetry())
|
|
210
|
+
} catch {
|
|
211
|
+
promise.reject(error)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
AsyncFunction("unloadImageModel") { (promise: Promise) in
|
|
217
|
+
Task { @MainActor in
|
|
218
|
+
do {
|
|
219
|
+
promise.resolve(try await ImageGenerationServiceClient.shared.unload())
|
|
220
|
+
} catch {
|
|
221
|
+
promise.reject(error)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
AsyncFunction("upscaleImage") { (request: UpscaleImageRequest) throws -> [String: Any] in
|
|
227
|
+
guard request.scale > 1, request.scale <= 4, request.scale.isFinite else {
|
|
228
|
+
throw ModuleError.invalidModel("Upscale scale must be greater than 1 and at most 4")
|
|
229
|
+
}
|
|
230
|
+
let sourceURL = URL(fileURLWithPath: request.imagePath)
|
|
231
|
+
guard sourceURL.isFileURL, FileManager.default.fileExists(atPath: sourceURL.path) else {
|
|
232
|
+
throw ModuleError.invalidMessage("Upscale image does not exist")
|
|
233
|
+
}
|
|
234
|
+
guard let source = CIImage(contentsOf: sourceURL) else {
|
|
235
|
+
throw ModuleError.invalidMessage("Upscale image could not be decoded")
|
|
236
|
+
}
|
|
237
|
+
let width = Int((source.extent.width * request.scale).rounded())
|
|
238
|
+
let height = Int((source.extent.height * request.scale).rounded())
|
|
239
|
+
guard width > 0, height > 0, width <= 16_384, height <= 16_384 else {
|
|
240
|
+
throw ModuleError.invalidModel("Upscaled dimensions must not exceed 16384 pixels")
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let filter = CIFilter.lanczosScaleTransform()
|
|
244
|
+
filter.inputImage = source
|
|
245
|
+
filter.scale = Float(request.scale)
|
|
246
|
+
filter.aspectRatio = 1
|
|
247
|
+
guard let output = filter.outputImage else {
|
|
248
|
+
throw ModuleError.imageGeneration("Image upscaling failed")
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let directory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
|
252
|
+
.appendingPathComponent("Bleam/Upscaled", isDirectory: true)
|
|
253
|
+
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
254
|
+
let outputURL = directory.appendingPathComponent("\(UUID().uuidString.lowercased()).png")
|
|
255
|
+
let colorSpace = source.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!
|
|
256
|
+
try CIContext().writePNGRepresentation(
|
|
257
|
+
of: output,
|
|
258
|
+
to: outputURL,
|
|
259
|
+
format: .RGBA8,
|
|
260
|
+
colorSpace: colorSpace
|
|
261
|
+
)
|
|
262
|
+
return [
|
|
263
|
+
"uri": outputURL.absoluteString,
|
|
264
|
+
"width": width,
|
|
265
|
+
"height": height,
|
|
266
|
+
"scale": request.scale,
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private static func imageRequest(from request: ImageGenerationRequest) throws -> FluxImageRequest {
|
|
272
|
+
guard !request.model.isEmpty else {
|
|
273
|
+
throw ModuleError.invalidModel("Missing image model")
|
|
274
|
+
}
|
|
275
|
+
guard request.model == "flux2-klein-4b-8bit-abliterated" else {
|
|
276
|
+
throw ModuleError.invalidModel("Unsupported image model: \(request.model)")
|
|
277
|
+
}
|
|
278
|
+
guard !request.prompt.isEmpty else {
|
|
279
|
+
throw ModuleError.invalidMessage("Expected prompt")
|
|
280
|
+
}
|
|
281
|
+
if let sourceImagePath = request.sourceImagePath, sourceImagePath.isEmpty {
|
|
282
|
+
throw ModuleError.invalidMessage("Source image path must not be empty")
|
|
283
|
+
}
|
|
284
|
+
if let maskImagePath = request.maskImagePath, maskImagePath.isEmpty {
|
|
285
|
+
throw ModuleError.invalidMessage("Mask image path must not be empty")
|
|
286
|
+
}
|
|
287
|
+
if request.maskImagePath != nil, request.sourceImagePath == nil {
|
|
288
|
+
throw ModuleError.invalidMessage("Mask image requires a source image")
|
|
289
|
+
}
|
|
290
|
+
if let maskMode = request.maskMode, maskMode != "paint" && maskMode != "preserve" {
|
|
291
|
+
throw ModuleError.invalidModel("Mask mode must be paint or preserve")
|
|
292
|
+
}
|
|
293
|
+
if request.outpaintWidth != nil || request.outpaintHeight != nil || request.outpaintSourceX != nil || request.outpaintSourceY != nil || request.outpaintSourceWidth != nil || request.outpaintSourceHeight != nil {
|
|
294
|
+
guard request.sourceImagePath != nil else {
|
|
295
|
+
throw ModuleError.invalidMessage("Outpaint requires a source image")
|
|
296
|
+
}
|
|
297
|
+
guard let outpaintWidth = request.outpaintWidth, outpaintWidth > 0 else {
|
|
298
|
+
throw ModuleError.invalidModel("Invalid outpaint width")
|
|
299
|
+
}
|
|
300
|
+
guard let outpaintHeight = request.outpaintHeight, outpaintHeight > 0 else {
|
|
301
|
+
throw ModuleError.invalidModel("Invalid outpaint height")
|
|
302
|
+
}
|
|
303
|
+
guard let sourceX = request.outpaintSourceX,
|
|
304
|
+
let sourceY = request.outpaintSourceY,
|
|
305
|
+
let sourceWidth = request.outpaintSourceWidth, sourceWidth > 0,
|
|
306
|
+
let sourceHeight = request.outpaintSourceHeight, sourceHeight > 0
|
|
307
|
+
else {
|
|
308
|
+
throw ModuleError.invalidModel("Outpaint requires a valid source rect")
|
|
309
|
+
}
|
|
310
|
+
guard sourceX < outpaintWidth, sourceY < outpaintHeight,
|
|
311
|
+
sourceX + sourceWidth > 0, sourceY + sourceHeight > 0
|
|
312
|
+
else {
|
|
313
|
+
throw ModuleError.invalidModel("Outpaint source rect must intersect the canvas")
|
|
314
|
+
}
|
|
315
|
+
if request.outpaintAutoCrop != true,
|
|
316
|
+
(sourceX < 0 || sourceY < 0 || sourceX + sourceWidth > outpaintWidth || sourceY + sourceHeight > outpaintHeight)
|
|
317
|
+
{
|
|
318
|
+
throw ModuleError.invalidModel("Outpaint source rect must fit within the canvas unless autoCrop is enabled")
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if let resizeMode = request.resizeMode, !["stretch", "fit", "crop"].contains(resizeMode) {
|
|
322
|
+
throw ModuleError.invalidModel("Resize mode must be stretch, fit, or crop")
|
|
323
|
+
}
|
|
324
|
+
if let maxInputLongSide = request.maxInputLongSide, maxInputLongSide != 0 && maxInputLongSide < 64 {
|
|
325
|
+
throw ModuleError.invalidModel("maxInputLongSide must be 0 or at least 64")
|
|
326
|
+
}
|
|
327
|
+
guard request.referenceImagePaths.allSatisfy({ !$0.isEmpty }) else {
|
|
328
|
+
throw ModuleError.invalidMessage("Reference image paths must not be empty")
|
|
329
|
+
}
|
|
330
|
+
guard request.referenceImagePaths.count <= 3 else {
|
|
331
|
+
throw ModuleError.invalidModel("Expected at most 3 reference images")
|
|
332
|
+
}
|
|
333
|
+
if let referenceInfluences = request.referenceInfluences {
|
|
334
|
+
guard referenceInfluences.count == request.referenceImagePaths.count else {
|
|
335
|
+
throw ModuleError.invalidModel("Reference influences must match reference image count")
|
|
336
|
+
}
|
|
337
|
+
guard referenceInfluences.allSatisfy({ $0.isFinite && $0 >= 0 }) else {
|
|
338
|
+
throw ModuleError.invalidModel("Reference influences must be non-negative numbers")
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
guard request.loraPaths.allSatisfy({ !$0.isEmpty }) else {
|
|
342
|
+
throw ModuleError.invalidMessage("LoRA paths must not be empty")
|
|
343
|
+
}
|
|
344
|
+
if let loraScales = request.loraScales {
|
|
345
|
+
guard loraScales.count == request.loraPaths.count else {
|
|
346
|
+
throw ModuleError.invalidModel("LoRA scales must match LoRA path count")
|
|
347
|
+
}
|
|
348
|
+
guard loraScales.allSatisfy({ $0.isFinite && $0 > 0 }) else {
|
|
349
|
+
throw ModuleError.invalidModel("LoRA scales must be positive numbers")
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if let loraIds = request.loraIds {
|
|
353
|
+
guard loraIds.count == request.loraPaths.count else {
|
|
354
|
+
throw ModuleError.invalidModel("LoRA ids must match LoRA path count")
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
guard !request.modelRoot.isEmpty else {
|
|
358
|
+
throw ModuleError.invalidModel("Missing model root")
|
|
359
|
+
}
|
|
360
|
+
guard !request.textEncoderPath.isEmpty else {
|
|
361
|
+
throw ModuleError.invalidModel("Missing text encoder path")
|
|
362
|
+
}
|
|
363
|
+
guard !request.transformerPath.isEmpty else {
|
|
364
|
+
throw ModuleError.invalidModel("Missing transformer path")
|
|
365
|
+
}
|
|
366
|
+
guard !request.vaePath.isEmpty else {
|
|
367
|
+
throw ModuleError.invalidModel("Missing VAE path")
|
|
368
|
+
}
|
|
369
|
+
guard request.width > 0 else {
|
|
370
|
+
throw ModuleError.invalidModel("Invalid image width")
|
|
371
|
+
}
|
|
372
|
+
guard request.height > 0 else {
|
|
373
|
+
throw ModuleError.invalidModel("Invalid image height")
|
|
374
|
+
}
|
|
375
|
+
guard request.steps > 0 else {
|
|
376
|
+
throw ModuleError.invalidModel("Invalid image steps")
|
|
377
|
+
}
|
|
378
|
+
guard request.guidance > 0 else {
|
|
379
|
+
throw ModuleError.invalidModel("Invalid image guidance")
|
|
380
|
+
}
|
|
381
|
+
if let strength = request.strength, !(strength > 0 && strength <= 1) {
|
|
382
|
+
throw ModuleError.invalidModel("Image strength must be greater than 0 and at most 1")
|
|
383
|
+
}
|
|
384
|
+
guard !request.inputIds.isEmpty else {
|
|
385
|
+
throw ModuleError.invalidMessage("Missing tokenized input ids")
|
|
386
|
+
}
|
|
387
|
+
guard request.attentionMask.count == request.inputIds.count else {
|
|
388
|
+
throw ModuleError.invalidMessage("Attention mask must match input ids")
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return FluxImageRequest(
|
|
392
|
+
model: request.model,
|
|
393
|
+
prompt: request.prompt,
|
|
394
|
+
sourceImagePath: request.sourceImagePath,
|
|
395
|
+
maskImagePath: request.maskImagePath,
|
|
396
|
+
maskMode: request.maskMode,
|
|
397
|
+
outpaintWidth: request.outpaintWidth,
|
|
398
|
+
outpaintHeight: request.outpaintHeight,
|
|
399
|
+
outpaintSourceX: request.outpaintSourceX,
|
|
400
|
+
outpaintSourceY: request.outpaintSourceY,
|
|
401
|
+
outpaintSourceWidth: request.outpaintSourceWidth,
|
|
402
|
+
outpaintSourceHeight: request.outpaintSourceHeight,
|
|
403
|
+
outpaintAutoCrop: request.outpaintAutoCrop,
|
|
404
|
+
resizeMode: request.resizeMode,
|
|
405
|
+
maxInputLongSide: request.maxInputLongSide,
|
|
406
|
+
referenceImagePaths: request.referenceImagePaths,
|
|
407
|
+
referenceInfluences: request.referenceInfluences,
|
|
408
|
+
loraPaths: request.loraPaths,
|
|
409
|
+
loraScales: request.loraScales,
|
|
410
|
+
loraIds: request.loraIds,
|
|
411
|
+
sigmaSchedule: request.sigmaSchedule,
|
|
412
|
+
width: request.width,
|
|
413
|
+
height: request.height,
|
|
414
|
+
steps: request.steps,
|
|
415
|
+
guidance: request.guidance,
|
|
416
|
+
strength: request.strength,
|
|
417
|
+
seed: request.seed,
|
|
418
|
+
inputIds: request.inputIds,
|
|
419
|
+
attentionMask: request.attentionMask,
|
|
420
|
+
modelRoot: request.modelRoot,
|
|
421
|
+
textEncoderPath: request.textEncoderPath,
|
|
422
|
+
transformerPath: request.transformerPath,
|
|
423
|
+
vaePath: request.vaePath
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private static func validateImageRequest(_ request: FluxImageRequest) throws {
|
|
428
|
+
let fileManager = FileManager.default
|
|
429
|
+
for path in [request.modelRoot, request.textEncoderPath, request.transformerPath, request.vaePath] {
|
|
430
|
+
var isDirectory: ObjCBool = false
|
|
431
|
+
guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory), isDirectory.boolValue else {
|
|
432
|
+
throw ModuleError.invalidModel("Missing model directory: \(path)")
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
for path in [request.sourceImagePath, request.maskImagePath].compactMap({ $0 }) + request.referenceImagePaths + request.loraPaths {
|
|
437
|
+
var isDirectory: ObjCBool = false
|
|
438
|
+
guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory), !isDirectory.boolValue else {
|
|
439
|
+
throw ModuleError.invalidMessage("Missing image file: \(path)")
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private static func workerRequest(from request: FluxImageRequest) throws -> GenerationWorkerRequest {
|
|
445
|
+
let jobID = UUID()
|
|
446
|
+
guard let appGroupContainer = FileManager.default.containerURL(
|
|
447
|
+
forSecurityApplicationGroupIdentifier: appGroupIdentifier
|
|
448
|
+
) else {
|
|
449
|
+
throw ModuleError.imageGeneration("Shared generation container is unavailable")
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
let stagingDirectory = appGroupContainer
|
|
453
|
+
.appendingPathComponent("GenerationRuns", isDirectory: true)
|
|
454
|
+
.appendingPathComponent("bleam-generation-jobs", isDirectory: true)
|
|
455
|
+
.appendingPathComponent(jobID.uuidString, isDirectory: true)
|
|
456
|
+
try FileManager.default.createDirectory(at: stagingDirectory, withIntermediateDirectories: true)
|
|
457
|
+
|
|
458
|
+
return GenerationWorkerRequest(
|
|
459
|
+
jobID: jobID,
|
|
460
|
+
model: request.model,
|
|
461
|
+
prompt: request.prompt,
|
|
462
|
+
sourceImagePath: request.sourceImagePath,
|
|
463
|
+
maskImagePath: request.maskImagePath,
|
|
464
|
+
maskMode: request.maskMode,
|
|
465
|
+
outpaintWidth: request.outpaintWidth,
|
|
466
|
+
outpaintHeight: request.outpaintHeight,
|
|
467
|
+
outpaintSourceX: request.outpaintSourceX,
|
|
468
|
+
outpaintSourceY: request.outpaintSourceY,
|
|
469
|
+
outpaintSourceWidth: request.outpaintSourceWidth,
|
|
470
|
+
outpaintSourceHeight: request.outpaintSourceHeight,
|
|
471
|
+
outpaintAutoCrop: request.outpaintAutoCrop,
|
|
472
|
+
resizeMode: request.resizeMode,
|
|
473
|
+
maxInputLongSide: request.maxInputLongSide,
|
|
474
|
+
referenceImagePaths: request.referenceImagePaths,
|
|
475
|
+
referenceInfluences: request.referenceInfluences,
|
|
476
|
+
loraPaths: request.loraPaths,
|
|
477
|
+
loraScales: request.loraScales,
|
|
478
|
+
loraIds: request.loraIds,
|
|
479
|
+
sigmaSchedule: request.sigmaSchedule,
|
|
480
|
+
width: request.width,
|
|
481
|
+
height: request.height,
|
|
482
|
+
steps: request.steps,
|
|
483
|
+
guidance: request.guidance,
|
|
484
|
+
strength: request.strength,
|
|
485
|
+
seed: request.seed,
|
|
486
|
+
inputIds: request.inputIds,
|
|
487
|
+
attentionMask: request.attentionMask,
|
|
488
|
+
modelRoot: request.modelRoot,
|
|
489
|
+
textEncoderPath: request.textEncoderPath,
|
|
490
|
+
transformerPath: request.transformerPath,
|
|
491
|
+
vaePath: request.vaePath,
|
|
492
|
+
stagingDirectoryPath: stagingDirectory.path
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private static func metadataDictionary(_ metadata: GenerationWorkerMetadata?) -> [String: Any]? {
|
|
497
|
+
guard let metadata else { return nil }
|
|
498
|
+
var value: [String: Any] = [
|
|
499
|
+
"model": metadata.model,
|
|
500
|
+
"width": metadata.width,
|
|
501
|
+
"height": metadata.height,
|
|
502
|
+
"steps": metadata.steps,
|
|
503
|
+
"guidance": metadata.guidance,
|
|
504
|
+
"elapsedMs": metadata.elapsedMs,
|
|
505
|
+
]
|
|
506
|
+
if let strength = metadata.strength { value["strength"] = strength }
|
|
507
|
+
if let seed = metadata.seed { value["seed"] = seed }
|
|
508
|
+
if let appliedLoRAs = metadata.appliedLoRAs {
|
|
509
|
+
value["appliedLoRAs"] = appliedLoRAs.map { lora in
|
|
510
|
+
var item: [String: Any] = [
|
|
511
|
+
"path": lora.path,
|
|
512
|
+
"scale": lora.scale,
|
|
513
|
+
]
|
|
514
|
+
if let id = lora.id { item["id"] = id }
|
|
515
|
+
return item
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if let sigmaSchedule = metadata.sigmaSchedule {
|
|
519
|
+
value["sigmaSchedule"] = sigmaSchedule
|
|
520
|
+
}
|
|
521
|
+
return value
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private static func rejectImageGeneration(_ error: Error, promise: Promise) {
|
|
525
|
+
if let failure = error as? GenerationWorkerFailure {
|
|
526
|
+
let message = failure.stage.map { "[\($0)] \(failure.message)" } ?? failure.message
|
|
527
|
+
promise.reject("IMAGE_GENERATION_ERROR", message)
|
|
528
|
+
return
|
|
529
|
+
}
|
|
530
|
+
promise.reject(error)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
#if canImport(FoundationModels)
|
|
534
|
+
@available(iOS 26, *)
|
|
535
|
+
private static func session(system: String?, tools: [any Tool]) -> LanguageModelSession {
|
|
536
|
+
if let system, !system.isEmpty {
|
|
537
|
+
return LanguageModelSession(tools: tools, instructions: system)
|
|
538
|
+
}
|
|
539
|
+
return LanguageModelSession(tools: tools)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
@available(iOS 26, *)
|
|
543
|
+
private static func tools(
|
|
544
|
+
from definitions: [ToolDefinitionRequest],
|
|
545
|
+
generationID: String,
|
|
546
|
+
coordinator: TextGenerationCoordinator,
|
|
547
|
+
emit: @escaping @MainActor @Sendable ([String: Any]) -> Void
|
|
548
|
+
) throws -> [any Tool] {
|
|
549
|
+
try definitions.map { definition in
|
|
550
|
+
let parameters = try generationSchema(from: definition.inputSchema)
|
|
551
|
+
return DynamicTool(
|
|
552
|
+
name: definition.name,
|
|
553
|
+
description: definition.description,
|
|
554
|
+
parameters: parameters,
|
|
555
|
+
generationID: generationID,
|
|
556
|
+
coordinator: coordinator,
|
|
557
|
+
emit: emit
|
|
558
|
+
)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
@available(iOS 26, *)
|
|
563
|
+
private static func generationOptions(from request: TextGenerationRequest) throws -> GenerationOptions {
|
|
564
|
+
return GenerationOptions(
|
|
565
|
+
sampling: request.temperature == nil ? .greedy : nil,
|
|
566
|
+
temperature: request.temperature,
|
|
567
|
+
maximumResponseTokens: request.maxTokens
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
@available(iOS 26, *)
|
|
572
|
+
private static func generationSchema(from schema: [String: Any]) throws -> GenerationSchema {
|
|
573
|
+
do {
|
|
574
|
+
return try GenerationSchema(root: dynamicSchema(from: schema), dependencies: [])
|
|
575
|
+
} catch {
|
|
576
|
+
throw ModuleError.invalidSchema(error.localizedDescription)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
@available(iOS 26, *)
|
|
581
|
+
private static func dynamicSchema(from schema: [String: Any]) throws -> DynamicGenerationSchema {
|
|
582
|
+
guard let type = schema["type"] as? String else {
|
|
583
|
+
throw ModuleError.invalidSchema("Missing schema type")
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
switch type {
|
|
587
|
+
case "object":
|
|
588
|
+
return try objectSchema(from: schema)
|
|
589
|
+
case "array":
|
|
590
|
+
guard let itemSchema = schema["items"] as? [String: Any] else {
|
|
591
|
+
throw ModuleError.invalidSchema("Array schema must include items")
|
|
592
|
+
}
|
|
593
|
+
return try DynamicGenerationSchema(arrayOf: dynamicSchema(from: itemSchema))
|
|
594
|
+
case "string":
|
|
595
|
+
return DynamicGenerationSchema(type: String.self)
|
|
596
|
+
case "number":
|
|
597
|
+
return DynamicGenerationSchema(type: Double.self)
|
|
598
|
+
case "boolean":
|
|
599
|
+
return DynamicGenerationSchema(type: Bool.self)
|
|
600
|
+
default:
|
|
601
|
+
throw ModuleError.invalidSchema("Unsupported schema type: \(type)")
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
@available(iOS 26, *)
|
|
606
|
+
private static func objectSchema(from schema: [String: Any]) throws -> DynamicGenerationSchema {
|
|
607
|
+
let propertiesDictionary = schema["properties"] as? [String: Any] ?? [:]
|
|
608
|
+
let required = Set(schema["required"] as? [String] ?? [])
|
|
609
|
+
var properties: [DynamicGenerationSchema.Property] = []
|
|
610
|
+
|
|
611
|
+
for key in propertiesDictionary.keys.sorted() {
|
|
612
|
+
guard let propertySchema = propertiesDictionary[key] as? [String: Any] else {
|
|
613
|
+
throw ModuleError.invalidSchema("Property \(key) schema must be an object")
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
properties.append(
|
|
617
|
+
DynamicGenerationSchema.Property(
|
|
618
|
+
name: key,
|
|
619
|
+
description: nil,
|
|
620
|
+
schema: try dynamicSchema(from: propertySchema),
|
|
621
|
+
isOptional: !required.contains(key)
|
|
622
|
+
)
|
|
623
|
+
)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return DynamicGenerationSchema(name: schema["title"] as? String ?? "Output", properties: properties)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
@available(iOS 26, *)
|
|
630
|
+
private static func value(from content: GeneratedContent, schema: [String: Any]) throws -> Any {
|
|
631
|
+
guard let type = schema["type"] as? String else {
|
|
632
|
+
throw ModuleError.invalidSchema("Missing schema type")
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
switch type {
|
|
636
|
+
case "object":
|
|
637
|
+
let propertiesDictionary = schema["properties"] as? [String: Any] ?? [:]
|
|
638
|
+
let output = NSMutableDictionary()
|
|
639
|
+
|
|
640
|
+
for key in propertiesDictionary.keys.sorted() {
|
|
641
|
+
guard let propertySchema = propertiesDictionary[key] as? [String: Any] else {
|
|
642
|
+
throw ModuleError.invalidSchema("Property \(key) schema must be an object")
|
|
643
|
+
}
|
|
644
|
+
output[key] = try propertyValue(from: content, property: key, schema: propertySchema)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return output
|
|
648
|
+
default:
|
|
649
|
+
throw ModuleError.invalidSchema("Root schema must be an object")
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
@available(iOS 26, *)
|
|
654
|
+
private static func propertyValue(
|
|
655
|
+
from content: GeneratedContent,
|
|
656
|
+
property: String,
|
|
657
|
+
schema: [String: Any]
|
|
658
|
+
) throws -> Any {
|
|
659
|
+
guard let type = schema["type"] as? String else {
|
|
660
|
+
throw ModuleError.invalidSchema("Missing schema type")
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
switch type {
|
|
664
|
+
case "string":
|
|
665
|
+
return try content.value(String.self, forProperty: property)
|
|
666
|
+
case "number":
|
|
667
|
+
return try content.value(Double.self, forProperty: property)
|
|
668
|
+
case "boolean":
|
|
669
|
+
return try content.value(Bool.self, forProperty: property)
|
|
670
|
+
case "object":
|
|
671
|
+
let nestedContent = try content.value(GeneratedContent.self, forProperty: property)
|
|
672
|
+
return try value(from: nestedContent, schema: schema)
|
|
673
|
+
case "array":
|
|
674
|
+
guard let itemSchema = schema["items"] as? [String: Any], let itemType = itemSchema["type"] as? String else {
|
|
675
|
+
throw ModuleError.invalidSchema("Array schema must include items")
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
switch itemType {
|
|
679
|
+
case "string":
|
|
680
|
+
return try content.value([String].self, forProperty: property)
|
|
681
|
+
case "number":
|
|
682
|
+
return try content.value([Double].self, forProperty: property)
|
|
683
|
+
case "boolean":
|
|
684
|
+
return try content.value([Bool].self, forProperty: property)
|
|
685
|
+
case "object":
|
|
686
|
+
let values = try content.value([GeneratedContent].self, forProperty: property)
|
|
687
|
+
return try values.map { try value(from: $0, schema: itemSchema) }
|
|
688
|
+
default:
|
|
689
|
+
throw ModuleError.invalidSchema("Unsupported array item type: \(itemType)")
|
|
690
|
+
}
|
|
691
|
+
default:
|
|
692
|
+
throw ModuleError.invalidSchema("Unsupported schema type: \(type)")
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
@available(iOS 26, *)
|
|
697
|
+
private static func reject(_ error: Error, promise: Promise) {
|
|
698
|
+
if error is CancellationError {
|
|
699
|
+
promise.reject("GENERATION_CANCELED", "Generation canceled")
|
|
700
|
+
return
|
|
701
|
+
}
|
|
702
|
+
if let generationError = error as? LanguageModelSession.GenerationError {
|
|
703
|
+
switch generationError {
|
|
704
|
+
case .exceededContextWindowSize:
|
|
705
|
+
promise.reject("CONTEXT_WINDOW_EXCEEDED", error.localizedDescription)
|
|
706
|
+
case .concurrentRequests:
|
|
707
|
+
promise.reject("CONCURRENT_REQUEST", error.localizedDescription)
|
|
708
|
+
case .guardrailViolation:
|
|
709
|
+
promise.reject("GUARDRAIL_VIOLATION", error.localizedDescription)
|
|
710
|
+
case .unsupportedLanguageOrLocale:
|
|
711
|
+
promise.reject("UNSUPPORTED_LANGUAGE_OR_LOCALE", error.localizedDescription)
|
|
712
|
+
case .rateLimited:
|
|
713
|
+
promise.reject("RATE_LIMITED", error.localizedDescription)
|
|
714
|
+
default:
|
|
715
|
+
promise.reject("GENERATION_ERROR", error.localizedDescription)
|
|
716
|
+
}
|
|
717
|
+
return
|
|
718
|
+
}
|
|
719
|
+
if let toolError = error as? LanguageModelSession.ToolCallError {
|
|
720
|
+
promise.reject("TOOL_ERROR", toolError.localizedDescription)
|
|
721
|
+
return
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
promise.reject(error)
|
|
725
|
+
}
|
|
726
|
+
#endif
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
#if canImport(FoundationModels)
|
|
730
|
+
@available(iOS 26, *)
|
|
731
|
+
private struct DynamicTool: Tool {
|
|
732
|
+
typealias Arguments = GeneratedContent
|
|
733
|
+
|
|
734
|
+
let name: String
|
|
735
|
+
let description: String
|
|
736
|
+
let parameters: GenerationSchema
|
|
737
|
+
let generationID: String
|
|
738
|
+
let coordinator: TextGenerationCoordinator
|
|
739
|
+
let emit: @MainActor @Sendable ([String: Any]) -> Void
|
|
740
|
+
|
|
741
|
+
func call(arguments: GeneratedContent) async throws -> String {
|
|
742
|
+
let data = Data(arguments.jsonString.utf8)
|
|
743
|
+
let value = try JSONSerialization.jsonObject(with: data)
|
|
744
|
+
return try await coordinator.call(
|
|
745
|
+
generationID: generationID,
|
|
746
|
+
name: name,
|
|
747
|
+
arguments: value,
|
|
748
|
+
emit: emit
|
|
749
|
+
)
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
#endif
|
|
753
|
+
|
|
754
|
+
private struct ToolExecutionError: LocalizedError {
|
|
755
|
+
let message: String
|
|
756
|
+
var errorDescription: String? { message }
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
@MainActor
|
|
760
|
+
private final class TextGenerationCoordinator {
|
|
761
|
+
private struct PendingToolCall {
|
|
762
|
+
let generationID: String
|
|
763
|
+
let continuation: CheckedContinuation<String, Error>
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
private var tasks: [String: Task<Void, Never>] = [:]
|
|
767
|
+
private var toolCalls: [String: PendingToolCall] = [:]
|
|
768
|
+
|
|
769
|
+
func start(_ id: String, task: Task<Void, Never>) {
|
|
770
|
+
tasks[id]?.cancel()
|
|
771
|
+
tasks[id] = task
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
func finish(_ id: String) {
|
|
775
|
+
tasks[id] = nil
|
|
776
|
+
failToolCalls(for: id, error: CancellationError())
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
func cancel(_ id: String) -> Bool {
|
|
780
|
+
guard let task = tasks.removeValue(forKey: id) else { return false }
|
|
781
|
+
task.cancel()
|
|
782
|
+
failToolCalls(for: id, error: CancellationError())
|
|
783
|
+
return true
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
func call(
|
|
787
|
+
generationID: String,
|
|
788
|
+
name: String,
|
|
789
|
+
arguments: Any,
|
|
790
|
+
emit: @escaping @MainActor @Sendable ([String: Any]) -> Void
|
|
791
|
+
) async throws -> String {
|
|
792
|
+
try Task.checkCancellation()
|
|
793
|
+
let callID = UUID().uuidString
|
|
794
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
795
|
+
toolCalls[callID] = PendingToolCall(generationID: generationID, continuation: continuation)
|
|
796
|
+
emit([
|
|
797
|
+
"generationId": generationID,
|
|
798
|
+
"callId": callID,
|
|
799
|
+
"name": name,
|
|
800
|
+
"arguments": arguments,
|
|
801
|
+
])
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
func resolve(_ request: ToolResultRequest) -> Bool {
|
|
806
|
+
guard let pending = toolCalls.removeValue(forKey: request.callId),
|
|
807
|
+
pending.generationID == request.generationId
|
|
808
|
+
else { return false }
|
|
809
|
+
|
|
810
|
+
if let error = request.error {
|
|
811
|
+
pending.continuation.resume(throwing: ToolExecutionError(message: error))
|
|
812
|
+
} else {
|
|
813
|
+
pending.continuation.resume(returning: request.output ?? "")
|
|
814
|
+
}
|
|
815
|
+
return true
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
private func failToolCalls(for generationID: String, error: Error) {
|
|
819
|
+
let matches = toolCalls.filter { $0.value.generationID == generationID }
|
|
820
|
+
for (id, pending) in matches {
|
|
821
|
+
toolCalls[id] = nil
|
|
822
|
+
pending.continuation.resume(throwing: error)
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
private enum ModuleError: Error, LocalizedError {
|
|
828
|
+
case invalidSchema(String)
|
|
829
|
+
case invalidMessage(String)
|
|
830
|
+
case invalidModel(String)
|
|
831
|
+
case imageGeneration(String)
|
|
832
|
+
|
|
833
|
+
var errorDescription: String? {
|
|
834
|
+
switch self {
|
|
835
|
+
case .invalidSchema(let message):
|
|
836
|
+
return "Invalid schema: \(message)"
|
|
837
|
+
case .invalidMessage(let message), .invalidModel(let message), .imageGeneration(let message):
|
|
838
|
+
return message
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
@MainActor
|
|
844
|
+
private final class ImageGenerationServiceClient {
|
|
845
|
+
static let shared = ImageGenerationServiceClient()
|
|
846
|
+
|
|
847
|
+
private static var serviceName: String {
|
|
848
|
+
"\(Bundle.main.bundleIdentifier ?? "dev.bleam.app").GenerationService"
|
|
849
|
+
}
|
|
850
|
+
private var connection: NSXPCConnection?
|
|
851
|
+
private var pendingRun: PendingRun?
|
|
852
|
+
private let eventReceiver = GenerationServiceEventReceiver()
|
|
853
|
+
|
|
854
|
+
private final class PendingRun {
|
|
855
|
+
let continuation: CheckedContinuation<GenerationWorkerEvent, Error>
|
|
856
|
+
let onProgress: (GenerationWorkerEvent) -> Void
|
|
857
|
+
|
|
858
|
+
init(
|
|
859
|
+
continuation: CheckedContinuation<GenerationWorkerEvent, Error>,
|
|
860
|
+
onProgress: @escaping (GenerationWorkerEvent) -> Void
|
|
861
|
+
) {
|
|
862
|
+
self.continuation = continuation
|
|
863
|
+
self.onProgress = onProgress
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
func run(
|
|
868
|
+
request: GenerationWorkerRequest,
|
|
869
|
+
onProgress: @escaping (GenerationWorkerEvent) -> Void = { _ in }
|
|
870
|
+
) async throws -> GenerationWorkerEvent {
|
|
871
|
+
let terminalEvent: GenerationWorkerEvent = try await withCheckedThrowingContinuation { continuation in
|
|
872
|
+
do {
|
|
873
|
+
guard pendingRun == nil else {
|
|
874
|
+
continuation.resume(throwing: GenerationServiceClientError.concurrentRequest)
|
|
875
|
+
return
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
eventReceiver.onEvent = { [weak self] event in
|
|
879
|
+
self?.deliver(event)
|
|
880
|
+
}
|
|
881
|
+
pendingRun = PendingRun(continuation: continuation, onProgress: onProgress)
|
|
882
|
+
|
|
883
|
+
let requestData = try JSONEncoder().encode(request)
|
|
884
|
+
let service = try remoteProxy(errorHandler: { [weak self] error in
|
|
885
|
+
Task { @MainActor in
|
|
886
|
+
self?.failPendingRun(Self.mapConnectionError(error))
|
|
887
|
+
}
|
|
888
|
+
})
|
|
889
|
+
service.startGeneration(requestData: requestData) { [weak self] accepted in
|
|
890
|
+
Task { @MainActor in
|
|
891
|
+
guard accepted else {
|
|
892
|
+
self?.failPendingRun(GenerationServiceClientError.serviceUnavailable)
|
|
893
|
+
return
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
} catch {
|
|
898
|
+
pendingRun = nil
|
|
899
|
+
eventReceiver.onEvent = nil
|
|
900
|
+
continuation.resume(throwing: error)
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
try handleTerminalEvent(terminalEvent)
|
|
905
|
+
return terminalEvent
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
func cancel() async throws -> Bool {
|
|
909
|
+
guard pendingRun != nil else {
|
|
910
|
+
return false
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
let service = try remoteProxy(errorHandler: { [weak self] error in
|
|
914
|
+
Task { @MainActor in
|
|
915
|
+
self?.failPendingRun(Self.mapConnectionError(error))
|
|
916
|
+
}
|
|
917
|
+
})
|
|
918
|
+
|
|
919
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
920
|
+
service.cancelGeneration { canceled in
|
|
921
|
+
continuation.resume(returning: canceled)
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
func memoryTelemetry() async throws -> [String: Any] {
|
|
927
|
+
let service = try remoteProxy(errorHandler: { _ in })
|
|
928
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
929
|
+
service.memoryTelemetry { data in
|
|
930
|
+
guard
|
|
931
|
+
let data,
|
|
932
|
+
let value = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
933
|
+
else {
|
|
934
|
+
continuation.resume(throwing: GenerationServiceClientError.serviceUnavailable)
|
|
935
|
+
return
|
|
936
|
+
}
|
|
937
|
+
continuation.resume(returning: value)
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
func unload() async throws -> Bool {
|
|
943
|
+
guard pendingRun == nil else { return false }
|
|
944
|
+
let service = try remoteProxy(errorHandler: { _ in })
|
|
945
|
+
let unloaded = try await withCheckedThrowingContinuation { continuation in
|
|
946
|
+
service.shutdown { accepted in
|
|
947
|
+
continuation.resume(returning: accepted)
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if unloaded {
|
|
951
|
+
connection?.invalidate()
|
|
952
|
+
connection = nil
|
|
953
|
+
}
|
|
954
|
+
return unloaded
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
private func remoteProxy(errorHandler: @escaping (Error) -> Void) throws -> GenerationServiceProtocol {
|
|
958
|
+
if connection == nil {
|
|
959
|
+
let nextConnection = try Self.makeConnection(serviceName: Self.serviceName)
|
|
960
|
+
nextConnection.remoteObjectInterface = NSXPCInterface(with: GenerationServiceProtocol.self)
|
|
961
|
+
nextConnection.exportedInterface = NSXPCInterface(with: GenerationServiceClientProtocol.self)
|
|
962
|
+
nextConnection.exportedObject = eventReceiver
|
|
963
|
+
nextConnection.invalidationHandler = { [weak self] in
|
|
964
|
+
Task { @MainActor in self?.handleConnectionLost() }
|
|
965
|
+
}
|
|
966
|
+
nextConnection.interruptionHandler = { [weak self] in
|
|
967
|
+
Task { @MainActor in self?.handleConnectionLost() }
|
|
968
|
+
}
|
|
969
|
+
nextConnection.resume()
|
|
970
|
+
connection = nextConnection
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
guard let service = connection?.remoteObjectProxyWithErrorHandler(errorHandler) as? GenerationServiceProtocol else {
|
|
974
|
+
throw GenerationServiceClientError.serviceUnavailable
|
|
975
|
+
}
|
|
976
|
+
return service
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
private static func makeConnection(serviceName: String) throws -> NSXPCConnection {
|
|
980
|
+
#if targetEnvironment(macCatalyst)
|
|
981
|
+
let connectionClass: AnyObject = NSXPCConnection.self as AnyObject
|
|
982
|
+
let allocSelector = NSSelectorFromString("alloc")
|
|
983
|
+
let initSelector = NSSelectorFromString("initWithServiceName:")
|
|
984
|
+
|
|
985
|
+
guard
|
|
986
|
+
let allocated = connectionClass.perform(allocSelector)?.takeUnretainedValue() as AnyObject?,
|
|
987
|
+
allocated.responds(to: initSelector),
|
|
988
|
+
let connection = allocated.perform(initSelector, with: serviceName)?.takeUnretainedValue() as? NSXPCConnection
|
|
989
|
+
else {
|
|
990
|
+
throw GenerationServiceClientError.serviceUnavailable
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return connection
|
|
994
|
+
#else
|
|
995
|
+
return NSXPCConnection(serviceName: serviceName)
|
|
996
|
+
#endif
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
private func deliver(_ event: GenerationWorkerEvent) {
|
|
1000
|
+
if event.type == .log {
|
|
1001
|
+
DevLogPoster.post(
|
|
1002
|
+
source: "xpc",
|
|
1003
|
+
level: event.level ?? "info",
|
|
1004
|
+
category: event.category,
|
|
1005
|
+
message: event.message ?? "",
|
|
1006
|
+
jobID: event.jobID
|
|
1007
|
+
)
|
|
1008
|
+
return
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
guard let pendingRun else {
|
|
1012
|
+
print("[GenerationServiceClient] event without pending request type=\(event.type.rawValue)")
|
|
1013
|
+
return
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if event.type == .progress {
|
|
1017
|
+
pendingRun.onProgress(event)
|
|
1018
|
+
return
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if event.type == .completed || event.type == .canceled || event.type == .failed {
|
|
1022
|
+
self.pendingRun = nil
|
|
1023
|
+
eventReceiver.onEvent = nil
|
|
1024
|
+
pendingRun.continuation.resume(returning: event)
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
private func failPendingRun(_ error: Error) {
|
|
1029
|
+
guard let pendingRun else { return }
|
|
1030
|
+
self.pendingRun = nil
|
|
1031
|
+
eventReceiver.onEvent = nil
|
|
1032
|
+
if GenerationServiceClientError.isServiceKilledInsufficientMemory(error) {
|
|
1033
|
+
invalidate()
|
|
1034
|
+
}
|
|
1035
|
+
pendingRun.continuation.resume(throwing: error)
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
private func handleConnectionLost() {
|
|
1039
|
+
connection = nil
|
|
1040
|
+
failPendingRun(GenerationServiceClientError.serviceInterrupted)
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
private func invalidate() {
|
|
1044
|
+
connection?.invalidate()
|
|
1045
|
+
connection = nil
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
private func handleTerminalEvent(_ event: GenerationWorkerEvent) throws {
|
|
1049
|
+
switch event.type {
|
|
1050
|
+
case .completed:
|
|
1051
|
+
return
|
|
1052
|
+
case .canceled:
|
|
1053
|
+
throw CancellationError()
|
|
1054
|
+
case .failed:
|
|
1055
|
+
throw GenerationWorkerFailure(message: event.message ?? "Generation failed.", stage: event.stage)
|
|
1056
|
+
default:
|
|
1057
|
+
throw GenerationServiceClientError.invalidResponse
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
private static func mapConnectionError(_ error: Error) -> Error {
|
|
1062
|
+
let nsError = error as NSError
|
|
1063
|
+
if nsError.domain == NSCocoaErrorDomain || nsError.domain.localizedCaseInsensitiveContains("xpc") {
|
|
1064
|
+
return GenerationServiceClientError.serviceInterrupted
|
|
1065
|
+
}
|
|
1066
|
+
return error
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
private enum DevLogPoster {
|
|
1071
|
+
static func post(source: String, level: String, category: String?, message: String, jobID: String? = nil) {
|
|
1072
|
+
Task.detached {
|
|
1073
|
+
guard let config = try? loadProjectConfig(), let devServer = config["devServer"] as? String else {
|
|
1074
|
+
return
|
|
1075
|
+
}
|
|
1076
|
+
guard let url = URL(string: "\(devServer)/bleam/logs") else {
|
|
1077
|
+
return
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
var payload: [String: Any] = [
|
|
1081
|
+
"source": source,
|
|
1082
|
+
"level": level,
|
|
1083
|
+
"message": message,
|
|
1084
|
+
]
|
|
1085
|
+
if let category { payload["category"] = category }
|
|
1086
|
+
if let jobID { payload["jobId"] = jobID }
|
|
1087
|
+
|
|
1088
|
+
guard let body = try? JSONSerialization.data(withJSONObject: payload) else {
|
|
1089
|
+
return
|
|
1090
|
+
}
|
|
1091
|
+
var request = URLRequest(url: url)
|
|
1092
|
+
request.httpMethod = "POST"
|
|
1093
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
1094
|
+
request.httpBody = body
|
|
1095
|
+
_ = try? await URLSession.shared.data(for: request)
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
private static func loadProjectConfig() throws -> [String: Any]? {
|
|
1100
|
+
guard let configPath = projectConfigPath() else {
|
|
1101
|
+
return nil
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
let data = try Data(contentsOf: URL(fileURLWithPath: configPath))
|
|
1105
|
+
return try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
private static func projectConfigPath() -> String? {
|
|
1109
|
+
let arguments = ProcessInfo.processInfo.arguments
|
|
1110
|
+
guard let flagIndex = arguments.firstIndex(of: "--bleam-project-config") else {
|
|
1111
|
+
return nil
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
let valueIndex = arguments.index(after: flagIndex)
|
|
1115
|
+
guard arguments.indices.contains(valueIndex) else {
|
|
1116
|
+
return nil
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return arguments[valueIndex]
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
private enum GenerationServiceClientError: LocalizedError, Equatable {
|
|
1124
|
+
case serviceUnavailable
|
|
1125
|
+
case invalidResponse
|
|
1126
|
+
case serviceInterrupted
|
|
1127
|
+
case serviceKilledInsufficientMemory
|
|
1128
|
+
case concurrentRequest
|
|
1129
|
+
|
|
1130
|
+
static func isServiceKilledInsufficientMemory(_ error: Error) -> Bool {
|
|
1131
|
+
guard let clientError = error as? GenerationServiceClientError else { return false }
|
|
1132
|
+
return clientError == .serviceKilledInsufficientMemory
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
var errorDescription: String? {
|
|
1136
|
+
switch self {
|
|
1137
|
+
case .serviceUnavailable:
|
|
1138
|
+
return "GenerationService is unavailable."
|
|
1139
|
+
case .invalidResponse:
|
|
1140
|
+
return "GenerationService returned an invalid response."
|
|
1141
|
+
case .serviceInterrupted:
|
|
1142
|
+
return "GenerationService was interrupted."
|
|
1143
|
+
case .serviceKilledInsufficientMemory:
|
|
1144
|
+
return "Generation failed because there was not enough available memory."
|
|
1145
|
+
case .concurrentRequest:
|
|
1146
|
+
return "GenerationService is already running a request."
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
private final class GenerationServiceEventReceiver: NSObject, GenerationServiceClientProtocol {
|
|
1152
|
+
@MainActor var onEvent: ((GenerationWorkerEvent) -> Void)?
|
|
1153
|
+
|
|
1154
|
+
func generationWorkerDidEmit(eventData: Data) {
|
|
1155
|
+
guard let event = try? JSONDecoder().decode(GenerationWorkerEvent.self, from: eventData) else { return }
|
|
1156
|
+
Task { @MainActor in
|
|
1157
|
+
onEvent?(event)
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
private struct TextGenerationRequest: Record {
|
|
1163
|
+
@Field var id: String?
|
|
1164
|
+
@Field var system: String?
|
|
1165
|
+
@Field var prompt: String = ""
|
|
1166
|
+
@Field var outputSchema: [String: Any]?
|
|
1167
|
+
@Field var temperature: Double?
|
|
1168
|
+
@Field var maxTokens: Int?
|
|
1169
|
+
@Field var tools: [ToolDefinitionRequest] = []
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
private struct ToolDefinitionRequest: Record {
|
|
1173
|
+
@Field var name: String = ""
|
|
1174
|
+
@Field var description: String = ""
|
|
1175
|
+
@Field var inputSchema: [String: Any] = [:]
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
private struct ToolResultRequest: Record {
|
|
1179
|
+
@Field var generationId: String = ""
|
|
1180
|
+
@Field var callId: String = ""
|
|
1181
|
+
@Field var output: String?
|
|
1182
|
+
@Field var error: String?
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
private struct ImageGenerationRequest: Record {
|
|
1186
|
+
@Field var model: String = ""
|
|
1187
|
+
@Field var prompt: String = ""
|
|
1188
|
+
@Field var sourceImagePath: String?
|
|
1189
|
+
@Field var maskImagePath: String?
|
|
1190
|
+
@Field var maskMode: String?
|
|
1191
|
+
@Field var outpaintWidth: Int?
|
|
1192
|
+
@Field var outpaintHeight: Int?
|
|
1193
|
+
@Field var outpaintSourceX: Int?
|
|
1194
|
+
@Field var outpaintSourceY: Int?
|
|
1195
|
+
@Field var outpaintSourceWidth: Int?
|
|
1196
|
+
@Field var outpaintSourceHeight: Int?
|
|
1197
|
+
@Field var outpaintAutoCrop: Bool?
|
|
1198
|
+
@Field var resizeMode: String?
|
|
1199
|
+
@Field var maxInputLongSide: Int?
|
|
1200
|
+
@Field var referenceImagePaths: [String] = []
|
|
1201
|
+
@Field var referenceInfluences: [Double]?
|
|
1202
|
+
@Field var loraPaths: [String] = []
|
|
1203
|
+
@Field var loraScales: [Double]?
|
|
1204
|
+
@Field var loraIds: [String]?
|
|
1205
|
+
@Field var sigmaSchedule: [Double]?
|
|
1206
|
+
@Field var width: Int = 0
|
|
1207
|
+
@Field var height: Int = 0
|
|
1208
|
+
@Field var steps: Int = 0
|
|
1209
|
+
@Field var guidance: Double = 0
|
|
1210
|
+
@Field var strength: Double?
|
|
1211
|
+
@Field var seed: Int?
|
|
1212
|
+
@Field var inputIds: [Int] = []
|
|
1213
|
+
@Field var attentionMask: [Int] = []
|
|
1214
|
+
@Field var modelRoot: String = ""
|
|
1215
|
+
@Field var textEncoderPath: String = ""
|
|
1216
|
+
@Field var transformerPath: String = ""
|
|
1217
|
+
@Field var vaePath: String = ""
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
private struct UpscaleImageRequest: Record {
|
|
1221
|
+
@Field var imagePath: String = ""
|
|
1222
|
+
@Field var scale: Double = 2
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
private struct FluxImageRequest {
|
|
1226
|
+
let model: String
|
|
1227
|
+
let prompt: String
|
|
1228
|
+
let sourceImagePath: String?
|
|
1229
|
+
let maskImagePath: String?
|
|
1230
|
+
let maskMode: String?
|
|
1231
|
+
let outpaintWidth: Int?
|
|
1232
|
+
let outpaintHeight: Int?
|
|
1233
|
+
let outpaintSourceX: Int?
|
|
1234
|
+
let outpaintSourceY: Int?
|
|
1235
|
+
let outpaintSourceWidth: Int?
|
|
1236
|
+
let outpaintSourceHeight: Int?
|
|
1237
|
+
let outpaintAutoCrop: Bool?
|
|
1238
|
+
let resizeMode: String?
|
|
1239
|
+
let maxInputLongSide: Int?
|
|
1240
|
+
let referenceImagePaths: [String]
|
|
1241
|
+
let referenceInfluences: [Double]?
|
|
1242
|
+
let loraPaths: [String]
|
|
1243
|
+
let loraScales: [Double]?
|
|
1244
|
+
let loraIds: [String]?
|
|
1245
|
+
let sigmaSchedule: [Double]?
|
|
1246
|
+
let width: Int
|
|
1247
|
+
let height: Int
|
|
1248
|
+
let steps: Int
|
|
1249
|
+
let guidance: Double
|
|
1250
|
+
let strength: Double?
|
|
1251
|
+
let seed: Int?
|
|
1252
|
+
let inputIds: [Int]
|
|
1253
|
+
let attentionMask: [Int]
|
|
1254
|
+
let modelRoot: String
|
|
1255
|
+
let textEncoderPath: String
|
|
1256
|
+
let transformerPath: String
|
|
1257
|
+
let vaePath: String
|
|
1258
|
+
}
|