bleam 0.0.9 → 0.0.11
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/ai.cjs +608 -59
- package/dist/ai.d.cts +217 -89
- package/dist/ai.d.ts +217 -89
- package/dist/ai.js +601 -59
- package/dist/cli.cjs +142 -94
- package/dist/cli.d.cts +11 -27
- package/dist/cli.d.ts +11 -27
- package/dist/cli.js +143 -95
- package/dist/files.d.cts +1 -1
- package/dist/files.d.ts +1 -1
- package/dist/platform.cjs +1 -13
- package/dist/platform.d.cts +1 -26
- package/dist/platform.d.ts +1 -26
- package/dist/platform.js +1 -10
- package/dist/schema.d.cts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/state.d.cts +1 -1
- package/dist/state.d.ts +1 -1
- package/dist/{ui-Bg11tvlc.d.ts → ui-CHc4xEs_.d.ts} +7 -7
- package/dist/ui.d.ts +1 -1
- package/dist/window.d.ts +1 -1
- package/package.json +2 -1
- package/templates/image-generation/app/index.tsx +3 -1
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +1 -1
- package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +7 -2
- package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextGenerator.swift +46 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextModel.swift +107 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextSnapshotValidator.swift +201 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/GenerationTypes.swift +85 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/ModelCapabilities.swift +21 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/TextKVCache.swift +23 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/TokenSampler.swift +79 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3CheckpointLoader.swift +161 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Configuration.swift +119 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3InferenceSession.swift +84 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Layers.swift +197 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Model.swift +130 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Checkpoint.swift +102 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Configuration.swift +140 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Model.swift +112 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Session.swift +121 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridCache.swift +42 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridFullAttention.swift +57 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLayers.swift +58 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLinearAttention.swift +111 -0
- package/templates/native/ios/Bleam/AppDelegate.swift +0 -97
- package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +128 -121
- package/templates/native/ios/GenerationService/BonsaiTextGenerationRunner.swift +103 -0
- package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +19 -7
- package/templates/native/ios/GenerationService/main.swift +174 -48
- package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +3 -0
- package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +44 -0
- package/templates/native/modules/bleam-runtime/ios/AIModule.swift +225 -5
- package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +47 -0
- package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +0 -175
- package/templates/text-generation/app/index.tsx +93 -0
- package/templates/text-generation/app.config.ts +5 -0
- package/templates/updates/README.md +116 -0
- package/templates/updates/src/index.ts +292 -0
- package/templates/updates/src/schema.ts +230 -0
- package/templates/updates/tsconfig.json +12 -0
- package/templates/updates/wrangler.jsonc +19 -0
- package/templates/native/ios/PlatformHelper/Info.plist +0 -29
- package/templates/native/ios/PlatformHelper/main.swift +0 -335
- /package/dist/{files-DwA7pzr3.d.cts → files-4ZEoAWiv.d.ts} +0 -0
- /package/dist/{files-VrkQlKIT.d.ts → files-Dt5mbzLq.d.cts} +0 -0
- /package/dist/{schema-DOOjfXvs.d.ts → schema-CYh6n8GS.d.ts} +0 -0
- /package/dist/{schema-ENSMX_1t.d.cts → schema-oeOrd3i1.d.cts} +0 -0
package/dist/ai.js
CHANGED
|
@@ -7,6 +7,53 @@ import { requireNativeModule } from "expo";
|
|
|
7
7
|
import { useRef, useState } from "react";
|
|
8
8
|
import { Image } from "react-native";
|
|
9
9
|
|
|
10
|
+
//#region src/ai/bonsai.ts
|
|
11
|
+
const internalTextModelCatalog = [
|
|
12
|
+
{
|
|
13
|
+
id: "ternary-bonsai-1.7b",
|
|
14
|
+
repo: "prism-ml/Ternary-Bonsai-1.7B-mlx-2bit",
|
|
15
|
+
revision: "5f3e306330f636cfc6c6241b4850fae6711c5985",
|
|
16
|
+
estimatedDownloadBytes: 495528947,
|
|
17
|
+
contextLength: 32768,
|
|
18
|
+
architecture: "qwen3"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "ternary-bonsai-4b",
|
|
22
|
+
repo: "prism-ml/Ternary-Bonsai-4B-mlx-2bit",
|
|
23
|
+
revision: "e1374ad6bf9b1b56afd743936b8faa33c409a75f",
|
|
24
|
+
estimatedDownloadBytes: 1143060044,
|
|
25
|
+
contextLength: 32768,
|
|
26
|
+
architecture: "qwen3"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "ternary-bonsai-8b",
|
|
30
|
+
repo: "prism-ml/Ternary-Bonsai-8B-mlx-2bit",
|
|
31
|
+
revision: "9260b24298e4211e804663e9f519962cf59f34be",
|
|
32
|
+
estimatedDownloadBytes: 2315155948,
|
|
33
|
+
contextLength: 65536,
|
|
34
|
+
architecture: "qwen3"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "ternary-bonsai-27b",
|
|
38
|
+
repo: "prism-ml/Ternary-Bonsai-27B-mlx-2bit",
|
|
39
|
+
revision: "70f75f3ad081ab840a42f3304c02c27e7f89bfb7",
|
|
40
|
+
estimatedDownloadBytes: 8510972679,
|
|
41
|
+
contextLength: 262144,
|
|
42
|
+
architecture: "qwen3.5"
|
|
43
|
+
}
|
|
44
|
+
];
|
|
45
|
+
const bonsaiTextModelCatalog = Object.freeze(internalTextModelCatalog.map(({ architecture: _,...metadata }) => Object.freeze(metadata)));
|
|
46
|
+
function bonsai(id) {
|
|
47
|
+
const text = internalTextModelCatalog.some((model) => model.id === id);
|
|
48
|
+
if (!text && id !== "ternary-bonsai-image-4b") throw new TypeError(`Unsupported Bonsai model: ${id}`);
|
|
49
|
+
return Object.freeze({
|
|
50
|
+
provider: "bonsai",
|
|
51
|
+
kind: text ? "text" : "image",
|
|
52
|
+
id
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
10
57
|
//#region src/ai/errors.ts
|
|
11
58
|
var AIError = class extends Error {
|
|
12
59
|
constructor(code, message, cause) {
|
|
@@ -46,6 +93,217 @@ function mapNativeError(error) {
|
|
|
46
93
|
return new AIError("generation_failed", message, error);
|
|
47
94
|
}
|
|
48
95
|
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/ai/bonsai-text.ts
|
|
98
|
+
const bonsaiTextModelFiles = [
|
|
99
|
+
"config.json",
|
|
100
|
+
"tokenizer.json",
|
|
101
|
+
"tokenizer_config.json",
|
|
102
|
+
"chat_template.jinja",
|
|
103
|
+
"model.safetensors",
|
|
104
|
+
"model.safetensors.index.json"
|
|
105
|
+
];
|
|
106
|
+
function modelId(model) {
|
|
107
|
+
return typeof model === "string" ? model : model.id;
|
|
108
|
+
}
|
|
109
|
+
function metadataFor(model) {
|
|
110
|
+
const id = modelId(model);
|
|
111
|
+
const metadata = bonsaiTextModelCatalog.find((candidate) => candidate.id === id);
|
|
112
|
+
if (!metadata) throw new AIError("invalid_model", `Unsupported Bonsai text model: ${id}`);
|
|
113
|
+
return metadata;
|
|
114
|
+
}
|
|
115
|
+
function bonsaiTextModelDirectory(model) {
|
|
116
|
+
return files.directory(files.paths.models, "bonsai", "text", modelId(model));
|
|
117
|
+
}
|
|
118
|
+
function modelDirectory(model, override) {
|
|
119
|
+
return override ? files.directory(override) : bonsaiTextModelDirectory(model);
|
|
120
|
+
}
|
|
121
|
+
function modelDirectoryIn(model, directory) {
|
|
122
|
+
return directory ? files.directory(directory, modelId(model)) : bonsaiTextModelDirectory(model);
|
|
123
|
+
}
|
|
124
|
+
function resolveUrl(repo, revision, file) {
|
|
125
|
+
return `https://huggingface.co/${repo.split("/").map(encodeURIComponent).join("/")}/resolve/${encodeURIComponent(revision)}/${encodeURIComponent(file)}`;
|
|
126
|
+
}
|
|
127
|
+
function throwIfAborted$1(signal) {
|
|
128
|
+
if (signal?.aborted) throw new AIError("generation_canceled", "Bonsai model preparation canceled");
|
|
129
|
+
}
|
|
130
|
+
const fileSizes = {
|
|
131
|
+
"ternary-bonsai-1.7b": {
|
|
132
|
+
"config.json": 2939,
|
|
133
|
+
"tokenizer.json": 11422650,
|
|
134
|
+
"tokenizer_config.json": 348,
|
|
135
|
+
"chat_template.jinja": 4063,
|
|
136
|
+
"model.safetensors": 484049216,
|
|
137
|
+
"model.safetensors.index.json": 49731
|
|
138
|
+
},
|
|
139
|
+
"ternary-bonsai-4b": {
|
|
140
|
+
"config.json": 3115,
|
|
141
|
+
"tokenizer.json": 11422650,
|
|
142
|
+
"tokenizer_config.json": 348,
|
|
143
|
+
"chat_template.jinja": 4063,
|
|
144
|
+
"model.safetensors": 1131565944,
|
|
145
|
+
"model.safetensors.index.json": 63924
|
|
146
|
+
},
|
|
147
|
+
"ternary-bonsai-8b": {
|
|
148
|
+
"config.json": 3118,
|
|
149
|
+
"tokenizer.json": 11422650,
|
|
150
|
+
"tokenizer_config.json": 348,
|
|
151
|
+
"chat_template.jinja": 4063,
|
|
152
|
+
"model.safetensors": 2303661704,
|
|
153
|
+
"model.safetensors.index.json": 64065
|
|
154
|
+
},
|
|
155
|
+
"ternary-bonsai-27b": {
|
|
156
|
+
"config.json": 3790,
|
|
157
|
+
"tokenizer.json": 19989343,
|
|
158
|
+
"tokenizer_config.json": 1098,
|
|
159
|
+
"chat_template.jinja": 7764,
|
|
160
|
+
"model.safetensors": 8490785104,
|
|
161
|
+
"model.safetensors.index.json": 185580
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
function validFile(model, file, size) {
|
|
165
|
+
return size === fileSizes[model][file];
|
|
166
|
+
}
|
|
167
|
+
async function stateFor(model, rootOverride) {
|
|
168
|
+
const metadata = metadataFor(model);
|
|
169
|
+
const root = modelDirectory(model, rootOverride);
|
|
170
|
+
const missingFiles = [];
|
|
171
|
+
const invalidFiles = [];
|
|
172
|
+
let bytesPresent = 0;
|
|
173
|
+
for (const file of bonsaiTextModelFiles) {
|
|
174
|
+
const stat = await root.file(file).stat();
|
|
175
|
+
if (stat.type !== "file") missingFiles.push(file);
|
|
176
|
+
else {
|
|
177
|
+
bytesPresent += stat.size ?? 0;
|
|
178
|
+
if (!validFile(metadata.id, file, stat.size)) invalidFiles.push(file);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const expectedRevision = metadata.revision;
|
|
182
|
+
const manifestFile = root.file("manifest.json");
|
|
183
|
+
if ((await manifestFile.stat()).type !== "file") missingFiles.push("manifest.json");
|
|
184
|
+
else try {
|
|
185
|
+
const manifest = await manifestFile.json();
|
|
186
|
+
if (!isRecord(manifest) || manifest.formatVersion !== 1 || manifest.model !== metadata.id || manifest.revision !== expectedRevision) invalidFiles.push("manifest.json");
|
|
187
|
+
} catch {
|
|
188
|
+
invalidFiles.push("manifest.json");
|
|
189
|
+
}
|
|
190
|
+
if (!missingFiles.includes("config.json")) try {
|
|
191
|
+
const config = await root.file("config.json").json();
|
|
192
|
+
if (!validConfig(metadata.id, config)) invalidFiles.push("config.json");
|
|
193
|
+
} catch {
|
|
194
|
+
invalidFiles.push("config.json");
|
|
195
|
+
}
|
|
196
|
+
const installed = missingFiles.length === 0;
|
|
197
|
+
return {
|
|
198
|
+
model: bonsai(metadata.id),
|
|
199
|
+
metadata,
|
|
200
|
+
path: root.path,
|
|
201
|
+
installed,
|
|
202
|
+
verified: installed && invalidFiles.length === 0,
|
|
203
|
+
bytesPresent,
|
|
204
|
+
estimatedTotalBytes: metadata.estimatedDownloadBytes,
|
|
205
|
+
missingFiles,
|
|
206
|
+
invalidFiles
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function validConfig(model, value) {
|
|
210
|
+
if (!isRecord(value)) return false;
|
|
211
|
+
const expected = {
|
|
212
|
+
"ternary-bonsai-1.7b": [
|
|
213
|
+
2048,
|
|
214
|
+
28,
|
|
215
|
+
32768
|
|
216
|
+
],
|
|
217
|
+
"ternary-bonsai-4b": [
|
|
218
|
+
2560,
|
|
219
|
+
36,
|
|
220
|
+
32768
|
|
221
|
+
],
|
|
222
|
+
"ternary-bonsai-8b": [
|
|
223
|
+
4096,
|
|
224
|
+
36,
|
|
225
|
+
65536
|
|
226
|
+
],
|
|
227
|
+
"ternary-bonsai-27b": [
|
|
228
|
+
5120,
|
|
229
|
+
64,
|
|
230
|
+
262144
|
|
231
|
+
]
|
|
232
|
+
}[model];
|
|
233
|
+
const config = model === "ternary-bonsai-27b" ? value.text_config : value;
|
|
234
|
+
if (!isRecord(config)) return false;
|
|
235
|
+
const quantization = config.quantization;
|
|
236
|
+
return isRecord(quantization) && quantization.bits === 2 && quantization.group_size === 128 && config.hidden_size === expected[0] && config.num_hidden_layers === expected[1] && config.max_position_embeddings === expected[2];
|
|
237
|
+
}
|
|
238
|
+
async function prepareBonsaiTextModel(model, options = {}) {
|
|
239
|
+
const metadata = metadataFor(model);
|
|
240
|
+
const root = modelDirectoryIn(model, options.directory);
|
|
241
|
+
throwIfAborted$1(options.signal);
|
|
242
|
+
await root.create({ recursive: true });
|
|
243
|
+
const headers = options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : void 0;
|
|
244
|
+
const completedBytes = /* @__PURE__ */ new Map();
|
|
245
|
+
const emit = (file, index, progress) => {
|
|
246
|
+
completedBytes.set(file, progress.bytesWritten);
|
|
247
|
+
const modelBytesWritten = Array.from(completedBytes.values()).reduce((total, bytes) => total + bytes, 0);
|
|
248
|
+
options.onProgress?.({
|
|
249
|
+
model: metadata.id,
|
|
250
|
+
file,
|
|
251
|
+
index,
|
|
252
|
+
total: bonsaiTextModelFiles.length,
|
|
253
|
+
...progress,
|
|
254
|
+
modelBytesWritten,
|
|
255
|
+
estimatedModelBytes: metadata.estimatedDownloadBytes,
|
|
256
|
+
modelFraction: Math.min(1, modelBytesWritten / metadata.estimatedDownloadBytes)
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
for (const [fileIndex, file] of bonsaiTextModelFiles.entries()) {
|
|
260
|
+
throwIfAborted$1(options.signal);
|
|
261
|
+
const index = fileIndex + 1;
|
|
262
|
+
const destination = root.file(file);
|
|
263
|
+
const expectedBytes = fileSizes[metadata.id][file];
|
|
264
|
+
const stat = await destination.stat();
|
|
265
|
+
if (stat.type === "file" && validFile(metadata.id, file, stat.size)) {
|
|
266
|
+
emit(file, index, {
|
|
267
|
+
bytesWritten: stat.size ?? 0,
|
|
268
|
+
totalBytes: stat.size,
|
|
269
|
+
fraction: 1
|
|
270
|
+
});
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
await destination.download(resolveUrl(metadata.repo, metadata.revision, file), {
|
|
274
|
+
headers,
|
|
275
|
+
expectedBytes,
|
|
276
|
+
atomic: true,
|
|
277
|
+
resume: true,
|
|
278
|
+
signal: options.signal,
|
|
279
|
+
onProgress(progress) {
|
|
280
|
+
emit(file, index, progress);
|
|
281
|
+
}
|
|
282
|
+
}).result;
|
|
283
|
+
throwIfAborted$1(options.signal);
|
|
284
|
+
}
|
|
285
|
+
await root.file("manifest.json").write(JSON.stringify({
|
|
286
|
+
formatVersion: 1,
|
|
287
|
+
model: metadata.id,
|
|
288
|
+
revision: metadata.revision
|
|
289
|
+
}));
|
|
290
|
+
const state = await stateFor(metadata.id, root.path);
|
|
291
|
+
if (!state.verified) throw new AIError("invalid_model", `Downloaded Bonsai model is incomplete: ${[...state.missingFiles, ...state.invalidFiles].join(", ")}`);
|
|
292
|
+
return {
|
|
293
|
+
model: state.model,
|
|
294
|
+
path: root.path
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function verifyBonsaiTextModel(model, options = {}) {
|
|
298
|
+
return stateFor(model, options.path);
|
|
299
|
+
}
|
|
300
|
+
function listBonsaiTextModels(options = {}) {
|
|
301
|
+
return Promise.all(bonsaiTextModelCatalog.map((metadata) => stateFor(metadata.id, options.directory ? files.directory(options.directory, metadata.id).path : void 0)));
|
|
302
|
+
}
|
|
303
|
+
async function deleteBonsaiTextModel(model, options = {}) {
|
|
304
|
+
await modelDirectory(model, options.path).delete({ recursive: true });
|
|
305
|
+
}
|
|
306
|
+
|
|
49
307
|
//#endregion
|
|
50
308
|
//#region src/ai/image-batch.ts
|
|
51
309
|
function batchImageSeeds(count, seed) {
|
|
@@ -243,7 +501,51 @@ const flux2Klein4B8BitAbliterated = {
|
|
|
243
501
|
}
|
|
244
502
|
}
|
|
245
503
|
};
|
|
246
|
-
const
|
|
504
|
+
const ternaryBonsaiImage4B = {
|
|
505
|
+
formatVersion: 1,
|
|
506
|
+
model: "ternary-bonsai-image-4b",
|
|
507
|
+
directory: "ternary-bonsai-image-4b",
|
|
508
|
+
metadata: {
|
|
509
|
+
displayName: "Ternary Bonsai Image 4B",
|
|
510
|
+
family: "bonsai-image",
|
|
511
|
+
precision: "2-bit",
|
|
512
|
+
estimatedMemoryGB: 8,
|
|
513
|
+
quality: "high",
|
|
514
|
+
speed: "fast",
|
|
515
|
+
capabilities: ["text-to-image"]
|
|
516
|
+
},
|
|
517
|
+
components: {
|
|
518
|
+
textEncoder: {
|
|
519
|
+
repo: "prism-ml/bonsai-image-ternary-4B-mlx-2bit",
|
|
520
|
+
revision: "2c24c81b934a658ba5590cf39088ba929985b4a8",
|
|
521
|
+
path: "text-encoder",
|
|
522
|
+
files: [
|
|
523
|
+
"text_encoder-mlx-4bit/config.json",
|
|
524
|
+
"text_encoder-mlx-4bit/tokenizer.json",
|
|
525
|
+
"text_encoder-mlx-4bit/tokenizer_config.json",
|
|
526
|
+
"text_encoder-mlx-4bit/model.safetensors",
|
|
527
|
+
"text_encoder-mlx-4bit/model.safetensors.index.json"
|
|
528
|
+
]
|
|
529
|
+
},
|
|
530
|
+
transformer: {
|
|
531
|
+
repo: "prism-ml/bonsai-image-ternary-4B-mlx-2bit",
|
|
532
|
+
revision: "2c24c81b934a658ba5590cf39088ba929985b4a8",
|
|
533
|
+
path: "transformer",
|
|
534
|
+
files: [
|
|
535
|
+
"transformer-packed-mflux/config.json",
|
|
536
|
+
"transformer-packed-mflux/diffusion_pytorch_model.safetensors",
|
|
537
|
+
"transformer-packed-mflux/quantization_config.json"
|
|
538
|
+
]
|
|
539
|
+
},
|
|
540
|
+
vae: {
|
|
541
|
+
repo: "prism-ml/bonsai-image-ternary-4B-mlx-2bit",
|
|
542
|
+
revision: "2c24c81b934a658ba5590cf39088ba929985b4a8",
|
|
543
|
+
path: "vae",
|
|
544
|
+
files: ["vae/config.json", "vae/diffusion_pytorch_model.safetensors"]
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
const imageModelManifests = [flux2Klein4B8BitAbliterated, ternaryBonsaiImage4B];
|
|
247
549
|
function imageModelManifest(model) {
|
|
248
550
|
return imageModelManifests.find((manifest) => manifest.model === model);
|
|
249
551
|
}
|
|
@@ -313,15 +615,90 @@ const flux2KleinModelFiles = [
|
|
|
313
615
|
}
|
|
314
616
|
];
|
|
315
617
|
const flux2KleinTotalBytes = flux2KleinModelFiles.reduce((total, file) => total + file.bytes, 0);
|
|
618
|
+
const bonsaiRepo = ternaryBonsaiImage4B.components.textEncoder.repo;
|
|
619
|
+
const bonsaiRevision = ternaryBonsaiImage4B.components.textEncoder.revision;
|
|
620
|
+
const ternaryBonsaiImageModelFiles = [
|
|
621
|
+
{
|
|
622
|
+
repo: bonsaiRepo,
|
|
623
|
+
revision: bonsaiRevision,
|
|
624
|
+
source: "text_encoder-mlx-4bit/config.json",
|
|
625
|
+
destination: "text-encoder/config.json",
|
|
626
|
+
bytes: 937
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
repo: bonsaiRepo,
|
|
630
|
+
revision: bonsaiRevision,
|
|
631
|
+
source: "text_encoder-mlx-4bit/tokenizer.json",
|
|
632
|
+
destination: "text-encoder/tokenizer.json",
|
|
633
|
+
bytes: 11422654
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
repo: bonsaiRepo,
|
|
637
|
+
revision: bonsaiRevision,
|
|
638
|
+
source: "text_encoder-mlx-4bit/tokenizer_config.json",
|
|
639
|
+
destination: "text-encoder/tokenizer_config.json",
|
|
640
|
+
bytes: 9706
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
repo: bonsaiRepo,
|
|
644
|
+
revision: bonsaiRevision,
|
|
645
|
+
source: "text_encoder-mlx-4bit/model.safetensors",
|
|
646
|
+
destination: "text-encoder/model.safetensors",
|
|
647
|
+
bytes: 2263022529
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
repo: bonsaiRepo,
|
|
651
|
+
revision: bonsaiRevision,
|
|
652
|
+
source: "text_encoder-mlx-4bit/model.safetensors.index.json",
|
|
653
|
+
destination: "text-encoder/model.safetensors.index.json",
|
|
654
|
+
bytes: 63924
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
repo: bonsaiRepo,
|
|
658
|
+
revision: bonsaiRevision,
|
|
659
|
+
source: "transformer-packed-mflux/config.json",
|
|
660
|
+
destination: "transformer/config.json",
|
|
661
|
+
bytes: 619
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
repo: bonsaiRepo,
|
|
665
|
+
revision: bonsaiRevision,
|
|
666
|
+
source: "transformer-packed-mflux/diffusion_pytorch_model.safetensors",
|
|
667
|
+
destination: "transformer/diffusion_pytorch_model.safetensors",
|
|
668
|
+
bytes: 1425271472
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
repo: bonsaiRepo,
|
|
672
|
+
revision: bonsaiRevision,
|
|
673
|
+
source: "transformer-packed-mflux/quantization_config.json",
|
|
674
|
+
destination: "transformer/quantization_config.json",
|
|
675
|
+
bytes: 5054
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
repo: bonsaiRepo,
|
|
679
|
+
revision: bonsaiRevision,
|
|
680
|
+
source: "vae/config.json",
|
|
681
|
+
destination: "vae/config.json",
|
|
682
|
+
bytes: 821
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
repo: bonsaiRepo,
|
|
686
|
+
revision: bonsaiRevision,
|
|
687
|
+
source: "vae/diffusion_pytorch_model.safetensors",
|
|
688
|
+
destination: "vae/diffusion_pytorch_model.safetensors",
|
|
689
|
+
bytes: 168120878
|
|
690
|
+
}
|
|
691
|
+
];
|
|
692
|
+
const ternaryBonsaiImageTotalBytes = ternaryBonsaiImageModelFiles.reduce((total, file) => total + file.bytes, 0);
|
|
316
693
|
function joinModelPath(root, component) {
|
|
317
694
|
return `${root.replace(/\/+$/, "")}/${component.replace(/^\/+/, "")}`;
|
|
318
695
|
}
|
|
319
|
-
function fluxImageModelPaths(
|
|
696
|
+
function fluxImageModelPaths(path, manifest = flux2Klein4B8BitAbliterated) {
|
|
320
697
|
return {
|
|
321
|
-
|
|
322
|
-
textEncoderPath: joinModelPath(
|
|
323
|
-
transformerPath: joinModelPath(
|
|
324
|
-
vaePath: joinModelPath(
|
|
698
|
+
path,
|
|
699
|
+
textEncoderPath: joinModelPath(path, manifest.components.textEncoder.path),
|
|
700
|
+
transformerPath: joinModelPath(path, manifest.components.transformer.path),
|
|
701
|
+
vaePath: joinModelPath(path, manifest.components.vae.path)
|
|
325
702
|
};
|
|
326
703
|
}
|
|
327
704
|
|
|
@@ -2420,14 +2797,31 @@ var Tokenizer_default = Tokenizer;
|
|
|
2420
2797
|
|
|
2421
2798
|
//#endregion
|
|
2422
2799
|
//#region src/ai/image.ts
|
|
2423
|
-
function
|
|
2800
|
+
function imageModelId(model) {
|
|
2801
|
+
return typeof model === "string" ? model : model.id;
|
|
2802
|
+
}
|
|
2803
|
+
function imageModelDefinition(model) {
|
|
2804
|
+
const id = imageModelId(model);
|
|
2805
|
+
if (id === flux2Klein4B8BitAbliterated.model) return {
|
|
2806
|
+
manifest: flux2Klein4B8BitAbliterated,
|
|
2807
|
+
files: flux2KleinModelFiles,
|
|
2808
|
+
totalBytes: flux2KleinTotalBytes
|
|
2809
|
+
};
|
|
2810
|
+
if (id === ternaryBonsaiImage4B.model) return {
|
|
2811
|
+
manifest: ternaryBonsaiImage4B,
|
|
2812
|
+
files: ternaryBonsaiImageModelFiles,
|
|
2813
|
+
totalBytes: ternaryBonsaiImageTotalBytes
|
|
2814
|
+
};
|
|
2815
|
+
throw new AIError("invalid_model", `Unsupported image model: ${id}`);
|
|
2816
|
+
}
|
|
2817
|
+
function modelCacheRoot(manifest) {
|
|
2424
2818
|
return files.directory(files.paths.models, manifest.directory);
|
|
2425
2819
|
}
|
|
2426
2820
|
function assertImageModel(model) {
|
|
2427
|
-
|
|
2821
|
+
imageModelDefinition(model);
|
|
2428
2822
|
}
|
|
2429
|
-
function
|
|
2430
|
-
return
|
|
2823
|
+
function modelPathFromOptions(definition, path) {
|
|
2824
|
+
return path ?? modelCacheRoot(definition.manifest).path;
|
|
2431
2825
|
}
|
|
2432
2826
|
function downloadConcurrency(value) {
|
|
2433
2827
|
if (value === void 0) return 2;
|
|
@@ -2594,11 +2988,11 @@ function cachedLoRAFile(lora) {
|
|
|
2594
2988
|
return imageLoRARoot().directory(id).file(safeCacheName(sourceFileName(lora), "lora filename"));
|
|
2595
2989
|
}
|
|
2596
2990
|
async function ensureModelFile(input) {
|
|
2597
|
-
const destination = files.file(input.
|
|
2991
|
+
const destination = files.file(input.path, input.file.destination);
|
|
2598
2992
|
const stat = await destination.stat();
|
|
2599
2993
|
if (stat.type === "file" && stat.size === input.file.bytes) {
|
|
2600
2994
|
input.onProgress?.({
|
|
2601
|
-
model:
|
|
2995
|
+
model: input.model,
|
|
2602
2996
|
file: input.file.destination,
|
|
2603
2997
|
index: input.index,
|
|
2604
2998
|
total: input.total,
|
|
@@ -2615,7 +3009,7 @@ async function ensureModelFile(input) {
|
|
|
2615
3009
|
resume: true,
|
|
2616
3010
|
onProgress(progress) {
|
|
2617
3011
|
input.onProgress?.({
|
|
2618
|
-
model:
|
|
3012
|
+
model: input.model,
|
|
2619
3013
|
file: input.file.destination,
|
|
2620
3014
|
index: input.index,
|
|
2621
3015
|
total: input.total,
|
|
@@ -2624,16 +3018,16 @@ async function ensureModelFile(input) {
|
|
|
2624
3018
|
}
|
|
2625
3019
|
}).result;
|
|
2626
3020
|
}
|
|
2627
|
-
async function writePreparedManifest(
|
|
2628
|
-
await files.file(
|
|
3021
|
+
async function writePreparedManifest(path, manifest) {
|
|
3022
|
+
await files.file(path, "manifest.json").write(JSON.stringify(manifest, null, 2));
|
|
2629
3023
|
}
|
|
2630
|
-
async function imageModelState(model,
|
|
2631
|
-
|
|
3024
|
+
async function imageModelState(model, path) {
|
|
3025
|
+
const definition = imageModelDefinition(model);
|
|
2632
3026
|
let bytesPresent = 0;
|
|
2633
3027
|
const missingFiles = [];
|
|
2634
3028
|
const invalidFiles = [];
|
|
2635
|
-
for (const file of
|
|
2636
|
-
const stat = await files.file(
|
|
3029
|
+
for (const file of definition.files) {
|
|
3030
|
+
const stat = await files.file(path, file.destination).stat();
|
|
2637
3031
|
if (stat.type !== "file") {
|
|
2638
3032
|
missingFiles.push(file.destination);
|
|
2639
3033
|
continue;
|
|
@@ -2641,17 +3035,17 @@ async function imageModelState(model, modelRoot) {
|
|
|
2641
3035
|
bytesPresent += Math.min(stat.size ?? 0, file.bytes);
|
|
2642
3036
|
if (stat.size !== file.bytes) invalidFiles.push(file.destination);
|
|
2643
3037
|
}
|
|
2644
|
-
const
|
|
2645
|
-
const manifestStat = await files.file(modelRoot, "manifest.json").stat();
|
|
3038
|
+
const manifestStat = await files.file(path, "manifest.json").stat();
|
|
2646
3039
|
if (manifestStat.type !== "file") missingFiles.push("manifest.json");
|
|
3040
|
+
const installed = missingFiles.length === 0;
|
|
2647
3041
|
return {
|
|
2648
3042
|
model,
|
|
2649
|
-
|
|
2650
|
-
paths: fluxImageModelPaths(
|
|
3043
|
+
path,
|
|
3044
|
+
paths: fluxImageModelPaths(path, definition.manifest),
|
|
2651
3045
|
installed,
|
|
2652
3046
|
verified: installed && invalidFiles.length === 0 && manifestStat.type === "file",
|
|
2653
3047
|
bytesPresent,
|
|
2654
|
-
totalBytes:
|
|
3048
|
+
totalBytes: definition.totalBytes,
|
|
2655
3049
|
missingFiles,
|
|
2656
3050
|
invalidFiles
|
|
2657
3051
|
};
|
|
@@ -2666,8 +3060,8 @@ function tokenArray(value, name) {
|
|
|
2666
3060
|
return number;
|
|
2667
3061
|
});
|
|
2668
3062
|
}
|
|
2669
|
-
async function createFluxTokenizer(
|
|
2670
|
-
const paths = fluxImageModelPaths(
|
|
3063
|
+
async function createFluxTokenizer(path, manifest) {
|
|
3064
|
+
const paths = fluxImageModelPaths(path, manifest);
|
|
2671
3065
|
const tokenizer = new Tokenizer_default(await files.file(paths.textEncoderPath, "tokenizer.json").json(), await files.file(paths.textEncoderPath, "tokenizer_config.json").json());
|
|
2672
3066
|
const padTokenId = 151643;
|
|
2673
3067
|
return ({ prompt, maxLength }) => {
|
|
@@ -2714,18 +3108,18 @@ function assertTokenArray(value, name) {
|
|
|
2714
3108
|
if (value.length === 0) throw new AIError("invalid_message", `${name} must not be empty`);
|
|
2715
3109
|
for (const token of value) if (!Number.isInteger(token) || token < 0) throw new AIError("invalid_message", `${name} must contain token ids`);
|
|
2716
3110
|
}
|
|
2717
|
-
function imageResult(value, fallbackMetadata) {
|
|
2718
|
-
if (!isRecord(value) || typeof value.uri !== "string" || typeof value.width !== "number" || typeof value.height !== "number" || value.model !==
|
|
3111
|
+
function imageResult(value, model, fallbackMetadata) {
|
|
3112
|
+
if (!isRecord(value) || typeof value.uri !== "string" || typeof value.width !== "number" || typeof value.height !== "number" || value.model !== model) throw new AIError("image_generation_failed", "Expected generated image URI");
|
|
2719
3113
|
const result = value;
|
|
2720
3114
|
const metadata = isRecord(result.metadata) ? result.metadata : {};
|
|
2721
3115
|
return {
|
|
2722
3116
|
uri: value.uri,
|
|
2723
3117
|
width: value.width,
|
|
2724
3118
|
height: value.height,
|
|
2725
|
-
model
|
|
3119
|
+
model,
|
|
2726
3120
|
metadata: {
|
|
2727
3121
|
...fallbackMetadata,
|
|
2728
|
-
model: metadata.model ===
|
|
3122
|
+
model: metadata.model === model ? model : fallbackMetadata.model,
|
|
2729
3123
|
width: typeof metadata.width === "number" ? metadata.width : value.width,
|
|
2730
3124
|
height: typeof metadata.height === "number" ? metadata.height : value.height,
|
|
2731
3125
|
steps: typeof metadata.steps === "number" ? metadata.steps : fallbackMetadata.steps,
|
|
@@ -2774,15 +3168,18 @@ function nativeErrorMessage(error) {
|
|
|
2774
3168
|
return (error instanceof Error ? error.message : String(error)).replace(/^\[(download|tokenize|encode|denoise|decode|write)]\s*/, "");
|
|
2775
3169
|
}
|
|
2776
3170
|
async function prepareImageModel(options = {}) {
|
|
2777
|
-
const
|
|
2778
|
-
|
|
2779
|
-
|
|
3171
|
+
const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
|
|
3172
|
+
return prepareImageModelAtPath(definition, options, options.directory ? files.directory(options.directory, definition.manifest.directory).path : modelCacheRoot(definition.manifest).path);
|
|
3173
|
+
}
|
|
3174
|
+
async function prepareImageModelAtPath(definition, options, path) {
|
|
3175
|
+
const model = definition.manifest.model;
|
|
3176
|
+
const root = files.directory(path);
|
|
2780
3177
|
await root.create({ recursive: true });
|
|
2781
3178
|
const headers = options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : void 0;
|
|
2782
|
-
const total =
|
|
3179
|
+
const total = definition.files.length;
|
|
2783
3180
|
const bytesByFile = /* @__PURE__ */ new Map();
|
|
2784
3181
|
const emitProgress = (progress) => {
|
|
2785
|
-
const file =
|
|
3182
|
+
const file = definition.files.find((candidate) => candidate.destination === progress.file);
|
|
2786
3183
|
const bytesWritten = file ? Math.min(progress.bytesWritten, file.bytes) : progress.bytesWritten;
|
|
2787
3184
|
bytesByFile.set(progress.file, bytesWritten);
|
|
2788
3185
|
const modelBytesWritten = Array.from(bytesByFile.values()).reduce((sum, bytes) => sum + bytes, 0);
|
|
@@ -2790,12 +3187,12 @@ async function prepareImageModel(options = {}) {
|
|
|
2790
3187
|
...progress,
|
|
2791
3188
|
bytesWritten,
|
|
2792
3189
|
modelBytesWritten,
|
|
2793
|
-
totalModelBytes:
|
|
2794
|
-
modelFraction: modelBytesWritten /
|
|
3190
|
+
totalModelBytes: definition.totalBytes,
|
|
3191
|
+
modelFraction: modelBytesWritten / definition.totalBytes
|
|
2795
3192
|
});
|
|
2796
3193
|
};
|
|
2797
3194
|
const missingFiles = [];
|
|
2798
|
-
for (const [index, file] of
|
|
3195
|
+
for (const [index, file] of definition.files.entries()) {
|
|
2799
3196
|
const stat = await files.file(root.path, file.destination).stat();
|
|
2800
3197
|
if (stat.type === "file" && stat.size === file.bytes) emitProgress({
|
|
2801
3198
|
model,
|
|
@@ -2813,7 +3210,8 @@ async function prepareImageModel(options = {}) {
|
|
|
2813
3210
|
}
|
|
2814
3211
|
await runConcurrent(missingFiles, downloadConcurrency(options.concurrency), async ({ file, index }) => {
|
|
2815
3212
|
await ensureModelFile({
|
|
2816
|
-
|
|
3213
|
+
model,
|
|
3214
|
+
path: root.path,
|
|
2817
3215
|
file,
|
|
2818
3216
|
headers,
|
|
2819
3217
|
index,
|
|
@@ -2821,22 +3219,26 @@ async function prepareImageModel(options = {}) {
|
|
|
2821
3219
|
onProgress: emitProgress
|
|
2822
3220
|
});
|
|
2823
3221
|
});
|
|
2824
|
-
await writePreparedManifest(root.path);
|
|
3222
|
+
await writePreparedManifest(root.path, definition.manifest);
|
|
2825
3223
|
return {
|
|
2826
3224
|
model,
|
|
2827
|
-
...fluxImageModelPaths(root.path),
|
|
2828
|
-
tokenizer: await createFluxTokenizer(root.path)
|
|
3225
|
+
...fluxImageModelPaths(root.path, definition.manifest),
|
|
3226
|
+
tokenizer: await createFluxTokenizer(root.path, definition.manifest)
|
|
2829
3227
|
};
|
|
2830
3228
|
}
|
|
2831
3229
|
async function verifyImageModel(options = {}) {
|
|
2832
|
-
|
|
3230
|
+
const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
|
|
3231
|
+
return imageModelState(definition.manifest.model, modelPathFromOptions(definition, options.path));
|
|
2833
3232
|
}
|
|
2834
3233
|
async function listImageModels(options = {}) {
|
|
2835
|
-
return [await
|
|
3234
|
+
return [...await Promise.all([flux2Klein4B8BitAbliterated.model, ternaryBonsaiImage4B.model].map((model) => {
|
|
3235
|
+
const definition = imageModelDefinition(model);
|
|
3236
|
+
return imageModelState(model, options.directory ? files.directory(options.directory, definition.manifest.directory).path : modelPathFromOptions(definition, void 0));
|
|
3237
|
+
}))];
|
|
2836
3238
|
}
|
|
2837
3239
|
async function deleteImageModel(options = {}) {
|
|
2838
|
-
|
|
2839
|
-
await files.directory(
|
|
3240
|
+
const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
|
|
3241
|
+
await files.directory(modelPathFromOptions(definition, options.path)).delete({ recursive: true });
|
|
2840
3242
|
}
|
|
2841
3243
|
async function imageMemoryTelemetry() {
|
|
2842
3244
|
const value = await bridge().imageMemoryTelemetry?.();
|
|
@@ -2974,8 +3376,8 @@ async function prepareSelectedImageLoRAs(loras, model, options = {}) {
|
|
|
2974
3376
|
}
|
|
2975
3377
|
async function generateImage(options) {
|
|
2976
3378
|
throwIfAborted(options.signal);
|
|
2977
|
-
const
|
|
2978
|
-
|
|
3379
|
+
const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
|
|
3380
|
+
const model = definition.manifest.model;
|
|
2979
3381
|
const prompt = options.prompt.trim();
|
|
2980
3382
|
if (!prompt) throw new AIError("invalid_message", "Expected prompt");
|
|
2981
3383
|
const presetSize = options.aspectRatio ? imageSizeForPreset(options.aspectRatio) : void 0;
|
|
@@ -3034,16 +3436,16 @@ async function generateImage(options) {
|
|
|
3034
3436
|
const sigmaSchedule = imageSigmaSchedule(selectedLoRAs.map((lora) => lora.metadata?.sigmaSchedule));
|
|
3035
3437
|
if (sigmaSchedule && options.steps !== void 0 && options.steps !== sigmaSchedule.length) throw new AIError("invalid_model", `steps must equal the LoRA sigma schedule length (${sigmaSchedule.length})`);
|
|
3036
3438
|
const generationSteps = sigmaSchedule?.length ?? steps;
|
|
3037
|
-
const prepared = options.
|
|
3439
|
+
const prepared = options.path && options.tokenizer ? void 0 : await withImageStage("download", () => prepareImageModelAtPath(definition, {
|
|
3038
3440
|
model,
|
|
3039
3441
|
accessToken: options.accessToken,
|
|
3040
3442
|
concurrency: options.concurrency,
|
|
3041
3443
|
onProgress: options.onModelProgress
|
|
3042
|
-
}));
|
|
3444
|
+
}, options.path ?? modelCacheRoot(definition.manifest).path));
|
|
3043
3445
|
throwIfAborted(options.signal);
|
|
3044
3446
|
const tokenizer = options.tokenizer ?? prepared?.tokenizer;
|
|
3045
|
-
const
|
|
3046
|
-
if (!tokenizer || !
|
|
3447
|
+
const modelPath = options.path ?? prepared?.path;
|
|
3448
|
+
if (!tokenizer || !modelPath) throw new AIError("invalid_model", "Expected image model path and tokenizer");
|
|
3047
3449
|
const tokenized = await withImageStage("tokenize", () => tokenizer({
|
|
3048
3450
|
prompt,
|
|
3049
3451
|
maxLength: 512
|
|
@@ -3054,7 +3456,7 @@ async function generateImage(options) {
|
|
|
3054
3456
|
assertTokenArray(tokenized.attentionMask, "attentionMask");
|
|
3055
3457
|
if (tokenized.inputIds.length !== tokenized.attentionMask.length) throw new AIError("invalid_message", "inputIds and attentionMask must have the same length");
|
|
3056
3458
|
});
|
|
3057
|
-
const paths = fluxImageModelPaths(
|
|
3459
|
+
const paths = fluxImageModelPaths(modelPath, definition.manifest);
|
|
3058
3460
|
const request = {
|
|
3059
3461
|
model,
|
|
3060
3462
|
prompt,
|
|
@@ -3084,7 +3486,10 @@ async function generateImage(options) {
|
|
|
3084
3486
|
seed,
|
|
3085
3487
|
inputIds: tokenized.inputIds,
|
|
3086
3488
|
attentionMask: tokenized.attentionMask,
|
|
3087
|
-
|
|
3489
|
+
modelRoot: paths.path,
|
|
3490
|
+
textEncoderPath: paths.textEncoderPath,
|
|
3491
|
+
transformerPath: paths.transformerPath,
|
|
3492
|
+
vaePath: paths.vaePath
|
|
3088
3493
|
};
|
|
3089
3494
|
const fallbackMetadata = {
|
|
3090
3495
|
model,
|
|
@@ -3115,7 +3520,7 @@ async function generateImage(options) {
|
|
|
3115
3520
|
throwIfAborted(options.signal);
|
|
3116
3521
|
const value = await native.generateImage?.(request);
|
|
3117
3522
|
throwIfAborted(options.signal);
|
|
3118
|
-
return imageResult(value, fallbackMetadata);
|
|
3523
|
+
return imageResult(value, model, fallbackMetadata);
|
|
3119
3524
|
} finally {
|
|
3120
3525
|
subscription?.remove();
|
|
3121
3526
|
options.signal?.removeEventListener("abort", cancelNative);
|
|
@@ -3883,6 +4288,59 @@ function stableStringify(value) {
|
|
|
3883
4288
|
return JSON.stringify(value) ?? "undefined";
|
|
3884
4289
|
}
|
|
3885
4290
|
|
|
4291
|
+
//#endregion
|
|
4292
|
+
//#region src/ai/bonsai-generation.ts
|
|
4293
|
+
async function prepareBonsaiGeneration(input) {
|
|
4294
|
+
const state = await verifyBonsaiTextModel(input.model, { path: input.path });
|
|
4295
|
+
if (!state.verified) throw new AIError("model_unavailable", `${input.model.id} is not prepared. Call prepareTextModel() before generation.`);
|
|
4296
|
+
const tokenizer = new Tokenizer_default(await files.file(state.path, "tokenizer.json").json(), await files.file(state.path, "tokenizer_config.json").json());
|
|
4297
|
+
const inputIds = tokenizer.encode(formatMessages(messagesFromInput(input))).ids.map(Number);
|
|
4298
|
+
if (!inputIds.length || inputIds.some((token) => !Number.isInteger(token))) throw new AIError("invalid_message", "Tokenizer returned invalid token ids");
|
|
4299
|
+
return {
|
|
4300
|
+
request: {
|
|
4301
|
+
id: input.id,
|
|
4302
|
+
model: input.model.id,
|
|
4303
|
+
modelPath: state.path,
|
|
4304
|
+
inputIds,
|
|
4305
|
+
endTokenIds: endTokenIds(input.model.id),
|
|
4306
|
+
maxTokens: input.maxTokens,
|
|
4307
|
+
temperature: input.temperature,
|
|
4308
|
+
seed: input.seed
|
|
4309
|
+
},
|
|
4310
|
+
decode(tokenIds) {
|
|
4311
|
+
return tokenIds.length ? tokenizer.decode(tokenIds, { skip_special_tokens: true }) : "";
|
|
4312
|
+
}
|
|
4313
|
+
};
|
|
4314
|
+
}
|
|
4315
|
+
function messagesFromInput(input) {
|
|
4316
|
+
if (input.messages) {
|
|
4317
|
+
if (input.system !== void 0 || input.prompt !== void 0) throw new AIError("invalid_message", "messages cannot be combined with system or prompt");
|
|
4318
|
+
if (!input.messages.length) throw new AIError("invalid_message", "Expected at least one message");
|
|
4319
|
+
return input.messages;
|
|
4320
|
+
}
|
|
4321
|
+
if (!input.prompt?.trim()) throw new AIError("invalid_message", "Expected prompt");
|
|
4322
|
+
return [...input.system ? [{
|
|
4323
|
+
role: "system",
|
|
4324
|
+
content: input.system
|
|
4325
|
+
}] : [], {
|
|
4326
|
+
role: "user",
|
|
4327
|
+
content: input.prompt
|
|
4328
|
+
}];
|
|
4329
|
+
}
|
|
4330
|
+
function formatMessages(messages) {
|
|
4331
|
+
if (messages[0]?.role === "assistant") throw new AIError("invalid_message", "Conversation cannot begin with assistant");
|
|
4332
|
+
const parts = messages.map((message, index) => {
|
|
4333
|
+
if (!message.content.trim()) throw new AIError("invalid_message", `Message ${index + 1} is empty`);
|
|
4334
|
+
if (message.role === "system" && index !== 0) throw new AIError("invalid_message", "System message must be first");
|
|
4335
|
+
return `<|im_start|>${message.role}\n${message.content}<|im_end|>\n`;
|
|
4336
|
+
});
|
|
4337
|
+
parts.push("<|im_start|>assistant\n<think>\n\n</think>\n\n");
|
|
4338
|
+
return parts.join("");
|
|
4339
|
+
}
|
|
4340
|
+
function endTokenIds(model) {
|
|
4341
|
+
return model === "ternary-bonsai-27b" ? [248044, 248046] : [151643, 151645];
|
|
4342
|
+
}
|
|
4343
|
+
|
|
3886
4344
|
//#endregion
|
|
3887
4345
|
//#region src/ai/text.ts
|
|
3888
4346
|
function promptFromInput(options) {
|
|
@@ -3893,14 +4351,27 @@ function promptFromInput(options) {
|
|
|
3893
4351
|
if (parts.length === 0) throw new AIError("invalid_message", "Expected prompt or input");
|
|
3894
4352
|
return parts.join("\n\n");
|
|
3895
4353
|
}
|
|
3896
|
-
async function isAvailable() {
|
|
4354
|
+
async function isAvailable(model, options = {}) {
|
|
4355
|
+
if (model) return (await availability(model, options)).available;
|
|
3897
4356
|
try {
|
|
3898
4357
|
return await bridge().isAvailable?.() ?? false;
|
|
3899
4358
|
} catch {
|
|
3900
4359
|
return false;
|
|
3901
4360
|
}
|
|
3902
4361
|
}
|
|
3903
|
-
async function availability() {
|
|
4362
|
+
async function availability(model, options = {}) {
|
|
4363
|
+
if (model) {
|
|
4364
|
+
const state = await verifyBonsaiTextModel(model, options);
|
|
4365
|
+
if (!state.installed) return {
|
|
4366
|
+
available: false,
|
|
4367
|
+
reason: "not_installed"
|
|
4368
|
+
};
|
|
4369
|
+
if (!state.verified) return {
|
|
4370
|
+
available: false,
|
|
4371
|
+
reason: "invalid_model"
|
|
4372
|
+
};
|
|
4373
|
+
return { available: true };
|
|
4374
|
+
}
|
|
3904
4375
|
try {
|
|
3905
4376
|
return await bridge().availability?.() ?? {
|
|
3906
4377
|
available: false,
|
|
@@ -3970,11 +4441,28 @@ async function generate(options) {
|
|
|
3970
4441
|
const output = options.output;
|
|
3971
4442
|
const id = generationId();
|
|
3972
4443
|
const native = bridge();
|
|
3973
|
-
const toolSubscription = listenForTools(id, options.tools);
|
|
4444
|
+
const toolSubscription = listenForTools(id, "tools" in options ? options.tools : void 0);
|
|
3974
4445
|
const abort = () => void native.cancelTextGeneration?.(id);
|
|
3975
4446
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
3976
4447
|
try {
|
|
3977
4448
|
if (options.signal?.aborted) throw new AIError("generation_canceled", "Generation canceled");
|
|
4449
|
+
if (options.model) {
|
|
4450
|
+
const prepared = await prepareBonsaiGeneration({
|
|
4451
|
+
id,
|
|
4452
|
+
model: options.model,
|
|
4453
|
+
path: options.path,
|
|
4454
|
+
system: options.system,
|
|
4455
|
+
prompt: options.prompt,
|
|
4456
|
+
messages: options.messages,
|
|
4457
|
+
temperature: options.temperature,
|
|
4458
|
+
maxTokens: options.maxTokens,
|
|
4459
|
+
seed: options.seed
|
|
4460
|
+
});
|
|
4461
|
+
if (options.signal?.aborted) throw new AIError("generation_canceled", "Generation canceled");
|
|
4462
|
+
const tokenIds = await native.generateLocalText?.(prepared.request);
|
|
4463
|
+
if (!tokenIds) throw new AIError("unsupported_runtime", "Local text generation is unavailable in this runtime");
|
|
4464
|
+
return prepared.decode(tokenIds);
|
|
4465
|
+
}
|
|
3978
4466
|
const value = await native.generate?.(nativeRequest(options, id));
|
|
3979
4467
|
if (output) {
|
|
3980
4468
|
const result = output["~standard"].validate(value);
|
|
@@ -3992,6 +4480,10 @@ async function generate(options) {
|
|
|
3992
4480
|
}
|
|
3993
4481
|
}
|
|
3994
4482
|
async function* stream(options) {
|
|
4483
|
+
if (options.model) {
|
|
4484
|
+
yield* streamBonsai(options);
|
|
4485
|
+
return;
|
|
4486
|
+
}
|
|
3995
4487
|
const id = generationId();
|
|
3996
4488
|
const native = bridge();
|
|
3997
4489
|
const updates = [];
|
|
@@ -4029,6 +4521,56 @@ async function* stream(options) {
|
|
|
4029
4521
|
toolSubscription?.remove();
|
|
4030
4522
|
}
|
|
4031
4523
|
}
|
|
4524
|
+
async function* streamBonsai(options) {
|
|
4525
|
+
const id = generationId();
|
|
4526
|
+
const native = bridge();
|
|
4527
|
+
if (options.signal?.aborted) throw new AIError("generation_canceled", "Generation canceled");
|
|
4528
|
+
const prepared = await prepareBonsaiGeneration({
|
|
4529
|
+
id,
|
|
4530
|
+
model: options.model,
|
|
4531
|
+
path: options.path,
|
|
4532
|
+
system: options.system,
|
|
4533
|
+
prompt: options.prompt,
|
|
4534
|
+
messages: options.messages,
|
|
4535
|
+
temperature: options.temperature,
|
|
4536
|
+
maxTokens: options.maxTokens,
|
|
4537
|
+
seed: options.seed
|
|
4538
|
+
});
|
|
4539
|
+
if (options.signal?.aborted) throw new AIError("generation_canceled", "Generation canceled");
|
|
4540
|
+
const updates = [];
|
|
4541
|
+
let wake;
|
|
4542
|
+
let settled = false;
|
|
4543
|
+
let failure;
|
|
4544
|
+
const updateSubscription = native.addListener?.("localTextGenerationUpdate", (event) => {
|
|
4545
|
+
if (event.id !== id) return;
|
|
4546
|
+
updates.push(event.tokenIds);
|
|
4547
|
+
wake?.();
|
|
4548
|
+
wake = void 0;
|
|
4549
|
+
});
|
|
4550
|
+
const abort = () => void native.cancelTextGeneration?.(id);
|
|
4551
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
4552
|
+
const completion = native.streamLocalText?.(prepared.request).catch((error) => {
|
|
4553
|
+
failure = error;
|
|
4554
|
+
return [];
|
|
4555
|
+
}).finally(() => {
|
|
4556
|
+
settled = true;
|
|
4557
|
+
wake?.();
|
|
4558
|
+
wake = void 0;
|
|
4559
|
+
});
|
|
4560
|
+
try {
|
|
4561
|
+
if (!completion) throw new AIError("unsupported_runtime", "Local text streaming is unavailable in this runtime");
|
|
4562
|
+
while (!settled || updates.length > 0) if (updates.length > 0) yield prepared.decode(updates.shift());
|
|
4563
|
+
else await new Promise((resolve) => {
|
|
4564
|
+
wake = resolve;
|
|
4565
|
+
});
|
|
4566
|
+
await completion;
|
|
4567
|
+
if (failure) throw mapNativeError(failure);
|
|
4568
|
+
} finally {
|
|
4569
|
+
if (!settled) await native.cancelTextGeneration?.(id);
|
|
4570
|
+
options.signal?.removeEventListener("abort", abort);
|
|
4571
|
+
updateSubscription?.remove();
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4032
4574
|
|
|
4033
4575
|
//#endregion
|
|
4034
4576
|
//#region src/ai/text-hook.ts
|
|
@@ -4099,4 +4641,4 @@ function useTextGeneration() {
|
|
|
4099
4641
|
}
|
|
4100
4642
|
|
|
4101
4643
|
//#endregion
|
|
4102
|
-
export { AIError, ImageGenerationError, availability, buildImageGenerationRequest, clampOutpaintRect, defaultImageLongSide, deleteImageLoRA, deleteImageModel, flux2Klein4B8BitAbliterated, fluxImageModelPaths, generate, generateImage, generateImages, getImageGenerationJob, imageMemoryTelemetry, imageModelManifest, imageModelManifests, imageSizeForAspectRatio, imageSizeForPreset, initialOutpaintRect, isAvailable, listImageGenerationJobs, listImageLoRAs, listImageModels, normalizeImageSize, prepareImageLoRA, prepareImageModel, rectForSourceScale, roundImageDimension, roundedImageSize, scheduleImageGeneration, shape, stream, unloadImageModel, upscaleImage, useImageComposer, useImageGeneration, useImageModel, useTextGeneration, verifyImageLoRA, verifyImageModel };
|
|
4644
|
+
export { AIError, ImageGenerationError, availability, bonsai, bonsaiTextModelCatalog, buildImageGenerationRequest, clampOutpaintRect, defaultImageLongSide, deleteImageLoRA, deleteImageModel, deleteBonsaiTextModel as deleteTextModel, flux2Klein4B8BitAbliterated, fluxImageModelPaths, generate, generateImage, generateImages, getImageGenerationJob, imageMemoryTelemetry, imageModelManifest, imageModelManifests, imageSizeForAspectRatio, imageSizeForPreset, initialOutpaintRect, isAvailable, listImageGenerationJobs, listImageLoRAs, listImageModels, listBonsaiTextModels as listTextModels, normalizeImageSize, prepareImageLoRA, prepareImageModel, prepareBonsaiTextModel as prepareTextModel, rectForSourceScale, roundImageDimension, roundedImageSize, scheduleImageGeneration, shape, stream, ternaryBonsaiImage4B, unloadImageModel, upscaleImage, useImageComposer, useImageGeneration, useImageModel, useTextGeneration, verifyImageLoRA, verifyImageModel, verifyBonsaiTextModel as verifyTextModel };
|