bleam 0.0.11 → 0.0.12
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 +2314 -2173
- package/dist/ai.d.cts +227 -322
- package/dist/ai.d.ts +227 -322
- package/dist/ai.js +2309 -2159
- package/dist/cli.d.cts +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/config.d.cts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/{files-Ds1wT8C2.js → files-BXVkPrPN.js} +6 -1
- package/dist/{files-Bo7h9fik.cjs → files-DxaQ-Nv0.cjs} +11 -0
- package/dist/files.cjs +1 -1
- package/dist/files.d.cts +1 -1
- package/dist/files.d.ts +1 -1
- package/dist/files.js +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/schema-B5BfdswF.js +226 -0
- package/dist/schema-BnVZOXfu.cjs +286 -0
- package/dist/schema-D5eImHxu.d.cts +125 -0
- package/dist/schema-SSjokbtw.d.ts +125 -0
- package/dist/schema.cjs +10 -2
- package/dist/schema.d.cts +2 -2
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js +2 -2
- package/dist/state-Dh3HLixb.js +874 -0
- package/dist/state-LssDgpff.cjs +973 -0
- package/dist/state.cjs +16 -12
- package/dist/state.d.cts +144 -140
- package/dist/state.d.ts +145 -140
- package/dist/state.js +3 -3
- package/dist/{ui-CHc4xEs_.d.ts → ui-D7bRLYee.d.ts} +7 -7
- package/dist/ui.d.ts +1 -1
- package/dist/window.d.ts +1 -1
- package/package.json +3 -1
- package/templates/foundation-models/app/index.tsx +77 -15
- package/templates/image-generation/app/index.tsx +2 -2
- package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +46 -46
- package/templates/native/ios/Podfile.lock +173 -173
- package/templates/native/modules/bleam-runtime/ios/AIModule.swift +35 -357
- package/templates/text-generation/app/index.tsx +81 -50
- package/templates/updates/README.md +1 -1
- package/dist/schema-Bo5Jvqus.js +0 -90
- package/dist/schema-CYh6n8GS.d.ts +0 -58
- package/dist/schema-oeOrd3i1.d.cts +0 -58
- package/dist/schema-rQ13mrpD.cjs +0 -102
- package/dist/state-Bx0VlTlO.cjs +0 -852
- package/dist/state-CAwe-Vw1.js +0 -767
- /package/dist/{config-Cms0rvqg.d.ts → config-COcRnn5a.d.cts} +0 -0
- /package/dist/{config-CufOVJV3.d.cts → config-Chi-flpJ.d.ts} +0 -0
- /package/dist/{files-4ZEoAWiv.d.ts → files-DwA7pzr3.d.cts} +0 -0
- /package/dist/{files-Dt5mbzLq.d.cts → files-VrkQlKIT.d.ts} +0 -0
package/dist/ai.cjs
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
2
2
|
require('./native-runtime-CsXnXkQn.cjs');
|
|
3
3
|
require('./crypto-CT3VZ7lF.cjs');
|
|
4
|
-
const require_files = require('./files-
|
|
5
|
-
const
|
|
6
|
-
require('./
|
|
4
|
+
const require_files = require('./files-DxaQ-Nv0.cjs');
|
|
5
|
+
const require_schema = require('./schema-BnVZOXfu.cjs');
|
|
6
|
+
const require_state = require('./state-LssDgpff.cjs');
|
|
7
7
|
let expo = require("expo");
|
|
8
8
|
expo = require_chunk.__toESM(expo);
|
|
9
9
|
let react = require("react");
|
|
10
10
|
react = require_chunk.__toESM(react);
|
|
11
|
-
let react_native = require("react-native");
|
|
12
|
-
react_native = require_chunk.__toESM(react_native);
|
|
13
11
|
|
|
14
12
|
//#region src/ai/bonsai.ts
|
|
15
13
|
const internalTextModelCatalog = [
|
|
@@ -309,486 +307,344 @@ async function deleteBonsaiTextModel(model, options = {}) {
|
|
|
309
307
|
}
|
|
310
308
|
|
|
311
309
|
//#endregion
|
|
312
|
-
//#region src/ai/
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
constructor(executor) {
|
|
310
|
+
//#region src/ai/chat-manager.ts
|
|
311
|
+
const missingSnapshot = {
|
|
312
|
+
chat: null,
|
|
313
|
+
messages: []
|
|
314
|
+
};
|
|
315
|
+
var ChatManager = class {
|
|
316
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
317
|
+
chatListeners = /* @__PURE__ */ new Map();
|
|
318
|
+
listListeners = /* @__PURE__ */ new Set();
|
|
319
|
+
controllers = /* @__PURE__ */ new Map();
|
|
320
|
+
listSnapshot = [];
|
|
321
|
+
operation = Promise.resolve();
|
|
322
|
+
ready;
|
|
323
|
+
constructor(repository, executor) {
|
|
324
|
+
this.repository = repository;
|
|
326
325
|
this.executor = executor;
|
|
326
|
+
this.ready = this.initialize();
|
|
327
327
|
}
|
|
328
328
|
setExecutor(executor) {
|
|
329
329
|
this.executor = executor;
|
|
330
330
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
331
|
+
async create(input = {}) {
|
|
332
|
+
await this.ready;
|
|
333
|
+
return this.mutate(async () => {
|
|
334
|
+
const chat$1 = await this.repository.create({ title: validTitle(input.title ?? "New Chat") });
|
|
335
|
+
this.set({
|
|
336
|
+
chat: chat$1,
|
|
337
|
+
messages: []
|
|
338
|
+
});
|
|
339
|
+
return chat$1;
|
|
338
340
|
});
|
|
339
|
-
const record = {
|
|
340
|
-
snapshot: {
|
|
341
|
-
id,
|
|
342
|
-
status: "queued",
|
|
343
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
344
|
-
},
|
|
345
|
-
options,
|
|
346
|
-
controller: new AbortController(),
|
|
347
|
-
listeners: /* @__PURE__ */ new Set(),
|
|
348
|
-
promise,
|
|
349
|
-
resolve,
|
|
350
|
-
reject
|
|
351
|
-
};
|
|
352
|
-
this.jobs.set(id, record);
|
|
353
|
-
this.pending.push(record);
|
|
354
|
-
if (options.signal) {
|
|
355
|
-
const cancel = () => this.cancel(id);
|
|
356
|
-
options.signal.addEventListener("abort", cancel, { once: true });
|
|
357
|
-
record.removeSignalListener = () => options.signal?.removeEventListener("abort", cancel);
|
|
358
|
-
if (options.signal.aborted) cancel();
|
|
359
|
-
}
|
|
360
|
-
this.runNext();
|
|
361
|
-
return this.handle(record);
|
|
362
|
-
}
|
|
363
|
-
get(id) {
|
|
364
|
-
const record = this.jobs.get(id);
|
|
365
|
-
return record ? this.handle(record) : void 0;
|
|
366
341
|
}
|
|
367
|
-
|
|
368
|
-
|
|
342
|
+
async get(id) {
|
|
343
|
+
await this.ready;
|
|
344
|
+
return this.snapshots.get(id)?.chat ?? null;
|
|
345
|
+
}
|
|
346
|
+
async list() {
|
|
347
|
+
await this.ready;
|
|
348
|
+
return this.listSnapshot;
|
|
349
|
+
}
|
|
350
|
+
async send(id, content, options = {}, callbacks = {}) {
|
|
351
|
+
await this.ready;
|
|
352
|
+
if (options.signal?.aborted) throw canceledException();
|
|
353
|
+
const value = content.trim();
|
|
354
|
+
if (!value) throw new AIError("invalid_message", "Message must not be empty");
|
|
355
|
+
const controller = new AbortController();
|
|
356
|
+
const accepted = await this.mutate(async () => {
|
|
357
|
+
if (this.controllers.has(id)) throw new AIError("concurrent_request", "This chat already has an active generation");
|
|
358
|
+
const next = await this.repository.acceptSend(id, value);
|
|
359
|
+
this.controllers.set(id, controller);
|
|
360
|
+
this.setAccepted(next);
|
|
361
|
+
return next;
|
|
362
|
+
});
|
|
363
|
+
this.run(accepted, options, callbacks, controller);
|
|
364
|
+
return resultFromAccepted(accepted);
|
|
365
|
+
}
|
|
366
|
+
async cancel(id) {
|
|
367
|
+
await this.ready;
|
|
368
|
+
return this.mutate(async () => {
|
|
369
|
+
const controller = this.controllers.get(id);
|
|
370
|
+
const assistant = this.snapshots.get(id)?.messages.findLast((message) => message.role === "assistant");
|
|
371
|
+
if (!controller || !assistant || !isActiveMessage(assistant)) return false;
|
|
372
|
+
const next = await this.repository.markCanceling(id, assistant.id);
|
|
373
|
+
this.setAccepted(next);
|
|
374
|
+
controller.abort();
|
|
375
|
+
return true;
|
|
376
|
+
});
|
|
369
377
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
this.
|
|
379
|
-
|
|
380
|
-
|
|
378
|
+
async retry(id, messageId, options = {}, callbacks = {}) {
|
|
379
|
+
await this.ready;
|
|
380
|
+
if (options.signal?.aborted) throw canceledException();
|
|
381
|
+
const controller = new AbortController();
|
|
382
|
+
const accepted = await this.mutate(async () => {
|
|
383
|
+
if (this.controllers.has(id)) throw new AIError("concurrent_request", "This chat already has an active generation");
|
|
384
|
+
const next = await this.repository.retry(id, messageId);
|
|
385
|
+
this.controllers.set(id, controller);
|
|
386
|
+
this.setAccepted(next);
|
|
387
|
+
return next;
|
|
388
|
+
});
|
|
389
|
+
this.run(accepted, options, callbacks, controller);
|
|
390
|
+
return resultFromAccepted(accepted);
|
|
391
|
+
}
|
|
392
|
+
async setTitle(id, title) {
|
|
393
|
+
await this.ready;
|
|
394
|
+
return this.mutate(async () => {
|
|
395
|
+
const chat$1 = await this.repository.setTitle(id, validTitle(title));
|
|
396
|
+
this.set({
|
|
397
|
+
chat: chat$1,
|
|
398
|
+
messages: this.snapshots.get(id)?.messages ?? []
|
|
381
399
|
});
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
return true;
|
|
400
|
+
return chat$1;
|
|
401
|
+
});
|
|
386
402
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
return () => record.listeners.delete(listener);
|
|
403
|
+
async remove(id) {
|
|
404
|
+
await this.ready;
|
|
405
|
+
return this.mutate(async () => {
|
|
406
|
+
if (this.controllers.has(id)) throw new AIError("concurrent_request", "Cancel the generation before removing this chat");
|
|
407
|
+
const removed = await this.repository.remove(id);
|
|
408
|
+
if (removed) {
|
|
409
|
+
this.snapshots.delete(id);
|
|
410
|
+
this.refreshList();
|
|
411
|
+
this.emit(id);
|
|
397
412
|
}
|
|
413
|
+
return removed;
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
getChatSnapshot(id) {
|
|
417
|
+
return this.snapshots.get(id) ?? missingSnapshot;
|
|
418
|
+
}
|
|
419
|
+
getListSnapshot() {
|
|
420
|
+
return this.listSnapshot;
|
|
421
|
+
}
|
|
422
|
+
subscribeChat(id, listener) {
|
|
423
|
+
const listeners = this.chatListeners.get(id) ?? /* @__PURE__ */ new Set();
|
|
424
|
+
listeners.add(listener);
|
|
425
|
+
this.chatListeners.set(id, listeners);
|
|
426
|
+
return () => {
|
|
427
|
+
listeners.delete(listener);
|
|
428
|
+
if (listeners.size === 0) this.chatListeners.delete(id);
|
|
398
429
|
};
|
|
399
430
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
this.
|
|
406
|
-
|
|
407
|
-
|
|
431
|
+
subscribeList(listener) {
|
|
432
|
+
this.listListeners.add(listener);
|
|
433
|
+
return () => this.listListeners.delete(listener);
|
|
434
|
+
}
|
|
435
|
+
async initialize() {
|
|
436
|
+
await this.repository.recoverActive();
|
|
437
|
+
const chats$1 = await this.repository.list();
|
|
438
|
+
const transcripts = await Promise.all(chats$1.map((chat$1) => this.repository.messages(chat$1.id)));
|
|
439
|
+
chats$1.forEach((chat$1, index) => {
|
|
440
|
+
this.snapshots.set(chat$1.id, {
|
|
441
|
+
chat: chat$1,
|
|
442
|
+
messages: transcripts[index] ?? []
|
|
443
|
+
});
|
|
408
444
|
});
|
|
445
|
+
this.refreshList();
|
|
446
|
+
}
|
|
447
|
+
run(accepted, options, callbacks, controller) {
|
|
448
|
+
const externalAbort = () => void this.cancel(accepted.chatId);
|
|
449
|
+
options.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
450
|
+
if (options.signal?.aborted) externalAbort();
|
|
451
|
+
this.generate(accepted, options, callbacks, controller).finally(() => {
|
|
452
|
+
options.signal?.removeEventListener("abort", externalAbort);
|
|
453
|
+
}).catch(() => void 0);
|
|
454
|
+
}
|
|
455
|
+
async generate(accepted, options, callbacks, controller) {
|
|
456
|
+
let current = accepted.assistantMessage;
|
|
457
|
+
let content = "";
|
|
458
|
+
let lastPersistedAt = 0;
|
|
409
459
|
try {
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
record.options.onProgress?.(progress);
|
|
415
|
-
this.progress(record, progress);
|
|
416
|
-
}
|
|
460
|
+
const started = await this.mutate(async () => {
|
|
461
|
+
const next = await this.repository.start(accepted.chatId, accepted.assistantMessageId);
|
|
462
|
+
this.setAccepted(next);
|
|
463
|
+
return next;
|
|
417
464
|
});
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
465
|
+
current = started.assistantMessage;
|
|
466
|
+
const messages = historyBefore(started.messages, current, options.system);
|
|
467
|
+
const executorOptions = {
|
|
468
|
+
...options,
|
|
469
|
+
messages,
|
|
470
|
+
signal: controller.signal
|
|
471
|
+
};
|
|
472
|
+
delete executorOptions.system;
|
|
473
|
+
for await (const snapshot of this.executor.stream(executorOptions)) {
|
|
474
|
+
content = snapshot;
|
|
475
|
+
await this.mutate(async () => {
|
|
476
|
+
current = {
|
|
477
|
+
...current,
|
|
478
|
+
content,
|
|
479
|
+
updatedAt: now$1()
|
|
480
|
+
};
|
|
481
|
+
this.replaceMessage(started.chatId, current);
|
|
482
|
+
const timestamp$1 = Date.now();
|
|
483
|
+
if (timestamp$1 - lastPersistedAt >= 100) {
|
|
484
|
+
current = await this.repository.updateContent(current);
|
|
485
|
+
lastPersistedAt = timestamp$1;
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
await this.mutate(async () => {
|
|
490
|
+
const next = await this.repository.finish(started.chatId, current.id, controller.signal.aborted ? "canceled" : "completed", content, controller.signal.aborted ? canceledError$1() : void 0);
|
|
491
|
+
this.controllers.delete(started.chatId);
|
|
492
|
+
this.setAccepted(next);
|
|
493
|
+
if (next.assistantMessage.status === "completed") notify(callbacks.onFinish, next.assistantMessage);
|
|
494
|
+
else notify(callbacks.onError, next.assistantMessage.error);
|
|
422
495
|
});
|
|
423
|
-
record.resolve(result);
|
|
424
496
|
} catch (error) {
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
497
|
+
const serialized = controller.signal.aborted ? canceledError$1() : serializeError(error);
|
|
498
|
+
await this.mutate(async () => {
|
|
499
|
+
const next = await this.repository.finish(accepted.chatId, accepted.assistantMessageId, controller.signal.aborted ? "canceled" : "failed", content, serialized);
|
|
500
|
+
this.controllers.delete(accepted.chatId);
|
|
501
|
+
this.setAccepted(next);
|
|
502
|
+
notify(callbacks.onError, serialized);
|
|
431
503
|
});
|
|
432
|
-
record.reject(failure);
|
|
433
504
|
} finally {
|
|
434
|
-
|
|
435
|
-
this.running = void 0;
|
|
436
|
-
this.runNext();
|
|
505
|
+
if (this.controllers.get(accepted.chatId) === controller) this.controllers.delete(accepted.chatId);
|
|
437
506
|
}
|
|
438
507
|
}
|
|
439
|
-
|
|
440
|
-
this.
|
|
508
|
+
mutate(action) {
|
|
509
|
+
const result = this.operation.then(action, action);
|
|
510
|
+
this.operation = result.then(() => void 0, () => void 0);
|
|
511
|
+
return result;
|
|
441
512
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
};
|
|
447
|
-
|
|
513
|
+
setAccepted(value) {
|
|
514
|
+
this.set({
|
|
515
|
+
chat: value.chat,
|
|
516
|
+
messages: value.messages
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
replaceMessage(chatId, message) {
|
|
520
|
+
const snapshot = this.snapshots.get(chatId);
|
|
521
|
+
if (!snapshot) return;
|
|
522
|
+
this.set({
|
|
523
|
+
chat: snapshot.chat,
|
|
524
|
+
messages: snapshot.messages.map((item) => item.id === message.id ? message : item)
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
set(snapshot) {
|
|
528
|
+
if (!snapshot.chat) return;
|
|
529
|
+
const previous = this.snapshots.get(snapshot.chat.id);
|
|
530
|
+
this.snapshots.set(snapshot.chat.id, snapshot);
|
|
531
|
+
if (previous?.chat !== snapshot.chat) this.refreshList();
|
|
532
|
+
this.emit(snapshot.chat.id);
|
|
533
|
+
}
|
|
534
|
+
refreshList() {
|
|
535
|
+
this.listSnapshot = [...this.snapshots.values()].flatMap((snapshot) => snapshot.chat ? [snapshot.chat] : []).sort((left, right) => left.updatedAt === right.updatedAt ? right.id.localeCompare(left.id) : right.updatedAt.localeCompare(left.updatedAt));
|
|
536
|
+
for (const listener of this.listListeners) listener();
|
|
537
|
+
}
|
|
538
|
+
emit(id) {
|
|
539
|
+
for (const listener of this.chatListeners.get(id) ?? []) listener();
|
|
448
540
|
}
|
|
449
541
|
};
|
|
450
|
-
function
|
|
451
|
-
return
|
|
542
|
+
function historyBefore(messages, target, system) {
|
|
543
|
+
return [...system?.trim() ? [{
|
|
544
|
+
role: "system",
|
|
545
|
+
content: system
|
|
546
|
+
}] : [], ...messages.filter((message) => message.sequence < target.sequence && (message.role === "user" || message.status === "completed")).map((message) => ({
|
|
547
|
+
role: message.role,
|
|
548
|
+
content: message.content
|
|
549
|
+
}))];
|
|
550
|
+
}
|
|
551
|
+
function isActiveMessage(message) {
|
|
552
|
+
return message.role === "assistant" && (message.status === "queued" || message.status === "generating" || message.status === "canceling");
|
|
553
|
+
}
|
|
554
|
+
function resultFromAccepted(value) {
|
|
555
|
+
return {
|
|
556
|
+
chatId: value.chatId,
|
|
557
|
+
userMessageId: value.userMessageId,
|
|
558
|
+
assistantMessageId: value.assistantMessageId
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
function validTitle(title) {
|
|
562
|
+
const value = title.trim();
|
|
563
|
+
if (!value) throw new AIError("invalid_message", "Title must not be empty");
|
|
564
|
+
return value;
|
|
565
|
+
}
|
|
566
|
+
function serializeError(error) {
|
|
567
|
+
const value = error instanceof Error ? error : new Error(String(error));
|
|
568
|
+
const details = value;
|
|
569
|
+
return {
|
|
570
|
+
name: value.name,
|
|
571
|
+
message: value.message,
|
|
572
|
+
code: typeof details.code === "string" ? details.code : void 0,
|
|
573
|
+
stage: typeof details.stage === "string" ? details.stage : void 0
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function canceledError$1() {
|
|
577
|
+
return {
|
|
578
|
+
name: "AIError",
|
|
579
|
+
code: "generation_canceled",
|
|
580
|
+
message: "Generation canceled"
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function canceledException() {
|
|
584
|
+
return new AIError("generation_canceled", "Generation canceled");
|
|
585
|
+
}
|
|
586
|
+
function notify(callback, value) {
|
|
587
|
+
if (!callback) return;
|
|
588
|
+
Promise.resolve().then(() => callback(value)).catch(() => void 0);
|
|
589
|
+
}
|
|
590
|
+
function now$1() {
|
|
591
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
452
592
|
}
|
|
453
593
|
|
|
454
594
|
//#endregion
|
|
455
|
-
//#region src/ai/
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
repo: "mlx-community/Josiefied-Qwen3-4B-abliterated-v1-8bit",
|
|
479
|
-
revision: "main",
|
|
480
|
-
path: "text-encoder",
|
|
481
|
-
files: [
|
|
482
|
-
"config.json",
|
|
483
|
-
"tokenizer.json",
|
|
484
|
-
"tokenizer_config.json",
|
|
485
|
-
"*.safetensors",
|
|
486
|
-
"*.safetensors.index.json"
|
|
487
|
-
]
|
|
488
|
-
},
|
|
489
|
-
transformer: {
|
|
490
|
-
repo: "moxin-org/FLUX.2-klein-4B-8bit-mlx",
|
|
491
|
-
revision: "main",
|
|
492
|
-
path: "transformer",
|
|
493
|
-
files: ["config.json", "quantized_8bit.safetensors"]
|
|
494
|
-
},
|
|
495
|
-
vae: {
|
|
496
|
-
repo: "black-forest-labs/FLUX.2-klein-4B",
|
|
497
|
-
revision: "main",
|
|
498
|
-
path: "vae",
|
|
499
|
-
files: [
|
|
500
|
-
"config.json",
|
|
501
|
-
"diffusion_pytorch_model.safetensors",
|
|
502
|
-
"diffusion_pytorch_model.safetensors.index.json",
|
|
503
|
-
"diffusion_pytorch_model-*.safetensors"
|
|
504
|
-
]
|
|
595
|
+
//#region src/ai/tokenizers.mjs
|
|
596
|
+
var DictionarySplitter = class {
|
|
597
|
+
/**
|
|
598
|
+
* @param dictionary The dictionary of words to use for splitting.
|
|
599
|
+
*/
|
|
600
|
+
constructor(dictionary) {
|
|
601
|
+
this.trie = this._build_trie(dictionary);
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Builds a trie from the given dictionary.
|
|
605
|
+
* @param dictionary The dictionary of words to build the trie from.
|
|
606
|
+
* @returns The root node of the trie.
|
|
607
|
+
* @private
|
|
608
|
+
*/
|
|
609
|
+
_build_trie(dictionary) {
|
|
610
|
+
const trie = /* @__PURE__ */ Object.create(null);
|
|
611
|
+
for (const word of dictionary) {
|
|
612
|
+
let node = trie;
|
|
613
|
+
for (let i = 0; i < word.length; ++i) {
|
|
614
|
+
const char = word[i];
|
|
615
|
+
node = node[char] ??= /* @__PURE__ */ Object.create(null);
|
|
616
|
+
}
|
|
617
|
+
node.end = word;
|
|
505
618
|
}
|
|
619
|
+
return trie;
|
|
506
620
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
"text_encoder-mlx-4bit/model.safetensors.index.json"
|
|
532
|
-
]
|
|
533
|
-
},
|
|
534
|
-
transformer: {
|
|
535
|
-
repo: "prism-ml/bonsai-image-ternary-4B-mlx-2bit",
|
|
536
|
-
revision: "2c24c81b934a658ba5590cf39088ba929985b4a8",
|
|
537
|
-
path: "transformer",
|
|
538
|
-
files: [
|
|
539
|
-
"transformer-packed-mflux/config.json",
|
|
540
|
-
"transformer-packed-mflux/diffusion_pytorch_model.safetensors",
|
|
541
|
-
"transformer-packed-mflux/quantization_config.json"
|
|
542
|
-
]
|
|
543
|
-
},
|
|
544
|
-
vae: {
|
|
545
|
-
repo: "prism-ml/bonsai-image-ternary-4B-mlx-2bit",
|
|
546
|
-
revision: "2c24c81b934a658ba5590cf39088ba929985b4a8",
|
|
547
|
-
path: "vae",
|
|
548
|
-
files: ["vae/config.json", "vae/diffusion_pytorch_model.safetensors"]
|
|
621
|
+
/**
|
|
622
|
+
* Splits the input text into tokens based on the dictionary.
|
|
623
|
+
* @param text The input text to split.
|
|
624
|
+
* @returns An array of tokens.
|
|
625
|
+
*/
|
|
626
|
+
split(text) {
|
|
627
|
+
const result = [];
|
|
628
|
+
const n = text.length;
|
|
629
|
+
let start = 0;
|
|
630
|
+
let i = 0;
|
|
631
|
+
while (i < n) {
|
|
632
|
+
let node = this.trie;
|
|
633
|
+
let match = null;
|
|
634
|
+
let j = i;
|
|
635
|
+
while (j < n && (node = node[text[j]])) {
|
|
636
|
+
if (node.end) match = node.end;
|
|
637
|
+
++j;
|
|
638
|
+
}
|
|
639
|
+
if (match) {
|
|
640
|
+
if (i > start) result.push(text.slice(start, i));
|
|
641
|
+
result.push(match);
|
|
642
|
+
i += match.length;
|
|
643
|
+
start = i;
|
|
644
|
+
} else ++i;
|
|
549
645
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
const imageModelManifests = [flux2Klein4B8BitAbliterated, ternaryBonsaiImage4B];
|
|
553
|
-
function imageModelManifest(model) {
|
|
554
|
-
return imageModelManifests.find((manifest) => manifest.model === model);
|
|
555
|
-
}
|
|
556
|
-
const flux2KleinModelFiles = [
|
|
557
|
-
{
|
|
558
|
-
repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
|
|
559
|
-
revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
|
|
560
|
-
source: "config.json",
|
|
561
|
-
destination: "text-encoder/config.json",
|
|
562
|
-
bytes: 937
|
|
563
|
-
},
|
|
564
|
-
{
|
|
565
|
-
repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
|
|
566
|
-
revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
|
|
567
|
-
source: "tokenizer.json",
|
|
568
|
-
destination: "text-encoder/tokenizer.json",
|
|
569
|
-
bytes: 11422654
|
|
570
|
-
},
|
|
571
|
-
{
|
|
572
|
-
repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
|
|
573
|
-
revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
|
|
574
|
-
source: "tokenizer_config.json",
|
|
575
|
-
destination: "text-encoder/tokenizer_config.json",
|
|
576
|
-
bytes: 9706
|
|
577
|
-
},
|
|
578
|
-
{
|
|
579
|
-
repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
|
|
580
|
-
revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
|
|
581
|
-
source: "model.safetensors",
|
|
582
|
-
destination: "text-encoder/model.safetensors",
|
|
583
|
-
bytes: 4274158989
|
|
584
|
-
},
|
|
585
|
-
{
|
|
586
|
-
repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
|
|
587
|
-
revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
|
|
588
|
-
source: "model.safetensors.index.json",
|
|
589
|
-
destination: "text-encoder/model.safetensors.index.json",
|
|
590
|
-
bytes: 63924
|
|
591
|
-
},
|
|
592
|
-
{
|
|
593
|
-
repo: flux2Klein4B8BitAbliterated.components.transformer.repo,
|
|
594
|
-
revision: flux2Klein4B8BitAbliterated.components.transformer.revision,
|
|
595
|
-
source: "transformer/config.json",
|
|
596
|
-
destination: "transformer/config.json",
|
|
597
|
-
bytes: 541
|
|
598
|
-
},
|
|
599
|
-
{
|
|
600
|
-
repo: flux2Klein4B8BitAbliterated.components.transformer.repo,
|
|
601
|
-
revision: flux2Klein4B8BitAbliterated.components.transformer.revision,
|
|
602
|
-
source: "transformer/quantized_8bit.safetensors",
|
|
603
|
-
destination: "transformer/quantized_8bit.safetensors",
|
|
604
|
-
bytes: 4360050983
|
|
605
|
-
},
|
|
606
|
-
{
|
|
607
|
-
repo: flux2Klein4B8BitAbliterated.components.vae.repo,
|
|
608
|
-
revision: flux2Klein4B8BitAbliterated.components.vae.revision,
|
|
609
|
-
source: "vae/config.json",
|
|
610
|
-
destination: "vae/config.json",
|
|
611
|
-
bytes: 821
|
|
612
|
-
},
|
|
613
|
-
{
|
|
614
|
-
repo: flux2Klein4B8BitAbliterated.components.vae.repo,
|
|
615
|
-
revision: flux2Klein4B8BitAbliterated.components.vae.revision,
|
|
616
|
-
source: "vae/diffusion_pytorch_model.safetensors",
|
|
617
|
-
destination: "vae/diffusion_pytorch_model.safetensors",
|
|
618
|
-
bytes: 168120878
|
|
619
|
-
}
|
|
620
|
-
];
|
|
621
|
-
const flux2KleinTotalBytes = flux2KleinModelFiles.reduce((total, file) => total + file.bytes, 0);
|
|
622
|
-
const bonsaiRepo = ternaryBonsaiImage4B.components.textEncoder.repo;
|
|
623
|
-
const bonsaiRevision = ternaryBonsaiImage4B.components.textEncoder.revision;
|
|
624
|
-
const ternaryBonsaiImageModelFiles = [
|
|
625
|
-
{
|
|
626
|
-
repo: bonsaiRepo,
|
|
627
|
-
revision: bonsaiRevision,
|
|
628
|
-
source: "text_encoder-mlx-4bit/config.json",
|
|
629
|
-
destination: "text-encoder/config.json",
|
|
630
|
-
bytes: 937
|
|
631
|
-
},
|
|
632
|
-
{
|
|
633
|
-
repo: bonsaiRepo,
|
|
634
|
-
revision: bonsaiRevision,
|
|
635
|
-
source: "text_encoder-mlx-4bit/tokenizer.json",
|
|
636
|
-
destination: "text-encoder/tokenizer.json",
|
|
637
|
-
bytes: 11422654
|
|
638
|
-
},
|
|
639
|
-
{
|
|
640
|
-
repo: bonsaiRepo,
|
|
641
|
-
revision: bonsaiRevision,
|
|
642
|
-
source: "text_encoder-mlx-4bit/tokenizer_config.json",
|
|
643
|
-
destination: "text-encoder/tokenizer_config.json",
|
|
644
|
-
bytes: 9706
|
|
645
|
-
},
|
|
646
|
-
{
|
|
647
|
-
repo: bonsaiRepo,
|
|
648
|
-
revision: bonsaiRevision,
|
|
649
|
-
source: "text_encoder-mlx-4bit/model.safetensors",
|
|
650
|
-
destination: "text-encoder/model.safetensors",
|
|
651
|
-
bytes: 2263022529
|
|
652
|
-
},
|
|
653
|
-
{
|
|
654
|
-
repo: bonsaiRepo,
|
|
655
|
-
revision: bonsaiRevision,
|
|
656
|
-
source: "text_encoder-mlx-4bit/model.safetensors.index.json",
|
|
657
|
-
destination: "text-encoder/model.safetensors.index.json",
|
|
658
|
-
bytes: 63924
|
|
659
|
-
},
|
|
660
|
-
{
|
|
661
|
-
repo: bonsaiRepo,
|
|
662
|
-
revision: bonsaiRevision,
|
|
663
|
-
source: "transformer-packed-mflux/config.json",
|
|
664
|
-
destination: "transformer/config.json",
|
|
665
|
-
bytes: 619
|
|
666
|
-
},
|
|
667
|
-
{
|
|
668
|
-
repo: bonsaiRepo,
|
|
669
|
-
revision: bonsaiRevision,
|
|
670
|
-
source: "transformer-packed-mflux/diffusion_pytorch_model.safetensors",
|
|
671
|
-
destination: "transformer/diffusion_pytorch_model.safetensors",
|
|
672
|
-
bytes: 1425271472
|
|
673
|
-
},
|
|
674
|
-
{
|
|
675
|
-
repo: bonsaiRepo,
|
|
676
|
-
revision: bonsaiRevision,
|
|
677
|
-
source: "transformer-packed-mflux/quantization_config.json",
|
|
678
|
-
destination: "transformer/quantization_config.json",
|
|
679
|
-
bytes: 5054
|
|
680
|
-
},
|
|
681
|
-
{
|
|
682
|
-
repo: bonsaiRepo,
|
|
683
|
-
revision: bonsaiRevision,
|
|
684
|
-
source: "vae/config.json",
|
|
685
|
-
destination: "vae/config.json",
|
|
686
|
-
bytes: 821
|
|
687
|
-
},
|
|
688
|
-
{
|
|
689
|
-
repo: bonsaiRepo,
|
|
690
|
-
revision: bonsaiRevision,
|
|
691
|
-
source: "vae/diffusion_pytorch_model.safetensors",
|
|
692
|
-
destination: "vae/diffusion_pytorch_model.safetensors",
|
|
693
|
-
bytes: 168120878
|
|
694
|
-
}
|
|
695
|
-
];
|
|
696
|
-
const ternaryBonsaiImageTotalBytes = ternaryBonsaiImageModelFiles.reduce((total, file) => total + file.bytes, 0);
|
|
697
|
-
function joinModelPath(root, component) {
|
|
698
|
-
return `${root.replace(/\/+$/, "")}/${component.replace(/^\/+/, "")}`;
|
|
699
|
-
}
|
|
700
|
-
function fluxImageModelPaths(path, manifest = flux2Klein4B8BitAbliterated) {
|
|
701
|
-
return {
|
|
702
|
-
path,
|
|
703
|
-
textEncoderPath: joinModelPath(path, manifest.components.textEncoder.path),
|
|
704
|
-
transformerPath: joinModelPath(path, manifest.components.transformer.path),
|
|
705
|
-
vaePath: joinModelPath(path, manifest.components.vae.path)
|
|
706
|
-
};
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
//#endregion
|
|
710
|
-
//#region src/ai/image-schedule.ts
|
|
711
|
-
function imageSigmaSchedule(schedules) {
|
|
712
|
-
const configured = schedules.filter((schedule) => schedule !== void 0);
|
|
713
|
-
if (configured.length === 0) return void 0;
|
|
714
|
-
const first = configured[0];
|
|
715
|
-
if (first.length === 0 || first.some((sigma, index) => !Number.isFinite(sigma) || sigma <= 0 || sigma > 1 || index > 0 && sigma >= first[index - 1])) throw new AIError("invalid_model", "LoRA sigmaSchedule must contain descending values greater than 0 and at most 1");
|
|
716
|
-
if (configured.some((schedule) => schedule.length !== first.length || schedule.some((sigma, index) => sigma !== first[index]))) throw new AIError("invalid_model", "Selected LoRAs require conflicting sigma schedules");
|
|
717
|
-
return [...first];
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
//#endregion
|
|
721
|
-
//#region src/ai/image-upscale.ts
|
|
722
|
-
function normalizeUpscaleScale(value) {
|
|
723
|
-
const scale = value ?? 2;
|
|
724
|
-
if (!Number.isFinite(scale) || scale <= 1 || scale > 4) throw new AIError("invalid_model", "Upscale scale must be greater than 1 and at most 4");
|
|
725
|
-
return scale;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
//#endregion
|
|
729
|
-
//#region src/ai/native.ts
|
|
730
|
-
function bridge() {
|
|
731
|
-
try {
|
|
732
|
-
return (0, expo.requireNativeModule)("AI");
|
|
733
|
-
} catch (error) {
|
|
734
|
-
throw new AIError("unsupported_runtime", "bleam/ai requires the Bleam native runtime", error);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
//#endregion
|
|
739
|
-
//#region src/ai/tokenizers.mjs
|
|
740
|
-
var DictionarySplitter = class {
|
|
741
|
-
/**
|
|
742
|
-
* @param dictionary The dictionary of words to use for splitting.
|
|
743
|
-
*/
|
|
744
|
-
constructor(dictionary) {
|
|
745
|
-
this.trie = this._build_trie(dictionary);
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* Builds a trie from the given dictionary.
|
|
749
|
-
* @param dictionary The dictionary of words to build the trie from.
|
|
750
|
-
* @returns The root node of the trie.
|
|
751
|
-
* @private
|
|
752
|
-
*/
|
|
753
|
-
_build_trie(dictionary) {
|
|
754
|
-
const trie = /* @__PURE__ */ Object.create(null);
|
|
755
|
-
for (const word of dictionary) {
|
|
756
|
-
let node = trie;
|
|
757
|
-
for (let i = 0; i < word.length; ++i) {
|
|
758
|
-
const char = word[i];
|
|
759
|
-
node = node[char] ??= /* @__PURE__ */ Object.create(null);
|
|
760
|
-
}
|
|
761
|
-
node.end = word;
|
|
762
|
-
}
|
|
763
|
-
return trie;
|
|
764
|
-
}
|
|
765
|
-
/**
|
|
766
|
-
* Splits the input text into tokens based on the dictionary.
|
|
767
|
-
* @param text The input text to split.
|
|
768
|
-
* @returns An array of tokens.
|
|
769
|
-
*/
|
|
770
|
-
split(text) {
|
|
771
|
-
const result = [];
|
|
772
|
-
const n = text.length;
|
|
773
|
-
let start = 0;
|
|
774
|
-
let i = 0;
|
|
775
|
-
while (i < n) {
|
|
776
|
-
let node = this.trie;
|
|
777
|
-
let match = null;
|
|
778
|
-
let j = i;
|
|
779
|
-
while (j < n && (node = node[text[j]])) {
|
|
780
|
-
if (node.end) match = node.end;
|
|
781
|
-
++j;
|
|
782
|
-
}
|
|
783
|
-
if (match) {
|
|
784
|
-
if (i > start) result.push(text.slice(start, i));
|
|
785
|
-
result.push(match);
|
|
786
|
-
i += match.length;
|
|
787
|
-
start = i;
|
|
788
|
-
} else ++i;
|
|
789
|
-
}
|
|
790
|
-
if (start < n) result.push(text.slice(start));
|
|
791
|
-
return result;
|
|
646
|
+
if (start < n) result.push(text.slice(start));
|
|
647
|
+
return result;
|
|
792
648
|
}
|
|
793
649
|
};
|
|
794
650
|
var DictionarySplitter_default = DictionarySplitter;
|
|
@@ -873,7 +729,7 @@ var create_pattern = (pattern, invert = true) => {
|
|
|
873
729
|
return null;
|
|
874
730
|
}
|
|
875
731
|
};
|
|
876
|
-
var escape_reg_exp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
732
|
+
var escape_reg_exp = (string$1) => string$1.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
877
733
|
var fuse_unk = (arr, tokens_to_ids, unk_token_id) => {
|
|
878
734
|
const fused = [];
|
|
879
735
|
let i = 0;
|
|
@@ -2595,16 +2451,16 @@ var ByteFallback = class extends Decoder_default {
|
|
|
2595
2451
|
if (bytes !== null) previous_byte_tokens.push(bytes);
|
|
2596
2452
|
else {
|
|
2597
2453
|
if (previous_byte_tokens.length > 0) {
|
|
2598
|
-
const string = this.text_decoder.decode(Uint8Array.from(previous_byte_tokens));
|
|
2599
|
-
new_tokens.push(string);
|
|
2454
|
+
const string$1 = this.text_decoder.decode(Uint8Array.from(previous_byte_tokens));
|
|
2455
|
+
new_tokens.push(string$1);
|
|
2600
2456
|
previous_byte_tokens = [];
|
|
2601
2457
|
}
|
|
2602
2458
|
new_tokens.push(token);
|
|
2603
2459
|
}
|
|
2604
2460
|
}
|
|
2605
2461
|
if (previous_byte_tokens.length > 0) {
|
|
2606
|
-
const string = this.text_decoder.decode(Uint8Array.from(previous_byte_tokens));
|
|
2607
|
-
new_tokens.push(string);
|
|
2462
|
+
const string$1 = this.text_decoder.decode(Uint8Array.from(previous_byte_tokens));
|
|
2463
|
+
new_tokens.push(string$1);
|
|
2608
2464
|
previous_byte_tokens = [];
|
|
2609
2465
|
}
|
|
2610
2466
|
return new_tokens;
|
|
@@ -2800,1857 +2656,2150 @@ var Tokenizer = class {
|
|
|
2800
2656
|
var Tokenizer_default = Tokenizer;
|
|
2801
2657
|
|
|
2802
2658
|
//#endregion
|
|
2803
|
-
//#region src/ai/
|
|
2804
|
-
function
|
|
2805
|
-
|
|
2806
|
-
}
|
|
2807
|
-
|
|
2808
|
-
const
|
|
2809
|
-
if (
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2659
|
+
//#region src/ai/bonsai-generation.ts
|
|
2660
|
+
async function prepareBonsaiGeneration(input) {
|
|
2661
|
+
const state = await verifyBonsaiTextModel(input.model, { path: input.path });
|
|
2662
|
+
if (!state.verified) throw new AIError("model_unavailable", `${input.model.id} is not prepared. Call prepareTextModel() before generation.`);
|
|
2663
|
+
const tokenizer = new Tokenizer_default(await require_files.files.file(state.path, "tokenizer.json").json(), await require_files.files.file(state.path, "tokenizer_config.json").json());
|
|
2664
|
+
const inputIds = tokenizer.encode(formatMessages(messagesFromInput(input))).ids.map(Number);
|
|
2665
|
+
if (!inputIds.length || inputIds.some((token) => !Number.isInteger(token))) throw new AIError("invalid_message", "Tokenizer returned invalid token ids");
|
|
2666
|
+
return {
|
|
2667
|
+
request: {
|
|
2668
|
+
id: input.id,
|
|
2669
|
+
model: input.model.id,
|
|
2670
|
+
modelPath: state.path,
|
|
2671
|
+
inputIds,
|
|
2672
|
+
endTokenIds: endTokenIds(input.model.id),
|
|
2673
|
+
maxTokens: input.maxTokens,
|
|
2674
|
+
temperature: input.temperature,
|
|
2675
|
+
seed: input.seed
|
|
2676
|
+
},
|
|
2677
|
+
decode(tokenIds) {
|
|
2678
|
+
return tokenIds.length ? tokenizer.decode(tokenIds, { skip_special_tokens: true }) : "";
|
|
2679
|
+
}
|
|
2818
2680
|
};
|
|
2819
|
-
throw new AIError("invalid_model", `Unsupported image model: ${id}`);
|
|
2820
2681
|
}
|
|
2821
|
-
function
|
|
2822
|
-
|
|
2682
|
+
function messagesFromInput(input) {
|
|
2683
|
+
if (input.messages) {
|
|
2684
|
+
if (input.system !== void 0 || input.prompt !== void 0) throw new AIError("invalid_message", "messages cannot be combined with system or prompt");
|
|
2685
|
+
if (!input.messages.length) throw new AIError("invalid_message", "Expected at least one message");
|
|
2686
|
+
return input.messages;
|
|
2687
|
+
}
|
|
2688
|
+
if (!input.prompt?.trim()) throw new AIError("invalid_message", "Expected prompt");
|
|
2689
|
+
return [...input.system ? [{
|
|
2690
|
+
role: "system",
|
|
2691
|
+
content: input.system
|
|
2692
|
+
}] : [], {
|
|
2693
|
+
role: "user",
|
|
2694
|
+
content: input.prompt
|
|
2695
|
+
}];
|
|
2823
2696
|
}
|
|
2824
|
-
function
|
|
2825
|
-
|
|
2697
|
+
function formatMessages(messages) {
|
|
2698
|
+
if (messages[0]?.role === "assistant") throw new AIError("invalid_message", "Conversation cannot begin with assistant");
|
|
2699
|
+
const parts = messages.map((message, index) => {
|
|
2700
|
+
if (!message.content.trim()) throw new AIError("invalid_message", `Message ${index + 1} is empty`);
|
|
2701
|
+
if (message.role === "system" && index !== 0) throw new AIError("invalid_message", "System message must be first");
|
|
2702
|
+
return `<|im_start|>${message.role}\n${message.content}<|im_end|>\n`;
|
|
2703
|
+
});
|
|
2704
|
+
parts.push("<|im_start|>assistant\n<think>\n\n</think>\n\n");
|
|
2705
|
+
return parts.join("");
|
|
2826
2706
|
}
|
|
2827
|
-
function
|
|
2828
|
-
return
|
|
2707
|
+
function endTokenIds(model) {
|
|
2708
|
+
return model === "ternary-bonsai-27b" ? [248044, 248046] : [151643, 151645];
|
|
2829
2709
|
}
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2710
|
+
|
|
2711
|
+
//#endregion
|
|
2712
|
+
//#region src/ai/native.ts
|
|
2713
|
+
function bridge() {
|
|
2714
|
+
try {
|
|
2715
|
+
return (0, expo.requireNativeModule)("AI");
|
|
2716
|
+
} catch (error) {
|
|
2717
|
+
throw new AIError("unsupported_runtime", "bleam/ai requires the Bleam native runtime", error);
|
|
2718
|
+
}
|
|
2834
2719
|
}
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2720
|
+
|
|
2721
|
+
//#endregion
|
|
2722
|
+
//#region src/ai/chat-executor.ts
|
|
2723
|
+
function generationId() {
|
|
2724
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
2725
|
+
}
|
|
2726
|
+
function foundationRequest(options, id) {
|
|
2727
|
+
if (options.temperature !== void 0 && (!Number.isFinite(options.temperature) || options.temperature < 0)) throw new AIError("invalid_message", "temperature must be a non-negative number");
|
|
2728
|
+
if (options.maxTokens !== void 0 && (!Number.isInteger(options.maxTokens) || options.maxTokens <= 0)) throw new AIError("invalid_message", "maxTokens must be a positive integer");
|
|
2729
|
+
if (options.messages.length === 0) throw new AIError("invalid_message", "Expected at least one message");
|
|
2730
|
+
let system;
|
|
2731
|
+
const conversation = [];
|
|
2732
|
+
for (const [index, message] of options.messages.entries()) {
|
|
2733
|
+
if (!message.content.trim()) throw new AIError("invalid_message", `Message ${index + 1} is empty`);
|
|
2734
|
+
if (message.role === "system") {
|
|
2735
|
+
if (index !== 0 || system !== void 0) throw new AIError("invalid_message", "System message must be first");
|
|
2736
|
+
system = message.content;
|
|
2737
|
+
} else conversation.push({
|
|
2738
|
+
role: message.role,
|
|
2739
|
+
content: message.content
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
if (conversation[0]?.role === "assistant") throw new AIError("invalid_message", "Conversation cannot begin with assistant");
|
|
2743
|
+
const final = conversation.at(-1);
|
|
2744
|
+
if (final?.role !== "user") throw new AIError("invalid_message", "Conversation must end with a user message");
|
|
2745
|
+
return {
|
|
2746
|
+
id,
|
|
2747
|
+
system,
|
|
2748
|
+
prompt: final.content,
|
|
2749
|
+
messages: conversation.slice(0, -1),
|
|
2750
|
+
temperature: options.temperature,
|
|
2751
|
+
maxTokens: options.maxTokens
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
async function* streamChat(options) {
|
|
2755
|
+
if (options.model) {
|
|
2756
|
+
yield* streamBonsaiChat(options);
|
|
2757
|
+
return;
|
|
2758
|
+
}
|
|
2759
|
+
const id = generationId();
|
|
2760
|
+
const native = bridge();
|
|
2761
|
+
const updates = [];
|
|
2762
|
+
let wake;
|
|
2763
|
+
let settled = false;
|
|
2764
|
+
let failure;
|
|
2765
|
+
const updateSubscription = native.addListener?.("textGenerationUpdate", (event) => {
|
|
2766
|
+
if (event.id !== id) return;
|
|
2767
|
+
updates.push(event.content);
|
|
2768
|
+
wake?.();
|
|
2769
|
+
wake = void 0;
|
|
2843
2770
|
});
|
|
2844
|
-
|
|
2771
|
+
const abort = () => void native.cancelTextGeneration?.(id);
|
|
2772
|
+
options.signal.addEventListener("abort", abort, { once: true });
|
|
2773
|
+
const completion = native.streamGenerate?.(foundationRequest(options, id)).catch((error) => {
|
|
2774
|
+
failure = error;
|
|
2775
|
+
}).finally(() => {
|
|
2776
|
+
settled = true;
|
|
2777
|
+
wake?.();
|
|
2778
|
+
wake = void 0;
|
|
2779
|
+
});
|
|
2780
|
+
try {
|
|
2781
|
+
if (options.signal.aborted) {
|
|
2782
|
+
abort();
|
|
2783
|
+
throw new AIError("generation_canceled", "Generation canceled");
|
|
2784
|
+
}
|
|
2785
|
+
if (!completion) throw new AIError("unsupported_runtime", "Streaming is unavailable in this runtime");
|
|
2786
|
+
while (!settled || updates.length > 0) if (updates.length > 0) yield updates.shift();
|
|
2787
|
+
else await new Promise((resolve) => {
|
|
2788
|
+
wake = resolve;
|
|
2789
|
+
});
|
|
2790
|
+
await completion;
|
|
2791
|
+
if (failure) throw mapNativeError(failure);
|
|
2792
|
+
} finally {
|
|
2793
|
+
if (!settled) await native.cancelTextGeneration?.(id);
|
|
2794
|
+
options.signal.removeEventListener("abort", abort);
|
|
2795
|
+
updateSubscription?.remove();
|
|
2796
|
+
}
|
|
2845
2797
|
}
|
|
2846
|
-
function
|
|
2847
|
-
const
|
|
2848
|
-
const
|
|
2849
|
-
|
|
2798
|
+
async function* streamBonsaiChat(options) {
|
|
2799
|
+
const id = generationId();
|
|
2800
|
+
const native = bridge();
|
|
2801
|
+
if (options.signal.aborted) throw new AIError("generation_canceled", "Generation canceled");
|
|
2802
|
+
const prepared = await prepareBonsaiGeneration({
|
|
2803
|
+
id,
|
|
2804
|
+
model: options.model,
|
|
2805
|
+
path: options.path,
|
|
2806
|
+
messages: options.messages,
|
|
2807
|
+
temperature: options.temperature,
|
|
2808
|
+
maxTokens: options.maxTokens,
|
|
2809
|
+
seed: options.seed
|
|
2810
|
+
});
|
|
2811
|
+
if (options.signal.aborted) throw new AIError("generation_canceled", "Generation canceled");
|
|
2812
|
+
const updates = [];
|
|
2813
|
+
let wake;
|
|
2814
|
+
let settled = false;
|
|
2815
|
+
let failure;
|
|
2816
|
+
const updateSubscription = native.addListener?.("localTextGenerationUpdate", (event) => {
|
|
2817
|
+
if (event.id !== id) return;
|
|
2818
|
+
updates.push(event.tokenIds);
|
|
2819
|
+
wake?.();
|
|
2820
|
+
wake = void 0;
|
|
2821
|
+
});
|
|
2822
|
+
const abort = () => void native.cancelTextGeneration?.(id);
|
|
2823
|
+
options.signal.addEventListener("abort", abort, { once: true });
|
|
2824
|
+
const completion = native.streamLocalText?.(prepared.request).catch((error) => {
|
|
2825
|
+
failure = error;
|
|
2826
|
+
return [];
|
|
2827
|
+
}).finally(() => {
|
|
2828
|
+
settled = true;
|
|
2829
|
+
wake?.();
|
|
2830
|
+
wake = void 0;
|
|
2831
|
+
});
|
|
2832
|
+
try {
|
|
2833
|
+
if (!completion) throw new AIError("unsupported_runtime", "Local text streaming is unavailable in this runtime");
|
|
2834
|
+
while (!settled || updates.length > 0) if (updates.length > 0) yield prepared.decode(updates.shift());
|
|
2835
|
+
else await new Promise((resolve) => {
|
|
2836
|
+
wake = resolve;
|
|
2837
|
+
});
|
|
2838
|
+
await completion;
|
|
2839
|
+
if (failure) throw mapNativeError(failure);
|
|
2840
|
+
} finally {
|
|
2841
|
+
if (!settled) await native.cancelTextGeneration?.(id);
|
|
2842
|
+
options.signal.removeEventListener("abort", abort);
|
|
2843
|
+
updateSubscription?.remove();
|
|
2844
|
+
}
|
|
2850
2845
|
}
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
const
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
"9:16": [9, 16],
|
|
2858
|
-
"4:3": [4, 3],
|
|
2859
|
-
"3:2": [3, 2],
|
|
2860
|
-
"2:3": [2, 3],
|
|
2861
|
-
"21:9": [21, 9]
|
|
2846
|
+
|
|
2847
|
+
//#endregion
|
|
2848
|
+
//#region src/ai/chat.tsx
|
|
2849
|
+
const chatShape = {
|
|
2850
|
+
title: require_schema.string({ default: "New Chat" }),
|
|
2851
|
+
status: require_schema.string({ default: "ready" })
|
|
2862
2852
|
};
|
|
2863
|
-
|
|
2864
|
-
|
|
2853
|
+
const errorShape = {
|
|
2854
|
+
name: require_schema.string(),
|
|
2855
|
+
message: require_schema.string(),
|
|
2856
|
+
code: require_schema.string(),
|
|
2857
|
+
stage: require_schema.string()
|
|
2858
|
+
};
|
|
2859
|
+
const messageShape = {
|
|
2860
|
+
chatId: require_schema.string(),
|
|
2861
|
+
role: require_schema.string(),
|
|
2862
|
+
status: require_schema.string(),
|
|
2863
|
+
content: require_schema.string({ default: "" }),
|
|
2864
|
+
attempt: require_schema.number({ default: 1 }),
|
|
2865
|
+
sequence: require_schema.number(),
|
|
2866
|
+
error: require_schema.object(errorShape),
|
|
2867
|
+
startedAt: require_schema.string(),
|
|
2868
|
+
completedAt: require_schema.string()
|
|
2869
|
+
};
|
|
2870
|
+
const chats = require_state.atomCollection("__bleam_ai_chats", chatShape);
|
|
2871
|
+
const chatMessages = require_state.atomCollection("__bleam_ai_chat_messages", messageShape);
|
|
2872
|
+
var CollectionChatRepository = class {
|
|
2873
|
+
async create(input) {
|
|
2874
|
+
const insert = require_state.mutateAtom(chats, (mutation) => mutation.insert());
|
|
2875
|
+
const id = await require_state.internalStore.set(require_state.internalJotaiWritableAtom(insert), {
|
|
2876
|
+
title: input.title ?? "New Chat",
|
|
2877
|
+
status: "ready"
|
|
2878
|
+
});
|
|
2879
|
+
return this.requireChat(id);
|
|
2880
|
+
}
|
|
2881
|
+
async get(id) {
|
|
2882
|
+
const row = require_state.queryAtom(chats, (query) => query.row(id));
|
|
2883
|
+
return chatFromRow(await require_state.internalStore.get(require_state.internalJotaiAtom(row)));
|
|
2884
|
+
}
|
|
2885
|
+
async list() {
|
|
2886
|
+
const ids = require_state.queryAtom(chats, (query) => query.sortBy("updatedAt", "desc").ids());
|
|
2887
|
+
const values = await require_state.internalStore.get(require_state.internalJotaiAtom(ids));
|
|
2888
|
+
return Promise.all(values.map((id) => this.requireChat(id)));
|
|
2889
|
+
}
|
|
2890
|
+
async messages(chatId) {
|
|
2891
|
+
const ids = require_state.queryAtom(chatMessages, (query) => query.where({ chatId }).sortBy("sequence").ids());
|
|
2892
|
+
const values = await require_state.internalStore.get(require_state.internalJotaiAtom(ids));
|
|
2893
|
+
return (await Promise.all(values.map((id) => this.messageRow(id)))).flatMap((row) => row ? [messageFromRow(row)] : []);
|
|
2894
|
+
}
|
|
2895
|
+
async acceptSend(chatId, content) {
|
|
2896
|
+
const transaction = require_state.transactionAtom(async (tx) => {
|
|
2897
|
+
const chat$1 = await tx.get(chats, chatId);
|
|
2898
|
+
if (!chat$1) throw new Error(`Chat not found: ${chatId}`);
|
|
2899
|
+
if (isActiveStatus(chat$1.data.status)) throw new Error("This chat already has an active generation");
|
|
2900
|
+
const previousIds = await tx.query(chatMessages, (query) => query.where({ chatId }).sortBy("sequence", "desc").take(1).ids());
|
|
2901
|
+
const sequence = ((previousIds[0] ? await tx.get(chatMessages, previousIds[0]) : null)?.data.sequence ?? 0) + 1;
|
|
2902
|
+
const userId = await tx.insert(chatMessages, {
|
|
2903
|
+
chatId,
|
|
2904
|
+
role: "user",
|
|
2905
|
+
status: "completed",
|
|
2906
|
+
content,
|
|
2907
|
+
attempt: 1,
|
|
2908
|
+
sequence,
|
|
2909
|
+
completedAt: timestamp()
|
|
2910
|
+
});
|
|
2911
|
+
const assistantId = await tx.insert(chatMessages, {
|
|
2912
|
+
chatId,
|
|
2913
|
+
role: "assistant",
|
|
2914
|
+
status: "queued",
|
|
2915
|
+
content: "",
|
|
2916
|
+
attempt: 1,
|
|
2917
|
+
sequence: sequence + 1
|
|
2918
|
+
});
|
|
2919
|
+
return acceptedFromTransaction(tx, await tx.update(chats, chatId, { status: "queued" }), assistantId, userId);
|
|
2920
|
+
});
|
|
2921
|
+
return require_state.internalStore.set(require_state.internalJotaiWritableAtom(transaction));
|
|
2922
|
+
}
|
|
2923
|
+
async start(chatId, messageId) {
|
|
2924
|
+
return this.transition(chatId, messageId, async (tx, _chat, message) => {
|
|
2925
|
+
if (message.data.status !== "queued") throw new Error("Only queued messages can start");
|
|
2926
|
+
const startedAt = timestamp();
|
|
2927
|
+
const assistant = await tx.update(chatMessages, messageId, {
|
|
2928
|
+
status: "generating",
|
|
2929
|
+
startedAt
|
|
2930
|
+
});
|
|
2931
|
+
return acceptedFromTransaction(tx, await tx.update(chats, chatId, { status: "generating" }), assistant.id);
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
async updateContent(message) {
|
|
2935
|
+
const update = require_state.mutateAtom(chatMessages, (mutation) => mutation.update(message.id));
|
|
2936
|
+
return messageFromRow(await require_state.internalStore.set(require_state.internalJotaiWritableAtom(update), { content: message.content }));
|
|
2937
|
+
}
|
|
2938
|
+
async markCanceling(chatId, messageId) {
|
|
2939
|
+
return this.transition(chatId, messageId, async (tx, _chat, message) => {
|
|
2940
|
+
if (!isActiveStatus(message.data.status ?? "")) throw new Error("Message is not active");
|
|
2941
|
+
const assistant = await tx.update(chatMessages, messageId, { status: "canceling" });
|
|
2942
|
+
return acceptedFromTransaction(tx, await tx.update(chats, chatId, { status: "canceling" }), assistant.id);
|
|
2943
|
+
});
|
|
2944
|
+
}
|
|
2945
|
+
async finish(chatId, messageId, status, content, error) {
|
|
2946
|
+
return this.transition(chatId, messageId, async (tx, _chat) => {
|
|
2947
|
+
const completedAt = timestamp();
|
|
2948
|
+
const assistant = await tx.update(chatMessages, messageId, {
|
|
2949
|
+
status,
|
|
2950
|
+
content,
|
|
2951
|
+
error,
|
|
2952
|
+
completedAt
|
|
2953
|
+
});
|
|
2954
|
+
return acceptedFromTransaction(tx, await tx.update(chats, chatId, { status: status === "completed" ? "ready" : status }), assistant.id);
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
async retry(chatId, messageId) {
|
|
2958
|
+
return this.transition(chatId, messageId, async (tx, _chat, message) => {
|
|
2959
|
+
const ids = await tx.query(chatMessages, (query) => query.where({ chatId }).sortBy("sequence", "desc").ids());
|
|
2960
|
+
const rows = await Promise.all(ids.map((id) => tx.get(chatMessages, id)));
|
|
2961
|
+
if (rows.find((row) => row?.data.role === "assistant")?.id !== messageId) throw new Error("Only the latest assistant message can be retried");
|
|
2962
|
+
if (message.data.status !== "failed" && message.data.status !== "canceled") throw new Error("Only failed or canceled messages can be retried");
|
|
2963
|
+
const assistant = await tx.update(chatMessages, messageId, {
|
|
2964
|
+
status: "queued",
|
|
2965
|
+
content: "",
|
|
2966
|
+
attempt: message.data.attempt + 1,
|
|
2967
|
+
error: void 0,
|
|
2968
|
+
startedAt: void 0,
|
|
2969
|
+
completedAt: void 0
|
|
2970
|
+
});
|
|
2971
|
+
const nextChat = await tx.update(chats, chatId, { status: "queued" });
|
|
2972
|
+
const user = rows.find((row) => row?.data.role === "user" && (row.data.sequence ?? 0) < (assistant.data.sequence ?? 0));
|
|
2973
|
+
return acceptedFromTransaction(tx, nextChat, assistant.id, user?.id);
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
async setTitle(id, title) {
|
|
2977
|
+
const update = require_state.mutateAtom(chats, (mutation) => mutation.update(id));
|
|
2978
|
+
return chatFromRow(await require_state.internalStore.set(require_state.internalJotaiWritableAtom(update), { title }));
|
|
2979
|
+
}
|
|
2980
|
+
async remove(id) {
|
|
2981
|
+
const transaction = require_state.transactionAtom(async (tx) => {
|
|
2982
|
+
const chat$1 = await tx.get(chats, id);
|
|
2983
|
+
if (!chat$1) return false;
|
|
2984
|
+
if (isActiveStatus(chat$1.data.status)) throw new Error("Active chats cannot be removed");
|
|
2985
|
+
const ids = await tx.query(chatMessages, (query) => query.where({ chatId: id }).ids());
|
|
2986
|
+
for (const messageId of ids) await tx.remove(chatMessages, messageId);
|
|
2987
|
+
return tx.remove(chats, id);
|
|
2988
|
+
});
|
|
2989
|
+
return require_state.internalStore.set(require_state.internalJotaiWritableAtom(transaction));
|
|
2990
|
+
}
|
|
2991
|
+
async recoverActive() {
|
|
2992
|
+
const transaction = require_state.transactionAtom(async (tx) => {
|
|
2993
|
+
const chatIds = await tx.query(chats, (query) => query.ids());
|
|
2994
|
+
for (const chatId of chatIds) {
|
|
2995
|
+
const chat$1 = await tx.get(chats, chatId);
|
|
2996
|
+
if (!chat$1) continue;
|
|
2997
|
+
const messageIds = await tx.query(chatMessages, (query) => query.where({ chatId }).ids());
|
|
2998
|
+
let recoveredMessage = false;
|
|
2999
|
+
for (const messageId of messageIds) {
|
|
3000
|
+
const message = await tx.get(chatMessages, messageId);
|
|
3001
|
+
if (message?.data.role === "assistant" && isActiveStatus(message.data.status ?? "")) {
|
|
3002
|
+
await tx.update(chatMessages, messageId, {
|
|
3003
|
+
status: "canceled",
|
|
3004
|
+
error: recoveryError(),
|
|
3005
|
+
completedAt: timestamp()
|
|
3006
|
+
});
|
|
3007
|
+
recoveredMessage = true;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
if (recoveredMessage || isActiveStatus(chat$1.data.status)) await tx.update(chats, chatId, { status: "canceled" });
|
|
3011
|
+
}
|
|
3012
|
+
});
|
|
3013
|
+
await require_state.internalStore.set(require_state.internalJotaiWritableAtom(transaction));
|
|
3014
|
+
}
|
|
3015
|
+
async requireChat(id) {
|
|
3016
|
+
const chat$1 = await this.get(id);
|
|
3017
|
+
if (!chat$1) throw new Error(`Chat not found: ${id}`);
|
|
3018
|
+
return chat$1;
|
|
3019
|
+
}
|
|
3020
|
+
async messageRow(id) {
|
|
3021
|
+
const row = require_state.queryAtom(chatMessages, (query) => query.row(id));
|
|
3022
|
+
return require_state.internalStore.get(require_state.internalJotaiAtom(row));
|
|
3023
|
+
}
|
|
3024
|
+
async transition(chatId, messageId, action) {
|
|
3025
|
+
const transaction = require_state.transactionAtom(async (tx) => {
|
|
3026
|
+
const chat$1 = await tx.get(chats, chatId);
|
|
3027
|
+
const message = await tx.get(chatMessages, messageId);
|
|
3028
|
+
if (!chat$1) throw new Error(`Chat not found: ${chatId}`);
|
|
3029
|
+
if (!message || message.data.chatId !== chatId) throw new Error(`Message not found: ${messageId}`);
|
|
3030
|
+
return action(tx, chat$1, message);
|
|
3031
|
+
});
|
|
3032
|
+
return require_state.internalStore.set(require_state.internalJotaiWritableAtom(transaction));
|
|
3033
|
+
}
|
|
3034
|
+
};
|
|
3035
|
+
const managerKey = Symbol.for("bleam.ai.chatManager.v1");
|
|
3036
|
+
function managedChats() {
|
|
3037
|
+
const scope = globalThis;
|
|
3038
|
+
scope[managerKey] ??= new ChatManager(new CollectionChatRepository(), { stream(options) {
|
|
3039
|
+
return streamChat(options);
|
|
3040
|
+
} });
|
|
3041
|
+
return scope[managerKey];
|
|
3042
|
+
}
|
|
3043
|
+
const chat = {
|
|
3044
|
+
create(input = {}) {
|
|
3045
|
+
return managedChats().create(input);
|
|
3046
|
+
},
|
|
3047
|
+
get(id) {
|
|
3048
|
+
return managedChats().get(id);
|
|
3049
|
+
},
|
|
3050
|
+
list() {
|
|
3051
|
+
return managedChats().list();
|
|
3052
|
+
},
|
|
3053
|
+
send(id, content, options) {
|
|
3054
|
+
return managedChats().send(id, content, options);
|
|
3055
|
+
},
|
|
3056
|
+
cancel(id) {
|
|
3057
|
+
return managedChats().cancel(id);
|
|
3058
|
+
},
|
|
3059
|
+
retry(id, messageId, options) {
|
|
3060
|
+
return managedChats().retry(id, messageId, options);
|
|
3061
|
+
},
|
|
3062
|
+
setTitle(id, title) {
|
|
3063
|
+
return managedChats().setTitle(id, title);
|
|
3064
|
+
},
|
|
3065
|
+
remove(id) {
|
|
3066
|
+
return managedChats().remove(id);
|
|
3067
|
+
}
|
|
3068
|
+
};
|
|
3069
|
+
function useChat(id, options = {}) {
|
|
3070
|
+
const manager = managedChats();
|
|
3071
|
+
(0, react.use)(manager.ready);
|
|
3072
|
+
const snapshot = (0, react.useSyncExternalStore)((listener) => manager.subscribeChat(id, listener), () => manager.getChatSnapshot(id));
|
|
3073
|
+
const generationOptions = optionsWithoutCallbacks(options);
|
|
3074
|
+
const callbacks = {
|
|
3075
|
+
onFinish: options.onFinish,
|
|
3076
|
+
onError: options.onError
|
|
3077
|
+
};
|
|
3078
|
+
const status = snapshot.chat?.status ?? "not-found";
|
|
3079
|
+
const active = status === "queued" || status === "generating" || status === "canceling";
|
|
3080
|
+
const error = snapshot.messages.findLast((message) => message.role === "assistant")?.error;
|
|
3081
|
+
return {
|
|
3082
|
+
chat: snapshot.chat,
|
|
3083
|
+
messages: snapshot.messages,
|
|
3084
|
+
status,
|
|
3085
|
+
error: error ?? null,
|
|
3086
|
+
isGenerating: active,
|
|
3087
|
+
canSend: snapshot.chat !== null && !active,
|
|
3088
|
+
canCancel: status === "queued" || status === "generating",
|
|
3089
|
+
send: (content, invocation) => manager.send(id, content, mergeOptions(generationOptions, invocation), callbacks),
|
|
3090
|
+
cancel: () => manager.cancel(id),
|
|
3091
|
+
retry: (messageId, invocation) => manager.retry(id, messageId, mergeOptions(generationOptions, invocation), callbacks),
|
|
3092
|
+
setTitle: (title) => manager.setTitle(id, title),
|
|
3093
|
+
remove: () => manager.remove(id)
|
|
3094
|
+
};
|
|
2865
3095
|
}
|
|
2866
|
-
function
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
3096
|
+
function useChatList() {
|
|
3097
|
+
const manager = managedChats();
|
|
3098
|
+
(0, react.use)(manager.ready);
|
|
3099
|
+
return (0, react.useSyncExternalStore)((listener) => manager.subscribeList(listener), () => manager.getListSnapshot());
|
|
3100
|
+
}
|
|
3101
|
+
async function acceptedFromTransaction(tx, chatRow, assistantId, userId) {
|
|
3102
|
+
const ids = await tx.query(chatMessages, (query) => query.where({ chatId: chatRow.id }).sortBy("sequence").ids());
|
|
3103
|
+
const messages = (await Promise.all(ids.map((id) => tx.get(chatMessages, id)))).flatMap((row) => row ? [messageFromRow(row)] : []);
|
|
3104
|
+
const assistantMessage = messages.find((message) => message.id === assistantId);
|
|
3105
|
+
if (assistantMessage?.role !== "assistant") throw new Error(`Assistant message not found: ${assistantId}`);
|
|
3106
|
+
const userMessage = userId ? messages.find((message) => message.id === userId) : void 0;
|
|
2871
3107
|
return {
|
|
2872
|
-
|
|
2873
|
-
|
|
3108
|
+
chatId: chatRow.id,
|
|
3109
|
+
userMessageId: userMessage?.id ?? "",
|
|
3110
|
+
assistantMessageId: assistantId,
|
|
3111
|
+
chat: chatFromRow(chatRow),
|
|
3112
|
+
userMessage,
|
|
3113
|
+
assistantMessage,
|
|
3114
|
+
messages
|
|
2874
3115
|
};
|
|
2875
3116
|
}
|
|
2876
|
-
function
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
}
|
|
2885
|
-
width: maxLongSide * widthRatio / heightRatio,
|
|
2886
|
-
height: maxLongSide
|
|
2887
|
-
}, maxLongSide);
|
|
3117
|
+
function chatFromRow(row) {
|
|
3118
|
+
if (!row) return null;
|
|
3119
|
+
return {
|
|
3120
|
+
id: row.id,
|
|
3121
|
+
title: row.data.title,
|
|
3122
|
+
status: row.data.status,
|
|
3123
|
+
createdAt: row.createdAt,
|
|
3124
|
+
updatedAt: row.updatedAt
|
|
3125
|
+
};
|
|
2888
3126
|
}
|
|
2889
|
-
function
|
|
2890
|
-
return
|
|
3127
|
+
function messageFromRow(row) {
|
|
3128
|
+
return {
|
|
3129
|
+
id: row.id,
|
|
3130
|
+
chatId: row.data.chatId,
|
|
3131
|
+
role: row.data.role,
|
|
3132
|
+
status: row.data.status,
|
|
3133
|
+
content: row.data.content,
|
|
3134
|
+
attempt: row.data.attempt,
|
|
3135
|
+
sequence: row.data.sequence,
|
|
3136
|
+
error: row.data.error,
|
|
3137
|
+
createdAt: row.createdAt,
|
|
3138
|
+
updatedAt: row.updatedAt,
|
|
3139
|
+
startedAt: row.data.startedAt,
|
|
3140
|
+
completedAt: row.data.completedAt
|
|
3141
|
+
};
|
|
2891
3142
|
}
|
|
2892
|
-
function
|
|
2893
|
-
const
|
|
2894
|
-
|
|
2895
|
-
return trimmed.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3143
|
+
function optionsWithoutCallbacks(options) {
|
|
3144
|
+
const { onFinish: _onFinish, onError: _onError,...generation } = options;
|
|
3145
|
+
return generation;
|
|
2896
3146
|
}
|
|
2897
|
-
function
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
3147
|
+
function mergeOptions(defaults, invocation) {
|
|
3148
|
+
return {
|
|
3149
|
+
...defaults,
|
|
3150
|
+
...invocation
|
|
3151
|
+
};
|
|
2901
3152
|
}
|
|
2902
|
-
function
|
|
2903
|
-
|
|
2904
|
-
if (!Number.isFinite(value) || value <= 0) throw new AIError("invalid_model", `${name} must be a positive number`);
|
|
2905
|
-
return value;
|
|
3153
|
+
function isActiveStatus(status) {
|
|
3154
|
+
return status === "queued" || status === "generating" || status === "canceling";
|
|
2906
3155
|
}
|
|
2907
|
-
function
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
"float16",
|
|
2914
|
-
"bfloat16",
|
|
2915
|
-
"float32"
|
|
2916
|
-
].includes(lora.metadata.precision)) throw new AIError("invalid_model", "lora.metadata.precision is unsupported");
|
|
3156
|
+
function recoveryError() {
|
|
3157
|
+
return {
|
|
3158
|
+
name: "AIError",
|
|
3159
|
+
code: "generation_canceled",
|
|
3160
|
+
message: "Generation was interrupted when the app exited"
|
|
3161
|
+
};
|
|
2917
3162
|
}
|
|
2918
|
-
function
|
|
2919
|
-
|
|
3163
|
+
function timestamp() {
|
|
3164
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2920
3165
|
}
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
3166
|
+
|
|
3167
|
+
//#endregion
|
|
3168
|
+
//#region src/ai/image-batch.ts
|
|
3169
|
+
function batchImageSeeds(count, seed) {
|
|
3170
|
+
if (!Number.isInteger(count) || count < 1 || count > 64) throw new AIError("invalid_message", "Batch count must be an integer from 1 to 64");
|
|
3171
|
+
const first = seed ?? Math.floor(Math.random() * 4294967296);
|
|
3172
|
+
return Array.from({ length: count }, (_, index) => first + index >>> 0);
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
//#endregion
|
|
3176
|
+
//#region src/ai/image-jobs.ts
|
|
3177
|
+
const jobShape = {
|
|
3178
|
+
status: require_schema.string({ default: "paused" }),
|
|
3179
|
+
request: require_schema.string({ default: "{}" }),
|
|
3180
|
+
progress: require_schema.string({ default: "" }),
|
|
3181
|
+
result: require_schema.string({ default: "" }),
|
|
3182
|
+
error: require_schema.string({ default: "" }),
|
|
3183
|
+
attempt: require_schema.number({ default: 1 }),
|
|
3184
|
+
startedAt: require_schema.string({ default: "" }),
|
|
3185
|
+
completedAt: require_schema.string({ default: "" })
|
|
3186
|
+
};
|
|
3187
|
+
var CollectionImageJobRepository = class {
|
|
3188
|
+
records = require_state.atomCollection("__bleam_ai_jobs", jobShape);
|
|
3189
|
+
insertRecord = require_state.mutateAtom(this.records, (mutation) => mutation.insert());
|
|
3190
|
+
async list() {
|
|
3191
|
+
const ids = await require_state.internalStore.get(require_state.internalJotaiAtom(this.records));
|
|
3192
|
+
return (await Promise.all(ids.map((id) => this.getRow(id)))).flatMap((row) => row ? [jobFromRow(row)] : []);
|
|
3193
|
+
}
|
|
3194
|
+
async insert(job) {
|
|
3195
|
+
const id = await require_state.internalStore.set(require_state.internalJotaiWritableAtom(this.insertRecord), dataFromJob(job));
|
|
3196
|
+
const row = await this.getRow(id);
|
|
3197
|
+
if (!row) throw new Error(`Could not load inserted image job ${id}`);
|
|
3198
|
+
return jobFromRow(row);
|
|
3199
|
+
}
|
|
3200
|
+
async update(job) {
|
|
3201
|
+
const updateRecord = require_state.mutateAtom(this.records, (mutation) => mutation.update(job.id));
|
|
3202
|
+
return jobFromRow(await require_state.internalStore.set(require_state.internalJotaiWritableAtom(updateRecord), dataFromJob(job)));
|
|
3203
|
+
}
|
|
3204
|
+
async remove(id) {
|
|
3205
|
+
const removeRecord = require_state.mutateAtom(this.records, (mutation) => mutation.remove(id));
|
|
3206
|
+
await require_state.internalStore.set(require_state.internalJotaiWritableAtom(removeRecord));
|
|
3207
|
+
}
|
|
3208
|
+
async getRow(id) {
|
|
3209
|
+
const row = require_state.queryAtom(this.records, (query) => query.row(id));
|
|
3210
|
+
return require_state.internalStore.get(require_state.internalJotaiAtom(row));
|
|
2963
3211
|
}
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
3212
|
+
};
|
|
3213
|
+
var ImageJobManager = class {
|
|
3214
|
+
records = /* @__PURE__ */ new Map();
|
|
3215
|
+
jobListeners = /* @__PURE__ */ new Map();
|
|
3216
|
+
listListeners = /* @__PURE__ */ new Set();
|
|
3217
|
+
listSnapshot = [];
|
|
3218
|
+
operation = Promise.resolve();
|
|
3219
|
+
pumping = false;
|
|
3220
|
+
runningId;
|
|
3221
|
+
controller;
|
|
3222
|
+
ready;
|
|
3223
|
+
constructor(repository, executor, jobFiles = nativeImageJobFiles) {
|
|
3224
|
+
this.repository = repository;
|
|
3225
|
+
this.executor = executor;
|
|
3226
|
+
this.jobFiles = jobFiles;
|
|
3227
|
+
this.ready = this.initialize();
|
|
3228
|
+
}
|
|
3229
|
+
setExecutor(executor) {
|
|
3230
|
+
this.executor = executor;
|
|
3231
|
+
}
|
|
3232
|
+
async queue(options) {
|
|
3233
|
+
await this.ready;
|
|
3234
|
+
const request = normalizeRequest(options);
|
|
3235
|
+
const provisional = await this.repository.insert({
|
|
3236
|
+
kind: "image",
|
|
3237
|
+
status: "paused",
|
|
3238
|
+
request,
|
|
3239
|
+
attempt: 0
|
|
3240
|
+
});
|
|
3241
|
+
try {
|
|
3242
|
+
const preparedRequest = await this.jobFiles.prepare(provisional.id, request);
|
|
3243
|
+
const queued = await this.repository.update({
|
|
3244
|
+
...provisional,
|
|
3245
|
+
status: "queued",
|
|
3246
|
+
request: preparedRequest,
|
|
3247
|
+
attempt: 1
|
|
3248
|
+
});
|
|
3249
|
+
this.set(queued);
|
|
3250
|
+
this.pump();
|
|
3251
|
+
return queued;
|
|
3252
|
+
} catch (error) {
|
|
3253
|
+
await this.repository.remove(provisional.id);
|
|
3254
|
+
await this.jobFiles.remove(provisional).catch(() => void 0);
|
|
3255
|
+
throw error;
|
|
2971
3256
|
}
|
|
2972
|
-
};
|
|
2973
|
-
}
|
|
2974
|
-
function loraUrl(lora) {
|
|
2975
|
-
if (lora.source.type === "url") return lora.source.url;
|
|
2976
|
-
if (lora.source.type === "huggingface") {
|
|
2977
|
-
if (!lora.source.path) return void 0;
|
|
2978
|
-
return hfResolveUrl(lora.source.repo, lora.source.revision ?? "main", lora.source.path);
|
|
2979
3257
|
}
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
fraction: 1
|
|
3258
|
+
async get(id) {
|
|
3259
|
+
await this.ready;
|
|
3260
|
+
return this.records.get(id);
|
|
3261
|
+
}
|
|
3262
|
+
async list() {
|
|
3263
|
+
await this.ready;
|
|
3264
|
+
return this.listSnapshot;
|
|
3265
|
+
}
|
|
3266
|
+
async cancel(id) {
|
|
3267
|
+
await this.ready;
|
|
3268
|
+
return this.mutate(async () => {
|
|
3269
|
+
const job = this.records.get(id);
|
|
3270
|
+
if (!job) return false;
|
|
3271
|
+
if (job.status === "queued" || job.status === "paused") {
|
|
3272
|
+
await this.replace(job, {
|
|
3273
|
+
status: "canceled",
|
|
3274
|
+
error: canceledError(),
|
|
3275
|
+
completedAt: now()
|
|
3276
|
+
});
|
|
3277
|
+
return true;
|
|
3278
|
+
}
|
|
3279
|
+
if (job.status !== "running") return false;
|
|
3280
|
+
await this.replace(job, { status: "canceling" });
|
|
3281
|
+
if (this.runningId === id) this.controller?.abort();
|
|
3282
|
+
return true;
|
|
3006
3283
|
});
|
|
3007
|
-
return;
|
|
3008
3284
|
}
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3285
|
+
async retry(id) {
|
|
3286
|
+
await this.ready;
|
|
3287
|
+
const resumed = await this.mutate(async () => {
|
|
3288
|
+
const job = this.records.get(id);
|
|
3289
|
+
if (!job || job.status !== "failed" && job.status !== "canceled") return false;
|
|
3290
|
+
await this.replace(job, {
|
|
3291
|
+
status: "queued",
|
|
3292
|
+
attempt: job.attempt + 1,
|
|
3293
|
+
progress: void 0,
|
|
3294
|
+
result: void 0,
|
|
3295
|
+
error: void 0,
|
|
3296
|
+
startedAt: void 0,
|
|
3297
|
+
completedAt: void 0
|
|
3298
|
+
});
|
|
3299
|
+
return true;
|
|
3300
|
+
});
|
|
3301
|
+
if (resumed) this.pump();
|
|
3302
|
+
return resumed;
|
|
3303
|
+
}
|
|
3304
|
+
async resume(id) {
|
|
3305
|
+
await this.ready;
|
|
3306
|
+
const resumed = await this.mutate(async () => {
|
|
3307
|
+
const job = this.records.get(id);
|
|
3308
|
+
if (job?.status !== "paused") return false;
|
|
3309
|
+
await this.replace(job, { status: "queued" });
|
|
3310
|
+
return true;
|
|
3311
|
+
});
|
|
3312
|
+
if (resumed) this.pump();
|
|
3313
|
+
return resumed;
|
|
3314
|
+
}
|
|
3315
|
+
async remove(id) {
|
|
3316
|
+
await this.ready;
|
|
3317
|
+
return this.mutate(async () => {
|
|
3318
|
+
const job = this.records.get(id);
|
|
3319
|
+
if (!job) return false;
|
|
3320
|
+
if (job.status === "queued" || job.status === "running" || job.status === "canceling") throw new AIError("concurrent_request", "Cancel the image job before removing it");
|
|
3321
|
+
await this.jobFiles.remove(job);
|
|
3322
|
+
await this.repository.remove(id);
|
|
3323
|
+
this.records.delete(id);
|
|
3324
|
+
this.refreshList();
|
|
3325
|
+
this.emit(id);
|
|
3326
|
+
return true;
|
|
3327
|
+
});
|
|
3328
|
+
}
|
|
3329
|
+
getJobSnapshot(id) {
|
|
3330
|
+
return this.records.get(id);
|
|
3331
|
+
}
|
|
3332
|
+
getListSnapshot() {
|
|
3333
|
+
return this.listSnapshot;
|
|
3334
|
+
}
|
|
3335
|
+
subscribeJob(id, listener) {
|
|
3336
|
+
const listeners = this.jobListeners.get(id) ?? /* @__PURE__ */ new Set();
|
|
3337
|
+
listeners.add(listener);
|
|
3338
|
+
this.jobListeners.set(id, listeners);
|
|
3339
|
+
return () => {
|
|
3340
|
+
listeners.delete(listener);
|
|
3341
|
+
if (listeners.size === 0) this.jobListeners.delete(id);
|
|
3342
|
+
};
|
|
3343
|
+
}
|
|
3344
|
+
subscribeList(listener) {
|
|
3345
|
+
this.listListeners.add(listener);
|
|
3346
|
+
return () => this.listListeners.delete(listener);
|
|
3347
|
+
}
|
|
3348
|
+
async initialize() {
|
|
3349
|
+
const jobs$1 = await this.repository.list();
|
|
3350
|
+
for (const job of jobs$1) {
|
|
3351
|
+
if (job.status === "paused" && job.attempt === 0) {
|
|
3352
|
+
await this.repository.remove(job.id);
|
|
3353
|
+
await this.jobFiles.remove(job).catch(() => void 0);
|
|
3354
|
+
continue;
|
|
3355
|
+
}
|
|
3356
|
+
let recovered = job;
|
|
3357
|
+
if (job.status === "running" || job.status === "canceling") recovered = await this.repository.update({
|
|
3358
|
+
...job,
|
|
3359
|
+
status: "canceled",
|
|
3360
|
+
error: canceledError("Generation was interrupted when the app exited"),
|
|
3361
|
+
completedAt: now()
|
|
3021
3362
|
});
|
|
3363
|
+
else if (job.status === "queued") recovered = await this.repository.update({
|
|
3364
|
+
...job,
|
|
3365
|
+
status: "paused"
|
|
3366
|
+
});
|
|
3367
|
+
this.records.set(recovered.id, recovered);
|
|
3022
3368
|
}
|
|
3023
|
-
|
|
3024
|
-
}
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3369
|
+
this.refreshList();
|
|
3370
|
+
}
|
|
3371
|
+
mutate(action) {
|
|
3372
|
+
const result = this.operation.then(action, action);
|
|
3373
|
+
this.operation = result.then(() => void 0, () => void 0);
|
|
3374
|
+
return result;
|
|
3375
|
+
}
|
|
3376
|
+
pump() {
|
|
3377
|
+
if (this.pumping) return;
|
|
3378
|
+
this.pumping = true;
|
|
3379
|
+
this.runLoop().finally(() => {
|
|
3380
|
+
this.pumping = false;
|
|
3381
|
+
if (this.nextQueued()) this.pump();
|
|
3382
|
+
});
|
|
3383
|
+
}
|
|
3384
|
+
async runLoop() {
|
|
3385
|
+
while (true) {
|
|
3386
|
+
const job = await this.mutate(async () => {
|
|
3387
|
+
const next = this.nextQueued();
|
|
3388
|
+
if (!next) return void 0;
|
|
3389
|
+
this.runningId = next.id;
|
|
3390
|
+
this.controller = new AbortController();
|
|
3391
|
+
return this.replace(next, {
|
|
3392
|
+
status: "running",
|
|
3393
|
+
startedAt: now(),
|
|
3394
|
+
completedAt: void 0,
|
|
3395
|
+
error: void 0,
|
|
3396
|
+
progress: void 0
|
|
3397
|
+
});
|
|
3398
|
+
});
|
|
3399
|
+
if (!job) return;
|
|
3400
|
+
const controller = this.controller;
|
|
3401
|
+
try {
|
|
3402
|
+
const result = await this.executor({
|
|
3403
|
+
...job.request,
|
|
3404
|
+
signal: controller.signal,
|
|
3405
|
+
onProgress: (progress) => {
|
|
3406
|
+
this.mutate(async () => {
|
|
3407
|
+
const current = this.records.get(job.id);
|
|
3408
|
+
if (current?.status === "running") await this.replace(current, { progress });
|
|
3409
|
+
});
|
|
3410
|
+
}
|
|
3411
|
+
});
|
|
3412
|
+
await this.mutate(async () => {
|
|
3413
|
+
const current = this.records.get(job.id);
|
|
3414
|
+
if (!current) return;
|
|
3415
|
+
if (controller.signal.aborted || current.status === "canceling") {
|
|
3416
|
+
await this.replace(current, {
|
|
3417
|
+
status: "canceled",
|
|
3418
|
+
error: canceledError(),
|
|
3419
|
+
completedAt: now()
|
|
3420
|
+
});
|
|
3421
|
+
return;
|
|
3422
|
+
}
|
|
3423
|
+
await this.replace(current, {
|
|
3424
|
+
status: "completed",
|
|
3425
|
+
progress: {
|
|
3426
|
+
currentStep: 1,
|
|
3427
|
+
totalSteps: 1,
|
|
3428
|
+
fraction: 1
|
|
3429
|
+
},
|
|
3430
|
+
result,
|
|
3431
|
+
completedAt: now()
|
|
3432
|
+
});
|
|
3433
|
+
});
|
|
3434
|
+
} catch (error) {
|
|
3435
|
+
await this.mutate(async () => {
|
|
3436
|
+
const current = this.records.get(job.id);
|
|
3437
|
+
if (!current) return;
|
|
3438
|
+
const canceled = controller.signal.aborted || current.status === "canceling";
|
|
3439
|
+
await this.replace(current, {
|
|
3440
|
+
status: canceled ? "canceled" : "failed",
|
|
3441
|
+
error: canceled ? canceledError() : serializedError(error),
|
|
3442
|
+
completedAt: now()
|
|
3443
|
+
});
|
|
3444
|
+
});
|
|
3445
|
+
} finally {
|
|
3446
|
+
this.runningId = void 0;
|
|
3447
|
+
this.controller = void 0;
|
|
3448
|
+
}
|
|
3038
3449
|
}
|
|
3039
|
-
bytesPresent += Math.min(stat.size ?? 0, file.bytes);
|
|
3040
|
-
if (stat.size !== file.bytes) invalidFiles.push(file.destination);
|
|
3041
3450
|
}
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3451
|
+
nextQueued() {
|
|
3452
|
+
return [...this.records.values()].filter((job) => job.status === "queued").sort((left, right) => left.createdAt === right.createdAt ? left.id.localeCompare(right.id) : left.createdAt.localeCompare(right.createdAt))[0];
|
|
3453
|
+
}
|
|
3454
|
+
async replace(job, update) {
|
|
3455
|
+
const persisted = await this.repository.update({
|
|
3456
|
+
...job,
|
|
3457
|
+
...update
|
|
3458
|
+
});
|
|
3459
|
+
this.set(persisted);
|
|
3460
|
+
return persisted;
|
|
3461
|
+
}
|
|
3462
|
+
set(job) {
|
|
3463
|
+
this.records.set(job.id, job);
|
|
3464
|
+
this.refreshList();
|
|
3465
|
+
this.emit(job.id);
|
|
3466
|
+
}
|
|
3467
|
+
refreshList() {
|
|
3468
|
+
this.listSnapshot = [...this.records.values()].sort((left, right) => left.createdAt === right.createdAt ? right.id.localeCompare(left.id) : right.createdAt.localeCompare(left.createdAt)).map(summaryFromJob);
|
|
3469
|
+
for (const listener of this.listListeners) listener();
|
|
3470
|
+
}
|
|
3471
|
+
emit(id) {
|
|
3472
|
+
for (const listener of this.jobListeners.get(id) ?? []) listener();
|
|
3473
|
+
}
|
|
3474
|
+
};
|
|
3475
|
+
function useManagedJob(manager, id) {
|
|
3476
|
+
(0, react.use)(manager.ready);
|
|
3477
|
+
return (0, react.useSyncExternalStore)((listener) => manager.subscribeJob(id, listener), () => manager.getJobSnapshot(id));
|
|
3478
|
+
}
|
|
3479
|
+
function useManagedJobList(manager) {
|
|
3480
|
+
(0, react.use)(manager.ready);
|
|
3481
|
+
return (0, react.useSyncExternalStore)((listener) => manager.subscribeList(listener), () => manager.getListSnapshot());
|
|
3482
|
+
}
|
|
3483
|
+
const nativeImageJobFiles = {
|
|
3484
|
+
async prepare(id, request) {
|
|
3485
|
+
const inputs = jobRoot(id).directory("Inputs");
|
|
3486
|
+
await inputs.create();
|
|
3487
|
+
return {
|
|
3488
|
+
...request,
|
|
3489
|
+
image: request.image ? await copyInput(request.image, inputs.path, "Image") : void 0,
|
|
3490
|
+
mask: request.mask ? await copyInput(request.mask, inputs.path, "Mask") : void 0,
|
|
3491
|
+
references: request.references ? await Promise.all(request.references.map((reference, index) => copyInput(reference, inputs.path, `Reference-${index + 1}`))) : void 0,
|
|
3492
|
+
loras: request.loras ? await Promise.all(request.loras.map(async (lora, index) => lora.source.type === "local" ? {
|
|
3493
|
+
...lora,
|
|
3494
|
+
source: {
|
|
3495
|
+
...lora.source,
|
|
3496
|
+
path: await copyInput(lora.source.path, inputs.path, `LoRA-${index + 1}`, ".safetensors")
|
|
3497
|
+
}
|
|
3498
|
+
} : lora)) : void 0
|
|
3499
|
+
};
|
|
3500
|
+
},
|
|
3501
|
+
async remove(job) {
|
|
3502
|
+
await jobRoot(job.id).delete().catch(() => void 0);
|
|
3503
|
+
if (job.result?.uri) await require_files.files.file(pathFromUri(job.result.uri)).delete().catch(() => void 0);
|
|
3504
|
+
}
|
|
3505
|
+
};
|
|
3506
|
+
function dataFromJob(job) {
|
|
3045
3507
|
return {
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
invalidFiles
|
|
3508
|
+
status: job.status,
|
|
3509
|
+
request: JSON.stringify(job.request),
|
|
3510
|
+
progress: encode(job.progress),
|
|
3511
|
+
result: encode(job.result),
|
|
3512
|
+
error: encode(job.error),
|
|
3513
|
+
attempt: job.attempt,
|
|
3514
|
+
startedAt: job.startedAt ?? "",
|
|
3515
|
+
completedAt: job.completedAt ?? ""
|
|
3055
3516
|
};
|
|
3056
3517
|
}
|
|
3057
|
-
function
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3518
|
+
function jobFromRow(row) {
|
|
3519
|
+
return {
|
|
3520
|
+
id: row.id,
|
|
3521
|
+
kind: "image",
|
|
3522
|
+
status: row.data.status,
|
|
3523
|
+
request: decode(row.data.request),
|
|
3524
|
+
progress: decode(row.data.progress),
|
|
3525
|
+
result: decode(row.data.result),
|
|
3526
|
+
error: decode(row.data.error),
|
|
3527
|
+
attempt: row.data.attempt,
|
|
3528
|
+
createdAt: row.createdAt,
|
|
3529
|
+
updatedAt: row.updatedAt,
|
|
3530
|
+
startedAt: row.data.startedAt || void 0,
|
|
3531
|
+
completedAt: row.data.completedAt || void 0
|
|
3532
|
+
};
|
|
3533
|
+
}
|
|
3534
|
+
function encode(value) {
|
|
3535
|
+
return value === void 0 ? "" : JSON.stringify(value);
|
|
3536
|
+
}
|
|
3537
|
+
function decode(value) {
|
|
3538
|
+
return value ? JSON.parse(value) : void 0;
|
|
3539
|
+
}
|
|
3540
|
+
function normalizeRequest(options) {
|
|
3541
|
+
const runtimeOptions = options;
|
|
3542
|
+
for (const key of [
|
|
3543
|
+
"accessToken",
|
|
3544
|
+
"onModelProgress",
|
|
3545
|
+
"onProgress",
|
|
3546
|
+
"signal",
|
|
3547
|
+
"tokenizer"
|
|
3548
|
+
]) if (runtimeOptions[key] !== void 0) throw new AIError("invalid_message", `${key} is not supported for persistent image jobs`);
|
|
3549
|
+
if (!options.prompt.trim()) throw new AIError("invalid_message", "Expected prompt");
|
|
3550
|
+
assertSerializable(options);
|
|
3551
|
+
return {
|
|
3552
|
+
...options,
|
|
3553
|
+
model: typeof options.model === "string" ? options.model : options.model?.id,
|
|
3554
|
+
seed: options.seed ?? Math.floor(Math.random() * 4294967296)
|
|
3555
|
+
};
|
|
3556
|
+
}
|
|
3557
|
+
function assertSerializable(value, path = "options") {
|
|
3558
|
+
if (typeof value === "number" && !Number.isFinite(value)) throw new AIError("invalid_message", `${path} must be finite`);
|
|
3559
|
+
if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") throw new AIError("invalid_message", `${path} must be serializable`);
|
|
3560
|
+
if (Array.isArray(value)) value.forEach((next, index) => {
|
|
3561
|
+
assertSerializable(next, `${path}[${index}]`);
|
|
3065
3562
|
});
|
|
3563
|
+
else if (value && typeof value === "object") for (const [key, next] of Object.entries(value)) assertSerializable(next, `${path}.${key}`);
|
|
3066
3564
|
}
|
|
3067
|
-
|
|
3068
|
-
const
|
|
3069
|
-
const
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
inputIds.push(padTokenId);
|
|
3076
|
-
attentionMask.push(0);
|
|
3077
|
-
}
|
|
3078
|
-
return {
|
|
3079
|
-
inputIds,
|
|
3080
|
-
attentionMask
|
|
3081
|
-
};
|
|
3565
|
+
function serializedError(error) {
|
|
3566
|
+
const value = error instanceof Error ? error : new Error(String(error));
|
|
3567
|
+
const record = value;
|
|
3568
|
+
return {
|
|
3569
|
+
name: value.name,
|
|
3570
|
+
message: value.message,
|
|
3571
|
+
code: typeof record.code === "string" ? record.code : void 0,
|
|
3572
|
+
stage: typeof record.stage === "string" ? record.stage : void 0
|
|
3082
3573
|
};
|
|
3083
3574
|
}
|
|
3084
|
-
function
|
|
3085
|
-
return
|
|
3086
|
-
"
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
"",
|
|
3091
|
-
"</think>",
|
|
3092
|
-
""
|
|
3093
|
-
].join("\n");
|
|
3575
|
+
function canceledError(message = "Image generation canceled") {
|
|
3576
|
+
return {
|
|
3577
|
+
name: "AIError",
|
|
3578
|
+
code: "generation_canceled",
|
|
3579
|
+
message
|
|
3580
|
+
};
|
|
3094
3581
|
}
|
|
3095
|
-
function
|
|
3096
|
-
|
|
3582
|
+
function summaryFromJob(job) {
|
|
3583
|
+
return {
|
|
3584
|
+
id: job.id,
|
|
3585
|
+
kind: job.kind,
|
|
3586
|
+
status: job.status,
|
|
3587
|
+
prompt: job.request.prompt,
|
|
3588
|
+
progress: job.progress,
|
|
3589
|
+
attempt: job.attempt,
|
|
3590
|
+
createdAt: job.createdAt,
|
|
3591
|
+
updatedAt: job.updatedAt,
|
|
3592
|
+
startedAt: job.startedAt,
|
|
3593
|
+
completedAt: job.completedAt
|
|
3594
|
+
};
|
|
3097
3595
|
}
|
|
3098
|
-
function
|
|
3099
|
-
|
|
3596
|
+
function jobRoot(id) {
|
|
3597
|
+
return require_files.files.directory(require_files.files.paths.shared, "ImageJobs", require_state.currentAppStorageKey(), id);
|
|
3100
3598
|
}
|
|
3101
|
-
function
|
|
3102
|
-
const path =
|
|
3103
|
-
|
|
3104
|
-
|
|
3599
|
+
async function copyInput(source, directory, name, fallbackExtension = ".png") {
|
|
3600
|
+
const path = pathFromUri(source);
|
|
3601
|
+
const destination = `${directory}/${name}${path.match(/\.[A-Za-z0-9]+$/)?.[0] ?? fallbackExtension}`;
|
|
3602
|
+
await require_files.nativeCopy(path, destination);
|
|
3603
|
+
return destination;
|
|
3604
|
+
}
|
|
3605
|
+
function pathFromUri(value) {
|
|
3606
|
+
if (!value.startsWith("file://")) return value;
|
|
3105
3607
|
try {
|
|
3106
|
-
return decodeURIComponent(new URL(
|
|
3107
|
-
} catch
|
|
3108
|
-
|
|
3608
|
+
return decodeURIComponent(new URL(value).pathname);
|
|
3609
|
+
} catch {
|
|
3610
|
+
return value.slice(7);
|
|
3109
3611
|
}
|
|
3110
3612
|
}
|
|
3111
|
-
function
|
|
3112
|
-
|
|
3113
|
-
for (const token of value) if (!Number.isInteger(token) || token < 0) throw new AIError("invalid_message", `${name} must contain token ids`);
|
|
3613
|
+
function now() {
|
|
3614
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3114
3615
|
}
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3616
|
+
|
|
3617
|
+
//#endregion
|
|
3618
|
+
//#region src/ai/image-models.ts
|
|
3619
|
+
const flux2Klein4B8BitAbliterated = {
|
|
3620
|
+
formatVersion: 1,
|
|
3621
|
+
model: "flux2-klein-4b-8bit-abliterated",
|
|
3622
|
+
directory: "flux2-klein-4b-8bit-abliterated",
|
|
3623
|
+
metadata: {
|
|
3624
|
+
displayName: "Flux.2 Klein 4B Abliterated",
|
|
3625
|
+
family: "flux2-klein",
|
|
3626
|
+
precision: "8-bit",
|
|
3627
|
+
estimatedMemoryGB: 12,
|
|
3628
|
+
quality: "high",
|
|
3629
|
+
speed: "fast",
|
|
3630
|
+
capabilities: [
|
|
3631
|
+
"text-to-image",
|
|
3632
|
+
"image-to-image",
|
|
3633
|
+
"inpainting",
|
|
3634
|
+
"outpainting",
|
|
3635
|
+
"references",
|
|
3636
|
+
"lora"
|
|
3637
|
+
]
|
|
3638
|
+
},
|
|
3639
|
+
components: {
|
|
3640
|
+
textEncoder: {
|
|
3641
|
+
repo: "mlx-community/Josiefied-Qwen3-4B-abliterated-v1-8bit",
|
|
3642
|
+
revision: "main",
|
|
3643
|
+
path: "text-encoder",
|
|
3644
|
+
files: [
|
|
3645
|
+
"config.json",
|
|
3646
|
+
"tokenizer.json",
|
|
3647
|
+
"tokenizer_config.json",
|
|
3648
|
+
"*.safetensors",
|
|
3649
|
+
"*.safetensors.index.json"
|
|
3650
|
+
]
|
|
3651
|
+
},
|
|
3652
|
+
transformer: {
|
|
3653
|
+
repo: "moxin-org/FLUX.2-klein-4B-8bit-mlx",
|
|
3654
|
+
revision: "main",
|
|
3655
|
+
path: "transformer",
|
|
3656
|
+
files: ["config.json", "quantized_8bit.safetensors"]
|
|
3657
|
+
},
|
|
3658
|
+
vae: {
|
|
3659
|
+
repo: "black-forest-labs/FLUX.2-klein-4B",
|
|
3660
|
+
revision: "main",
|
|
3661
|
+
path: "vae",
|
|
3662
|
+
files: [
|
|
3663
|
+
"config.json",
|
|
3664
|
+
"diffusion_pytorch_model.safetensors",
|
|
3665
|
+
"diffusion_pytorch_model.safetensors.index.json",
|
|
3666
|
+
"diffusion_pytorch_model-*.safetensors"
|
|
3667
|
+
]
|
|
3142
3668
|
}
|
|
3143
|
-
}
|
|
3669
|
+
}
|
|
3670
|
+
};
|
|
3671
|
+
const ternaryBonsaiImage4B = {
|
|
3672
|
+
formatVersion: 1,
|
|
3673
|
+
model: "ternary-bonsai-image-4b",
|
|
3674
|
+
directory: "ternary-bonsai-image-4b",
|
|
3675
|
+
metadata: {
|
|
3676
|
+
displayName: "Ternary Bonsai Image 4B",
|
|
3677
|
+
family: "bonsai-image",
|
|
3678
|
+
precision: "2-bit",
|
|
3679
|
+
estimatedMemoryGB: 8,
|
|
3680
|
+
quality: "high",
|
|
3681
|
+
speed: "fast",
|
|
3682
|
+
capabilities: ["text-to-image"]
|
|
3683
|
+
},
|
|
3684
|
+
components: {
|
|
3685
|
+
textEncoder: {
|
|
3686
|
+
repo: "prism-ml/bonsai-image-ternary-4B-mlx-2bit",
|
|
3687
|
+
revision: "2c24c81b934a658ba5590cf39088ba929985b4a8",
|
|
3688
|
+
path: "text-encoder",
|
|
3689
|
+
files: [
|
|
3690
|
+
"text_encoder-mlx-4bit/config.json",
|
|
3691
|
+
"text_encoder-mlx-4bit/tokenizer.json",
|
|
3692
|
+
"text_encoder-mlx-4bit/tokenizer_config.json",
|
|
3693
|
+
"text_encoder-mlx-4bit/model.safetensors",
|
|
3694
|
+
"text_encoder-mlx-4bit/model.safetensors.index.json"
|
|
3695
|
+
]
|
|
3696
|
+
},
|
|
3697
|
+
transformer: {
|
|
3698
|
+
repo: "prism-ml/bonsai-image-ternary-4B-mlx-2bit",
|
|
3699
|
+
revision: "2c24c81b934a658ba5590cf39088ba929985b4a8",
|
|
3700
|
+
path: "transformer",
|
|
3701
|
+
files: [
|
|
3702
|
+
"transformer-packed-mflux/config.json",
|
|
3703
|
+
"transformer-packed-mflux/diffusion_pytorch_model.safetensors",
|
|
3704
|
+
"transformer-packed-mflux/quantization_config.json"
|
|
3705
|
+
]
|
|
3706
|
+
},
|
|
3707
|
+
vae: {
|
|
3708
|
+
repo: "prism-ml/bonsai-image-ternary-4B-mlx-2bit",
|
|
3709
|
+
revision: "2c24c81b934a658ba5590cf39088ba929985b4a8",
|
|
3710
|
+
path: "vae",
|
|
3711
|
+
files: ["vae/config.json", "vae/diffusion_pytorch_model.safetensors"]
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
};
|
|
3715
|
+
const imageModelManifests = [flux2Klein4B8BitAbliterated, ternaryBonsaiImage4B];
|
|
3716
|
+
function imageModelManifest(model) {
|
|
3717
|
+
return imageModelManifests.find((manifest) => manifest.model === model);
|
|
3144
3718
|
}
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3719
|
+
const flux2KleinModelFiles = [
|
|
3720
|
+
{
|
|
3721
|
+
repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
|
|
3722
|
+
revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
|
|
3723
|
+
source: "config.json",
|
|
3724
|
+
destination: "text-encoder/config.json",
|
|
3725
|
+
bytes: 937
|
|
3726
|
+
},
|
|
3727
|
+
{
|
|
3728
|
+
repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
|
|
3729
|
+
revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
|
|
3730
|
+
source: "tokenizer.json",
|
|
3731
|
+
destination: "text-encoder/tokenizer.json",
|
|
3732
|
+
bytes: 11422654
|
|
3733
|
+
},
|
|
3734
|
+
{
|
|
3735
|
+
repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
|
|
3736
|
+
revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
|
|
3737
|
+
source: "tokenizer_config.json",
|
|
3738
|
+
destination: "text-encoder/tokenizer_config.json",
|
|
3739
|
+
bytes: 9706
|
|
3740
|
+
},
|
|
3741
|
+
{
|
|
3742
|
+
repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
|
|
3743
|
+
revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
|
|
3744
|
+
source: "model.safetensors",
|
|
3745
|
+
destination: "text-encoder/model.safetensors",
|
|
3746
|
+
bytes: 4274158989
|
|
3747
|
+
},
|
|
3748
|
+
{
|
|
3749
|
+
repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
|
|
3750
|
+
revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
|
|
3751
|
+
source: "model.safetensors.index.json",
|
|
3752
|
+
destination: "text-encoder/model.safetensors.index.json",
|
|
3753
|
+
bytes: 63924
|
|
3754
|
+
},
|
|
3755
|
+
{
|
|
3756
|
+
repo: flux2Klein4B8BitAbliterated.components.transformer.repo,
|
|
3757
|
+
revision: flux2Klein4B8BitAbliterated.components.transformer.revision,
|
|
3758
|
+
source: "transformer/config.json",
|
|
3759
|
+
destination: "transformer/config.json",
|
|
3760
|
+
bytes: 541
|
|
3761
|
+
},
|
|
3762
|
+
{
|
|
3763
|
+
repo: flux2Klein4B8BitAbliterated.components.transformer.repo,
|
|
3764
|
+
revision: flux2Klein4B8BitAbliterated.components.transformer.revision,
|
|
3765
|
+
source: "transformer/quantized_8bit.safetensors",
|
|
3766
|
+
destination: "transformer/quantized_8bit.safetensors",
|
|
3767
|
+
bytes: 4360050983
|
|
3768
|
+
},
|
|
3769
|
+
{
|
|
3770
|
+
repo: flux2Klein4B8BitAbliterated.components.vae.repo,
|
|
3771
|
+
revision: flux2Klein4B8BitAbliterated.components.vae.revision,
|
|
3772
|
+
source: "vae/config.json",
|
|
3773
|
+
destination: "vae/config.json",
|
|
3774
|
+
bytes: 821
|
|
3775
|
+
},
|
|
3776
|
+
{
|
|
3777
|
+
repo: flux2Klein4B8BitAbliterated.components.vae.repo,
|
|
3778
|
+
revision: flux2Klein4B8BitAbliterated.components.vae.revision,
|
|
3779
|
+
source: "vae/diffusion_pytorch_model.safetensors",
|
|
3780
|
+
destination: "vae/diffusion_pytorch_model.safetensors",
|
|
3781
|
+
bytes: 168120878
|
|
3152
3782
|
}
|
|
3783
|
+
];
|
|
3784
|
+
const flux2KleinTotalBytes = flux2KleinModelFiles.reduce((total, file) => total + file.bytes, 0);
|
|
3785
|
+
const bonsaiRepo = ternaryBonsaiImage4B.components.textEncoder.repo;
|
|
3786
|
+
const bonsaiRevision = ternaryBonsaiImage4B.components.textEncoder.revision;
|
|
3787
|
+
const ternaryBonsaiImageModelFiles = [
|
|
3788
|
+
{
|
|
3789
|
+
repo: bonsaiRepo,
|
|
3790
|
+
revision: bonsaiRevision,
|
|
3791
|
+
source: "text_encoder-mlx-4bit/config.json",
|
|
3792
|
+
destination: "text-encoder/config.json",
|
|
3793
|
+
bytes: 937
|
|
3794
|
+
},
|
|
3795
|
+
{
|
|
3796
|
+
repo: bonsaiRepo,
|
|
3797
|
+
revision: bonsaiRevision,
|
|
3798
|
+
source: "text_encoder-mlx-4bit/tokenizer.json",
|
|
3799
|
+
destination: "text-encoder/tokenizer.json",
|
|
3800
|
+
bytes: 11422654
|
|
3801
|
+
},
|
|
3802
|
+
{
|
|
3803
|
+
repo: bonsaiRepo,
|
|
3804
|
+
revision: bonsaiRevision,
|
|
3805
|
+
source: "text_encoder-mlx-4bit/tokenizer_config.json",
|
|
3806
|
+
destination: "text-encoder/tokenizer_config.json",
|
|
3807
|
+
bytes: 9706
|
|
3808
|
+
},
|
|
3809
|
+
{
|
|
3810
|
+
repo: bonsaiRepo,
|
|
3811
|
+
revision: bonsaiRevision,
|
|
3812
|
+
source: "text_encoder-mlx-4bit/model.safetensors",
|
|
3813
|
+
destination: "text-encoder/model.safetensors",
|
|
3814
|
+
bytes: 2263022529
|
|
3815
|
+
},
|
|
3816
|
+
{
|
|
3817
|
+
repo: bonsaiRepo,
|
|
3818
|
+
revision: bonsaiRevision,
|
|
3819
|
+
source: "text_encoder-mlx-4bit/model.safetensors.index.json",
|
|
3820
|
+
destination: "text-encoder/model.safetensors.index.json",
|
|
3821
|
+
bytes: 63924
|
|
3822
|
+
},
|
|
3823
|
+
{
|
|
3824
|
+
repo: bonsaiRepo,
|
|
3825
|
+
revision: bonsaiRevision,
|
|
3826
|
+
source: "transformer-packed-mflux/config.json",
|
|
3827
|
+
destination: "transformer/config.json",
|
|
3828
|
+
bytes: 619
|
|
3829
|
+
},
|
|
3830
|
+
{
|
|
3831
|
+
repo: bonsaiRepo,
|
|
3832
|
+
revision: bonsaiRevision,
|
|
3833
|
+
source: "transformer-packed-mflux/diffusion_pytorch_model.safetensors",
|
|
3834
|
+
destination: "transformer/diffusion_pytorch_model.safetensors",
|
|
3835
|
+
bytes: 1425271472
|
|
3836
|
+
},
|
|
3837
|
+
{
|
|
3838
|
+
repo: bonsaiRepo,
|
|
3839
|
+
revision: bonsaiRevision,
|
|
3840
|
+
source: "transformer-packed-mflux/quantization_config.json",
|
|
3841
|
+
destination: "transformer/quantization_config.json",
|
|
3842
|
+
bytes: 5054
|
|
3843
|
+
},
|
|
3844
|
+
{
|
|
3845
|
+
repo: bonsaiRepo,
|
|
3846
|
+
revision: bonsaiRevision,
|
|
3847
|
+
source: "vae/config.json",
|
|
3848
|
+
destination: "vae/config.json",
|
|
3849
|
+
bytes: 821
|
|
3850
|
+
},
|
|
3851
|
+
{
|
|
3852
|
+
repo: bonsaiRepo,
|
|
3853
|
+
revision: bonsaiRevision,
|
|
3854
|
+
source: "vae/diffusion_pytorch_model.safetensors",
|
|
3855
|
+
destination: "vae/diffusion_pytorch_model.safetensors",
|
|
3856
|
+
bytes: 168120878
|
|
3857
|
+
}
|
|
3858
|
+
];
|
|
3859
|
+
const ternaryBonsaiImageTotalBytes = ternaryBonsaiImageModelFiles.reduce((total, file) => total + file.bytes, 0);
|
|
3860
|
+
function joinModelPath(root, component) {
|
|
3861
|
+
return `${root.replace(/\/+$/, "")}/${component.replace(/^\/+/, "")}`;
|
|
3153
3862
|
}
|
|
3154
|
-
function
|
|
3155
|
-
const currentStep = value.currentStep;
|
|
3156
|
-
const totalSteps = value.totalSteps;
|
|
3157
|
-
if (typeof currentStep !== "number" || !Number.isFinite(currentStep) || typeof totalSteps !== "number" || !Number.isFinite(totalSteps) || totalSteps <= 0) return;
|
|
3863
|
+
function fluxImageModelPaths(path, manifest = flux2Klein4B8BitAbliterated) {
|
|
3158
3864
|
return {
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3865
|
+
path,
|
|
3866
|
+
textEncoderPath: joinModelPath(path, manifest.components.textEncoder.path),
|
|
3867
|
+
transformerPath: joinModelPath(path, manifest.components.transformer.path),
|
|
3868
|
+
vaePath: joinModelPath(path, manifest.components.vae.path)
|
|
3162
3869
|
};
|
|
3163
3870
|
}
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
function
|
|
3168
|
-
const
|
|
3169
|
-
|
|
3871
|
+
|
|
3872
|
+
//#endregion
|
|
3873
|
+
//#region src/ai/image-schedule.ts
|
|
3874
|
+
function imageSigmaSchedule(schedules) {
|
|
3875
|
+
const configured = schedules.filter((schedule) => schedule !== void 0);
|
|
3876
|
+
if (configured.length === 0) return void 0;
|
|
3877
|
+
const first = configured[0];
|
|
3878
|
+
if (first.length === 0 || first.some((sigma, index) => !Number.isFinite(sigma) || sigma <= 0 || sigma > 1 || index > 0 && sigma >= first[index - 1])) throw new AIError("invalid_model", "LoRA sigmaSchedule must contain descending values greater than 0 and at most 1");
|
|
3879
|
+
if (configured.some((schedule) => schedule.length !== first.length || schedule.some((sigma, index) => sigma !== first[index]))) throw new AIError("invalid_model", "Selected LoRAs require conflicting sigma schedules");
|
|
3880
|
+
return [...first];
|
|
3170
3881
|
}
|
|
3171
|
-
|
|
3172
|
-
|
|
3882
|
+
|
|
3883
|
+
//#endregion
|
|
3884
|
+
//#region src/ai/image-upscale.ts
|
|
3885
|
+
function normalizeUpscaleScale(value) {
|
|
3886
|
+
const scale = value ?? 2;
|
|
3887
|
+
if (!Number.isFinite(scale) || scale <= 1 || scale > 4) throw new AIError("invalid_model", "Upscale scale must be greater than 1 and at most 4");
|
|
3888
|
+
return scale;
|
|
3173
3889
|
}
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3890
|
+
|
|
3891
|
+
//#endregion
|
|
3892
|
+
//#region src/ai/image.ts
|
|
3893
|
+
function imageModelId(model) {
|
|
3894
|
+
return typeof model === "string" ? model : model.id;
|
|
3177
3895
|
}
|
|
3178
|
-
|
|
3179
|
-
const
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
const bytesByFile = /* @__PURE__ */ new Map();
|
|
3185
|
-
const emitProgress = (progress) => {
|
|
3186
|
-
const file = definition.files.find((candidate) => candidate.destination === progress.file);
|
|
3187
|
-
const bytesWritten = file ? Math.min(progress.bytesWritten, file.bytes) : progress.bytesWritten;
|
|
3188
|
-
bytesByFile.set(progress.file, bytesWritten);
|
|
3189
|
-
const modelBytesWritten = Array.from(bytesByFile.values()).reduce((sum, bytes) => sum + bytes, 0);
|
|
3190
|
-
options.onProgress?.({
|
|
3191
|
-
...progress,
|
|
3192
|
-
bytesWritten,
|
|
3193
|
-
modelBytesWritten,
|
|
3194
|
-
totalModelBytes: definition.totalBytes,
|
|
3195
|
-
modelFraction: modelBytesWritten / definition.totalBytes
|
|
3196
|
-
});
|
|
3896
|
+
function imageModelDefinition(model) {
|
|
3897
|
+
const id = imageModelId(model);
|
|
3898
|
+
if (id === flux2Klein4B8BitAbliterated.model) return {
|
|
3899
|
+
manifest: flux2Klein4B8BitAbliterated,
|
|
3900
|
+
files: flux2KleinModelFiles,
|
|
3901
|
+
totalBytes: flux2KleinTotalBytes
|
|
3197
3902
|
};
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
model,
|
|
3203
|
-
file: file.destination,
|
|
3204
|
-
index: index + 1,
|
|
3205
|
-
total,
|
|
3206
|
-
bytesWritten: file.bytes,
|
|
3207
|
-
totalBytes: file.bytes,
|
|
3208
|
-
fraction: 1
|
|
3209
|
-
});
|
|
3210
|
-
else missingFiles.push({
|
|
3211
|
-
file,
|
|
3212
|
-
index: index + 1
|
|
3213
|
-
});
|
|
3214
|
-
}
|
|
3215
|
-
await runConcurrent(missingFiles, downloadConcurrency(options.concurrency), async ({ file, index }) => {
|
|
3216
|
-
await ensureModelFile({
|
|
3217
|
-
model,
|
|
3218
|
-
path: root.path,
|
|
3219
|
-
file,
|
|
3220
|
-
headers,
|
|
3221
|
-
index,
|
|
3222
|
-
total,
|
|
3223
|
-
onProgress: emitProgress
|
|
3224
|
-
});
|
|
3225
|
-
});
|
|
3226
|
-
await writePreparedManifest(root.path, definition.manifest);
|
|
3227
|
-
return {
|
|
3228
|
-
model,
|
|
3229
|
-
...fluxImageModelPaths(root.path, definition.manifest),
|
|
3230
|
-
tokenizer: await createFluxTokenizer(root.path, definition.manifest)
|
|
3903
|
+
if (id === ternaryBonsaiImage4B.model) return {
|
|
3904
|
+
manifest: ternaryBonsaiImage4B,
|
|
3905
|
+
files: ternaryBonsaiImageModelFiles,
|
|
3906
|
+
totalBytes: ternaryBonsaiImageTotalBytes
|
|
3231
3907
|
};
|
|
3908
|
+
throw new AIError("invalid_model", `Unsupported image model: ${id}`);
|
|
3232
3909
|
}
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
return imageModelState(definition.manifest.model, modelPathFromOptions(definition, options.path));
|
|
3236
|
-
}
|
|
3237
|
-
async function listImageModels(options = {}) {
|
|
3238
|
-
return [...await Promise.all([flux2Klein4B8BitAbliterated.model, ternaryBonsaiImage4B.model].map((model) => {
|
|
3239
|
-
const definition = imageModelDefinition(model);
|
|
3240
|
-
return imageModelState(model, options.directory ? require_files.files.directory(options.directory, definition.manifest.directory).path : modelPathFromOptions(definition, void 0));
|
|
3241
|
-
}))];
|
|
3910
|
+
function modelCacheRoot(manifest) {
|
|
3911
|
+
return require_files.files.directory(require_files.files.paths.models, manifest.directory);
|
|
3242
3912
|
}
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
await require_files.files.directory(modelPathFromOptions(definition, options.path)).delete({ recursive: true });
|
|
3913
|
+
function assertImageModel(model) {
|
|
3914
|
+
imageModelDefinition(model);
|
|
3246
3915
|
}
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
if (!value) throw new AIError("image_generation_failed", "Image service is unavailable");
|
|
3250
|
-
return value;
|
|
3916
|
+
function modelPathFromOptions(definition, path) {
|
|
3917
|
+
return path ?? modelCacheRoot(definition.manifest).path;
|
|
3251
3918
|
}
|
|
3252
|
-
|
|
3253
|
-
|
|
3919
|
+
function downloadConcurrency(value) {
|
|
3920
|
+
if (value === void 0) return 2;
|
|
3921
|
+
if (!Number.isInteger(value)) throw new AIError("invalid_model", "concurrency must be an integer");
|
|
3922
|
+
return Math.min(4, Math.max(1, value));
|
|
3254
3923
|
}
|
|
3255
|
-
async function
|
|
3256
|
-
|
|
3257
|
-
const
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3924
|
+
async function runConcurrent(items, concurrency, worker) {
|
|
3925
|
+
let nextIndex = 0;
|
|
3926
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
3927
|
+
while (nextIndex < items.length) {
|
|
3928
|
+
const item = items[nextIndex];
|
|
3929
|
+
nextIndex += 1;
|
|
3930
|
+
await worker(item);
|
|
3931
|
+
}
|
|
3261
3932
|
});
|
|
3262
|
-
|
|
3263
|
-
if (!value || typeof value.uri !== "string" || !Number.isInteger(value.width) || !Number.isInteger(value.height) || value.scale !== scale) throw new AIError("image_generation_failed", "Native image upscaling returned an invalid result");
|
|
3264
|
-
return value;
|
|
3933
|
+
await Promise.all(workers);
|
|
3265
3934
|
}
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
const
|
|
3269
|
-
|
|
3270
|
-
const scale = assertLoRAScale(lora.scale ?? lora.metadata?.recommendedScale);
|
|
3271
|
-
if (resolvedDefinition.source.type === "local") {
|
|
3272
|
-
const path = filePathFromUri(resolvedDefinition.source.path, `lora ${id}`);
|
|
3273
|
-
const stat$1 = await require_files.files.file(path).stat();
|
|
3274
|
-
if (stat$1.type !== "file") throw new AIError("invalid_model", `Missing LoRA file: ${path}`);
|
|
3275
|
-
if (!hasSafetensorsHeader(stat$1.size)) throw new AIError("invalid_model", `Invalid LoRA safetensors file: ${path}`);
|
|
3276
|
-
return {
|
|
3277
|
-
id: lora.id,
|
|
3278
|
-
description: lora.description,
|
|
3279
|
-
path,
|
|
3280
|
-
uri: stat$1.uri,
|
|
3281
|
-
scale,
|
|
3282
|
-
source: resolvedDefinition.source,
|
|
3283
|
-
metadata: lora.metadata
|
|
3284
|
-
};
|
|
3285
|
-
}
|
|
3286
|
-
const url = loraUrl(resolvedDefinition);
|
|
3287
|
-
if (!url) throw new AIError("invalid_model", `Unsupported LoRA source: ${lora.id}`);
|
|
3288
|
-
const root = imageLoRARoot().directory(id);
|
|
3289
|
-
await root.create({ recursive: true });
|
|
3290
|
-
const destination = root.file(safeCacheName(sourceFileName(resolvedDefinition), "lora filename"));
|
|
3291
|
-
const expectedBytes = loraExpectedBytes(resolvedDefinition);
|
|
3292
|
-
const stat = await destination.stat();
|
|
3293
|
-
if (stat.type !== "file" || !hasSafetensorsHeader(stat.size) || expectedBytes !== void 0 && stat.size !== expectedBytes) await destination.download(url, {
|
|
3294
|
-
headers: loraHeaders(resolvedDefinition, options.accessToken),
|
|
3295
|
-
expectedBytes,
|
|
3296
|
-
atomic: true,
|
|
3297
|
-
resume: true,
|
|
3298
|
-
signal: options.signal,
|
|
3299
|
-
onProgress(progress) {
|
|
3300
|
-
options.onProgress?.({
|
|
3301
|
-
lora: lora.id,
|
|
3302
|
-
file: destination.path,
|
|
3303
|
-
bytesWritten: progress.bytesWritten,
|
|
3304
|
-
totalBytes: progress.totalBytes,
|
|
3305
|
-
fraction: progress.fraction
|
|
3306
|
-
});
|
|
3307
|
-
}
|
|
3308
|
-
}).result;
|
|
3309
|
-
await root.file("manifest.json").write(JSON.stringify({
|
|
3310
|
-
formatVersion: 1,
|
|
3311
|
-
id: lora.id,
|
|
3312
|
-
file: destination.path.split("/").pop(),
|
|
3313
|
-
source: resolvedDefinition.source,
|
|
3314
|
-
metadata: lora.metadata
|
|
3315
|
-
}, null, 2));
|
|
3316
|
-
const preparedStat = await destination.stat();
|
|
3317
|
-
if (preparedStat.type !== "file" || !hasSafetensorsHeader(preparedStat.size)) throw new AIError("invalid_model", `Invalid LoRA safetensors file: ${destination.path}`);
|
|
3318
|
-
return {
|
|
3319
|
-
id: lora.id,
|
|
3320
|
-
description: lora.description,
|
|
3321
|
-
path: destination.path,
|
|
3322
|
-
uri: preparedStat.uri,
|
|
3323
|
-
scale,
|
|
3324
|
-
source: resolvedDefinition.source,
|
|
3325
|
-
metadata: lora.metadata
|
|
3326
|
-
};
|
|
3935
|
+
function hfResolveUrl(repo, revision, path) {
|
|
3936
|
+
const encodedRepo = repo.split("/").map(encodeURIComponent).join("/");
|
|
3937
|
+
const encodedPath = path.split("/").map(encodeURIComponent).join("/");
|
|
3938
|
+
return `https://huggingface.co/${encodedRepo}/resolve/${encodeURIComponent(revision)}/${encodedPath}`;
|
|
3327
3939
|
}
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
}
|
|
3347
|
-
const
|
|
3348
|
-
const stat = await file.stat();
|
|
3349
|
-
const expectedBytes = loraExpectedBytes(resolvedDefinition);
|
|
3350
|
-
const installed = stat.type === "file";
|
|
3351
|
-
const verified = installed && hasSafetensorsHeader(stat.size) && (expectedBytes === void 0 || stat.size === expectedBytes);
|
|
3940
|
+
const imageDimensionMultiple = 16;
|
|
3941
|
+
const minimumImageDimension = 64;
|
|
3942
|
+
const defaultImageLongSide = 1152;
|
|
3943
|
+
const aspectRatioValues = {
|
|
3944
|
+
"1:1": [1, 1],
|
|
3945
|
+
"16:9": [16, 9],
|
|
3946
|
+
"9:16": [9, 16],
|
|
3947
|
+
"4:3": [4, 3],
|
|
3948
|
+
"3:2": [3, 2],
|
|
3949
|
+
"2:3": [2, 3],
|
|
3950
|
+
"21:9": [21, 9]
|
|
3951
|
+
};
|
|
3952
|
+
function normalizedDimension(value) {
|
|
3953
|
+
return Math.max(minimumImageDimension, Math.floor(value / imageDimensionMultiple) * imageDimensionMultiple);
|
|
3954
|
+
}
|
|
3955
|
+
function normalizeImageSize(size, maxLongSide = defaultImageLongSide) {
|
|
3956
|
+
if (!Number.isFinite(size.width) || size.width <= 0) throw new AIError("invalid_model", "size.width must be positive");
|
|
3957
|
+
if (!Number.isFinite(size.height) || size.height <= 0) throw new AIError("invalid_model", "size.height must be positive");
|
|
3958
|
+
if (maxLongSide !== false && (!Number.isInteger(maxLongSide) || maxLongSide < minimumImageDimension)) throw new AIError("invalid_model", `maxLongSide must be false or an integer of at least ${minimumImageDimension}`);
|
|
3959
|
+
const scale = maxLongSide === false ? 1 : Math.min(1, maxLongSide / Math.max(size.width, size.height));
|
|
3352
3960
|
return {
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
path: file.path,
|
|
3356
|
-
uri: stat.uri,
|
|
3357
|
-
scale,
|
|
3358
|
-
source: resolvedDefinition.source,
|
|
3359
|
-
metadata: lora.metadata,
|
|
3360
|
-
installed,
|
|
3361
|
-
verified,
|
|
3362
|
-
size: stat.size
|
|
3961
|
+
width: normalizedDimension(size.width * scale),
|
|
3962
|
+
height: normalizedDimension(size.height * scale)
|
|
3363
3963
|
};
|
|
3364
3964
|
}
|
|
3365
|
-
|
|
3366
|
-
|
|
3965
|
+
function imageSizeForPreset(aspectRatio, maxLongSide = defaultImageLongSide) {
|
|
3966
|
+
const ratio = aspectRatioValues[aspectRatio];
|
|
3967
|
+
if (!ratio) throw new AIError("invalid_model", `Unsupported image aspect ratio: ${aspectRatio}`);
|
|
3968
|
+
assertPositiveInteger(maxLongSide, "maxLongSide");
|
|
3969
|
+
const [widthRatio, heightRatio] = ratio;
|
|
3970
|
+
return normalizeImageSize(widthRatio >= heightRatio ? {
|
|
3971
|
+
width: maxLongSide,
|
|
3972
|
+
height: maxLongSide * heightRatio / widthRatio
|
|
3973
|
+
} : {
|
|
3974
|
+
width: maxLongSide * widthRatio / heightRatio,
|
|
3975
|
+
height: maxLongSide
|
|
3976
|
+
}, maxLongSide);
|
|
3977
|
+
}
|
|
3978
|
+
function imageLoRARoot() {
|
|
3979
|
+
return require_files.files.directory(require_files.files.paths.models, "image-loras");
|
|
3367
3980
|
}
|
|
3368
|
-
|
|
3369
|
-
const
|
|
3370
|
-
|
|
3981
|
+
function safeCacheName(value, name) {
|
|
3982
|
+
const trimmed = value.trim();
|
|
3983
|
+
if (!trimmed) throw new AIError("invalid_model", `${name} must not be empty`);
|
|
3984
|
+
return trimmed.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3371
3985
|
}
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
assertLoRACompatibility(lora, model);
|
|
3377
|
-
prepared.push(await prepareImageLoRA(lora, options));
|
|
3378
|
-
}
|
|
3379
|
-
return prepared;
|
|
3986
|
+
function sourceFileName(lora) {
|
|
3987
|
+
if (lora.source.type === "local") return lora.source.path.split("/").pop() || `${lora.id}.safetensors`;
|
|
3988
|
+
if (lora.source.type === "url") return lora.source.filename ?? lora.source.url.split("/").pop() ?? `${lora.id}.safetensors`;
|
|
3989
|
+
return lora.source.path?.split("/").pop() || `${lora.id}.safetensors`;
|
|
3380
3990
|
}
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
if (
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
if (
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
if (
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
if (!options.outpaint.autoCrop && (rect.x < 0 || rect.y < 0 || rect.x + rect.width > canvasWidth || rect.y + rect.height > canvasHeight)) throw new AIError("invalid_model", "outpaint.sourceRect must fit within the outpaint canvas");
|
|
3422
|
-
}
|
|
3423
|
-
if (options.resizeMode !== void 0 && options.resizeMode !== "stretch" && options.resizeMode !== "fit" && options.resizeMode !== "crop") throw new AIError("invalid_model", "resizeMode must be stretch, fit, or crop");
|
|
3424
|
-
if (options.maxInputLongSide !== void 0 && options.maxInputLongSide !== false && (!Number.isInteger(options.maxInputLongSide) || options.maxInputLongSide < 64)) throw new AIError("invalid_model", "maxInputLongSide must be false or an integer of at least 64");
|
|
3425
|
-
assertPositiveInteger(width, "width");
|
|
3426
|
-
assertPositiveInteger(height, "height");
|
|
3427
|
-
assertPositiveInteger(steps, "steps");
|
|
3428
|
-
if (seed !== void 0 && (!Number.isInteger(seed) || seed < 0)) throw new AIError("invalid_model", "seed must be a non-negative integer");
|
|
3429
|
-
if (!Number.isFinite(guidance) || guidance <= 0) throw new AIError("invalid_model", "guidance must be a positive number");
|
|
3430
|
-
if (referenceImagePaths.length > 3) throw new AIError("invalid_model", "references must contain at most 3 images");
|
|
3431
|
-
if (options.referenceInfluences !== void 0) {
|
|
3432
|
-
if (options.referenceInfluences.length !== referenceImagePaths.length) throw new AIError("invalid_model", "referenceInfluences must match references length");
|
|
3433
|
-
for (const [index, influence] of options.referenceInfluences.entries()) if (!Number.isFinite(influence) || influence < 0) throw new AIError("invalid_model", `referenceInfluences[${index}] must be a non-negative number`);
|
|
3991
|
+
function assertLoRAScale(scale, name = "lora.scale") {
|
|
3992
|
+
const value = scale ?? 1;
|
|
3993
|
+
if (!Number.isFinite(value) || value <= 0) throw new AIError("invalid_model", `${name} must be a positive number`);
|
|
3994
|
+
return value;
|
|
3995
|
+
}
|
|
3996
|
+
function assertLoRADefinition(lora) {
|
|
3997
|
+
safeCacheName(lora.id, "lora.id");
|
|
3998
|
+
assertLoRAScale(lora.scale ?? lora.metadata?.recommendedScale);
|
|
3999
|
+
imageSigmaSchedule([lora.metadata?.sigmaSchedule]);
|
|
4000
|
+
if (lora.metadata?.baseModel !== void 0) assertImageModel(lora.metadata.baseModel);
|
|
4001
|
+
if (lora.metadata?.precision !== void 0 && ![
|
|
4002
|
+
"float16",
|
|
4003
|
+
"bfloat16",
|
|
4004
|
+
"float32"
|
|
4005
|
+
].includes(lora.metadata.precision)) throw new AIError("invalid_model", "lora.metadata.precision is unsupported");
|
|
4006
|
+
}
|
|
4007
|
+
function assertLoRACompatibility(lora, model) {
|
|
4008
|
+
if (lora.metadata?.baseModel && lora.metadata.baseModel !== model) throw new AIError("invalid_model", `LoRA ${lora.id} targets ${lora.metadata.baseModel}, not ${model}`);
|
|
4009
|
+
}
|
|
4010
|
+
const commonLoRAFileNames = [
|
|
4011
|
+
"pytorch_lora_weights.safetensors",
|
|
4012
|
+
"adapter_model.safetensors",
|
|
4013
|
+
"lora.safetensors",
|
|
4014
|
+
"model.safetensors"
|
|
4015
|
+
];
|
|
4016
|
+
async function resolveHuggingFaceLoRA(lora, options) {
|
|
4017
|
+
if (lora.source.type !== "huggingface" || lora.source.path) return lora;
|
|
4018
|
+
const revision = lora.source.revision ?? "main";
|
|
4019
|
+
const manifestFile = imageLoRARoot().directory(safeCacheName(lora.id, "lora.id")).file("manifest.json");
|
|
4020
|
+
if ((await manifestFile.stat()).type === "file") {
|
|
4021
|
+
const manifest = await manifestFile.json();
|
|
4022
|
+
if (isRecord(manifest) && isRecord(manifest.source) && manifest.source.type === "huggingface" && manifest.source.repo === lora.source.repo && (manifest.source.revision ?? "main") === revision && typeof manifest.source.path === "string") return {
|
|
4023
|
+
...lora,
|
|
4024
|
+
source: {
|
|
4025
|
+
...lora.source,
|
|
4026
|
+
revision,
|
|
4027
|
+
path: manifest.source.path,
|
|
4028
|
+
bytes: lora.source.bytes ?? (typeof manifest.source.bytes === "number" ? manifest.source.bytes : void 0)
|
|
4029
|
+
}
|
|
4030
|
+
};
|
|
3434
4031
|
}
|
|
3435
|
-
|
|
3436
|
-
const
|
|
3437
|
-
accessToken: options.accessToken,
|
|
4032
|
+
const encodedRepo = lora.source.repo.split("/").map(encodeURIComponent).join("/");
|
|
4033
|
+
const response = await fetch(`https://huggingface.co/api/models/${encodedRepo}/revision/${encodeURIComponent(revision)}`, {
|
|
4034
|
+
headers: options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : void 0,
|
|
3438
4035
|
signal: options.signal
|
|
3439
|
-
})) : [];
|
|
3440
|
-
const sigmaSchedule = imageSigmaSchedule(selectedLoRAs.map((lora) => lora.metadata?.sigmaSchedule));
|
|
3441
|
-
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})`);
|
|
3442
|
-
const generationSteps = sigmaSchedule?.length ?? steps;
|
|
3443
|
-
const prepared = options.path && options.tokenizer ? void 0 : await withImageStage("download", () => prepareImageModelAtPath(definition, {
|
|
3444
|
-
model,
|
|
3445
|
-
accessToken: options.accessToken,
|
|
3446
|
-
concurrency: options.concurrency,
|
|
3447
|
-
onProgress: options.onModelProgress
|
|
3448
|
-
}, options.path ?? modelCacheRoot(definition.manifest).path));
|
|
3449
|
-
throwIfAborted(options.signal);
|
|
3450
|
-
const tokenizer = options.tokenizer ?? prepared?.tokenizer;
|
|
3451
|
-
const modelPath = options.path ?? prepared?.path;
|
|
3452
|
-
if (!tokenizer || !modelPath) throw new AIError("invalid_model", "Expected image model path and tokenizer");
|
|
3453
|
-
const tokenized = await withImageStage("tokenize", () => tokenizer({
|
|
3454
|
-
prompt,
|
|
3455
|
-
maxLength: 512
|
|
3456
|
-
}));
|
|
3457
|
-
throwIfAborted(options.signal);
|
|
3458
|
-
await withImageStage("tokenize", () => {
|
|
3459
|
-
assertTokenArray(tokenized.inputIds, "inputIds");
|
|
3460
|
-
assertTokenArray(tokenized.attentionMask, "attentionMask");
|
|
3461
|
-
if (tokenized.inputIds.length !== tokenized.attentionMask.length) throw new AIError("invalid_message", "inputIds and attentionMask must have the same length");
|
|
3462
4036
|
});
|
|
3463
|
-
|
|
3464
|
-
const
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
maxInputLongSide: options.maxInputLongSide === false ? 0 : options.maxInputLongSide ?? defaultImageLongSide,
|
|
3479
|
-
referenceImagePaths,
|
|
3480
|
-
referenceInfluences: options.referenceInfluences,
|
|
3481
|
-
loraPaths: selectedLoRAs.map((lora) => lora.path),
|
|
3482
|
-
loraScales: selectedLoRAs.map((lora) => lora.scale),
|
|
3483
|
-
loraIds: selectedLoRAs.map((lora) => lora.id),
|
|
3484
|
-
sigmaSchedule,
|
|
3485
|
-
width,
|
|
3486
|
-
height,
|
|
3487
|
-
steps: generationSteps,
|
|
3488
|
-
guidance,
|
|
3489
|
-
strength,
|
|
3490
|
-
seed,
|
|
3491
|
-
inputIds: tokenized.inputIds,
|
|
3492
|
-
attentionMask: tokenized.attentionMask,
|
|
3493
|
-
modelRoot: paths.path,
|
|
3494
|
-
textEncoderPath: paths.textEncoderPath,
|
|
3495
|
-
transformerPath: paths.transformerPath,
|
|
3496
|
-
vaePath: paths.vaePath
|
|
3497
|
-
};
|
|
3498
|
-
const fallbackMetadata = {
|
|
3499
|
-
model,
|
|
3500
|
-
width: outpaintSize?.width ?? width,
|
|
3501
|
-
height: outpaintSize?.height ?? height,
|
|
3502
|
-
steps: generationSteps,
|
|
3503
|
-
guidance,
|
|
3504
|
-
strength,
|
|
3505
|
-
seed,
|
|
3506
|
-
appliedLoRAs: selectedLoRAs.map((lora) => ({
|
|
3507
|
-
id: lora.id,
|
|
3508
|
-
path: lora.path,
|
|
3509
|
-
scale: lora.scale
|
|
3510
|
-
})),
|
|
3511
|
-
sigmaSchedule
|
|
3512
|
-
};
|
|
3513
|
-
try {
|
|
3514
|
-
const native = bridge();
|
|
3515
|
-
const cancelNative = () => {
|
|
3516
|
-
native.cancelImagegen?.();
|
|
3517
|
-
};
|
|
3518
|
-
const subscription = options.onProgress ? native.addListener?.("imagegenProgress", (event) => {
|
|
3519
|
-
const progress = imagegenProgress(event);
|
|
3520
|
-
if (progress) options.onProgress?.(progress);
|
|
3521
|
-
}) : void 0;
|
|
3522
|
-
try {
|
|
3523
|
-
options.signal?.addEventListener("abort", cancelNative, { once: true });
|
|
3524
|
-
throwIfAborted(options.signal);
|
|
3525
|
-
const value = await native.generateImage?.(request);
|
|
3526
|
-
throwIfAborted(options.signal);
|
|
3527
|
-
return imageResult(value, model, fallbackMetadata);
|
|
3528
|
-
} finally {
|
|
3529
|
-
subscription?.remove();
|
|
3530
|
-
options.signal?.removeEventListener("abort", cancelNative);
|
|
3531
|
-
}
|
|
3532
|
-
} catch (error) {
|
|
3533
|
-
if (error instanceof ImageGenerationError) throw error;
|
|
3534
|
-
if (error instanceof AIError) throw error;
|
|
3535
|
-
if (options.signal?.aborted) throw new AIError("generation_canceled", "Image generation canceled", error);
|
|
3536
|
-
const mapped = mapNativeError(error);
|
|
3537
|
-
if (mapped.code === "generation_failed" || mapped.code === "image_generation_failed") throw new ImageGenerationError(nativeErrorStage(error) ?? "denoise", nativeErrorMessage(error), error);
|
|
3538
|
-
throw mapped;
|
|
3539
|
-
}
|
|
3540
|
-
}
|
|
3541
|
-
async function generateImages(options) {
|
|
3542
|
-
const { count,...imageOptions } = options;
|
|
3543
|
-
const results = [];
|
|
3544
|
-
for (const seed of batchImageSeeds(count, options.seed)) {
|
|
3545
|
-
throwIfAborted(options.signal);
|
|
3546
|
-
results.push(await generateImage({
|
|
3547
|
-
...imageOptions,
|
|
3548
|
-
seed
|
|
3549
|
-
}));
|
|
4037
|
+
if (!response.ok) throw new AIError("invalid_model", `Unable to inspect Hugging Face LoRA ${lora.source.repo}: ${response.status}`);
|
|
4038
|
+
const value = await response.json();
|
|
4039
|
+
const siblings = isRecord(value) && Array.isArray(value.siblings) ? value.siblings.flatMap((item) => {
|
|
4040
|
+
if (!isRecord(item) || typeof item.rfilename !== "string") return [];
|
|
4041
|
+
return [{
|
|
4042
|
+
path: item.rfilename,
|
|
4043
|
+
bytes: typeof item.size === "number" ? item.size : void 0
|
|
4044
|
+
}];
|
|
4045
|
+
}) : [];
|
|
4046
|
+
const common = commonLoRAFileNames.map((name) => siblings.find((file) => file.path === name)).find(Boolean);
|
|
4047
|
+
const safetensors = siblings.filter((file) => file.path.endsWith(".safetensors"));
|
|
4048
|
+
const selected = common ?? (safetensors.length === 1 ? safetensors[0] : void 0);
|
|
4049
|
+
if (!selected) {
|
|
4050
|
+
const candidates = safetensors.map((file) => file.path).join(", ");
|
|
4051
|
+
throw new AIError("invalid_model", candidates ? `Multiple LoRA weights found for ${lora.id}; set source.path to one of: ${candidates}` : `No .safetensors LoRA weight found for ${lora.id}`);
|
|
3550
4052
|
}
|
|
3551
|
-
return
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
4053
|
+
return {
|
|
4054
|
+
...lora,
|
|
4055
|
+
source: {
|
|
4056
|
+
...lora.source,
|
|
4057
|
+
revision,
|
|
4058
|
+
path: selected.path,
|
|
4059
|
+
bytes: lora.source.bytes ?? selected.bytes
|
|
4060
|
+
}
|
|
4061
|
+
};
|
|
3560
4062
|
}
|
|
3561
|
-
function
|
|
3562
|
-
|
|
4063
|
+
function loraUrl(lora) {
|
|
4064
|
+
if (lora.source.type === "url") return lora.source.url;
|
|
4065
|
+
if (lora.source.type === "huggingface") {
|
|
4066
|
+
if (!lora.source.path) return void 0;
|
|
4067
|
+
return hfResolveUrl(lora.source.repo, lora.source.revision ?? "main", lora.source.path);
|
|
4068
|
+
}
|
|
3563
4069
|
}
|
|
3564
|
-
function
|
|
3565
|
-
return
|
|
4070
|
+
function loraExpectedBytes(lora) {
|
|
4071
|
+
return lora.source.type === "local" ? void 0 : lora.source.bytes;
|
|
3566
4072
|
}
|
|
3567
|
-
function
|
|
3568
|
-
return
|
|
4073
|
+
function hasSafetensorsHeader(size) {
|
|
4074
|
+
return size !== void 0 && size > 8;
|
|
3569
4075
|
}
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
//#region src/ai/image-composer.tsx
|
|
3573
|
-
const defaultImageSize = defaultImageLongSide;
|
|
3574
|
-
const defaultStepCount = 4;
|
|
3575
|
-
const minOutpaintScale = .35;
|
|
3576
|
-
const maxOutpaintScale = .95;
|
|
3577
|
-
function roundImageDimension(value) {
|
|
3578
|
-
return normalizeImageSize({
|
|
3579
|
-
width: value,
|
|
3580
|
-
height: value
|
|
3581
|
-
}, false).width;
|
|
4076
|
+
function loraHeaders(lora, accessToken) {
|
|
4077
|
+
return lora.source.type === "huggingface" && accessToken ? { Authorization: `Bearer ${accessToken}` } : void 0;
|
|
3582
4078
|
}
|
|
3583
|
-
function
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
height: maxSide
|
|
3587
|
-
};
|
|
3588
|
-
const aspect = size.width / size.height;
|
|
3589
|
-
return normalizeImageSize({
|
|
3590
|
-
width: aspect >= 1 ? maxSide : maxSide * aspect,
|
|
3591
|
-
height: aspect >= 1 ? maxSide / aspect : maxSide
|
|
3592
|
-
}, maxSide);
|
|
4079
|
+
function cachedLoRAFile(lora) {
|
|
4080
|
+
const id = safeCacheName(lora.id, "lora.id");
|
|
4081
|
+
return imageLoRARoot().directory(id).file(safeCacheName(sourceFileName(lora), "lora filename"));
|
|
3593
4082
|
}
|
|
3594
|
-
function
|
|
3595
|
-
|
|
4083
|
+
async function ensureModelFile(input) {
|
|
4084
|
+
const destination = require_files.files.file(input.path, input.file.destination);
|
|
4085
|
+
const stat = await destination.stat();
|
|
4086
|
+
if (stat.type === "file" && stat.size === input.file.bytes) {
|
|
4087
|
+
input.onProgress?.({
|
|
4088
|
+
model: input.model,
|
|
4089
|
+
file: input.file.destination,
|
|
4090
|
+
index: input.index,
|
|
4091
|
+
total: input.total,
|
|
4092
|
+
bytesWritten: input.file.bytes,
|
|
4093
|
+
totalBytes: input.file.bytes,
|
|
4094
|
+
fraction: 1
|
|
4095
|
+
});
|
|
4096
|
+
return;
|
|
4097
|
+
}
|
|
4098
|
+
await destination.download(hfResolveUrl(input.file.repo, input.file.revision, input.file.source), {
|
|
4099
|
+
headers: input.headers,
|
|
4100
|
+
expectedBytes: input.file.bytes,
|
|
4101
|
+
atomic: true,
|
|
4102
|
+
resume: true,
|
|
4103
|
+
onProgress(progress) {
|
|
4104
|
+
input.onProgress?.({
|
|
4105
|
+
model: input.model,
|
|
4106
|
+
file: input.file.destination,
|
|
4107
|
+
index: input.index,
|
|
4108
|
+
total: input.total,
|
|
4109
|
+
...progress
|
|
4110
|
+
});
|
|
4111
|
+
}
|
|
4112
|
+
}).result;
|
|
3596
4113
|
}
|
|
3597
|
-
function
|
|
3598
|
-
|
|
4114
|
+
async function writePreparedManifest(path, manifest) {
|
|
4115
|
+
await require_files.files.file(path, "manifest.json").write(JSON.stringify(manifest, null, 2));
|
|
3599
4116
|
}
|
|
3600
|
-
function
|
|
3601
|
-
const
|
|
3602
|
-
|
|
4117
|
+
async function imageModelState(model, path) {
|
|
4118
|
+
const definition = imageModelDefinition(model);
|
|
4119
|
+
let bytesPresent = 0;
|
|
4120
|
+
const missingFiles = [];
|
|
4121
|
+
const invalidFiles = [];
|
|
4122
|
+
for (const file of definition.files) {
|
|
4123
|
+
const stat = await require_files.files.file(path, file.destination).stat();
|
|
4124
|
+
if (stat.type !== "file") {
|
|
4125
|
+
missingFiles.push(file.destination);
|
|
4126
|
+
continue;
|
|
4127
|
+
}
|
|
4128
|
+
bytesPresent += Math.min(stat.size ?? 0, file.bytes);
|
|
4129
|
+
if (stat.size !== file.bytes) invalidFiles.push(file.destination);
|
|
4130
|
+
}
|
|
4131
|
+
const manifestStat = await require_files.files.file(path, "manifest.json").stat();
|
|
4132
|
+
if (manifestStat.type !== "file") missingFiles.push("manifest.json");
|
|
4133
|
+
const installed = missingFiles.length === 0;
|
|
3603
4134
|
return {
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
4135
|
+
model,
|
|
4136
|
+
path,
|
|
4137
|
+
paths: fluxImageModelPaths(path, definition.manifest),
|
|
4138
|
+
installed,
|
|
4139
|
+
verified: installed && invalidFiles.length === 0 && manifestStat.type === "file",
|
|
4140
|
+
bytesPresent,
|
|
4141
|
+
totalBytes: definition.totalBytes,
|
|
4142
|
+
missingFiles,
|
|
4143
|
+
invalidFiles
|
|
3608
4144
|
};
|
|
3609
4145
|
}
|
|
3610
|
-
function
|
|
3611
|
-
const
|
|
3612
|
-
const
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
};
|
|
3619
|
-
return clampOutpaintRect({
|
|
3620
|
-
x: nextCenter.x - width / 2,
|
|
3621
|
-
y: nextCenter.y - height / 2,
|
|
3622
|
-
width,
|
|
3623
|
-
height
|
|
4146
|
+
function tokenArray(value, name) {
|
|
4147
|
+
const candidate = isRecord(value) && "data" in value ? value.data : value;
|
|
4148
|
+
const values = Array.isArray(candidate) ? candidate : ArrayBuffer.isView(candidate) ? Array.from(candidate) : void 0;
|
|
4149
|
+
if (!values) throw new AIError("invalid_message", `Tokenizer did not return ${name}`);
|
|
4150
|
+
return values.map((token) => {
|
|
4151
|
+
const number$1 = typeof token === "bigint" ? Number(token) : token;
|
|
4152
|
+
if (!Number.isInteger(number$1) || number$1 < 0) throw new AIError("invalid_message", `${name} must contain token ids`);
|
|
4153
|
+
return number$1;
|
|
3624
4154
|
});
|
|
3625
4155
|
}
|
|
3626
|
-
function
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
if (input.mode === "text") {
|
|
3638
|
-
const references = (input.refs ?? []).map((image) => image.path);
|
|
3639
|
-
return {
|
|
3640
|
-
...base,
|
|
3641
|
-
references: references.length > 0 ? references : void 0,
|
|
3642
|
-
referenceInfluences: references.length > 0 ? references.map(() => 1) : void 0,
|
|
3643
|
-
strength: references.length > 0 ? 1 : void 0,
|
|
3644
|
-
width: defaultImageSize,
|
|
3645
|
-
height: defaultImageSize
|
|
3646
|
-
};
|
|
3647
|
-
}
|
|
3648
|
-
if (input.mode === "subject") {
|
|
3649
|
-
if (!input.subject) throw new Error("Subject mode requires a subject image");
|
|
3650
|
-
const refs = input.refs ?? [];
|
|
3651
|
-
const references = [input.subject, ...refs].map((image) => image.path);
|
|
3652
|
-
const size = imageSizeForAspectRatio(input.subject.size);
|
|
3653
|
-
return {
|
|
3654
|
-
...base,
|
|
3655
|
-
references,
|
|
3656
|
-
referenceInfluences: references.map(() => 1),
|
|
3657
|
-
strength: 1,
|
|
3658
|
-
width: size.width,
|
|
3659
|
-
height: size.height
|
|
3660
|
-
};
|
|
3661
|
-
}
|
|
3662
|
-
if (input.mode === "edit") {
|
|
3663
|
-
if (!input.source) throw new Error("Edit requires a source image");
|
|
3664
|
-
const references = (input.refs ?? []).map((image) => image.path);
|
|
3665
|
-
const size = roundedImageSize(input.source.size);
|
|
4156
|
+
async function createFluxTokenizer(path, manifest) {
|
|
4157
|
+
const paths = fluxImageModelPaths(path, manifest);
|
|
4158
|
+
const tokenizer = new Tokenizer_default(await require_files.files.file(paths.textEncoderPath, "tokenizer.json").json(), await require_files.files.file(paths.textEncoderPath, "tokenizer_config.json").json());
|
|
4159
|
+
const padTokenId = 151643;
|
|
4160
|
+
return ({ prompt, maxLength }) => {
|
|
4161
|
+
const inputIds = tokenArray(tokenizer.encode(formatQwen3KleinPrompt(prompt)).ids, "inputIds").slice(0, maxLength);
|
|
4162
|
+
const attentionMask = inputIds.map(() => 1);
|
|
4163
|
+
while (inputIds.length < maxLength) {
|
|
4164
|
+
inputIds.push(padTokenId);
|
|
4165
|
+
attentionMask.push(0);
|
|
4166
|
+
}
|
|
3666
4167
|
return {
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
references: references.length > 0 ? references : void 0,
|
|
3670
|
-
referenceInfluences: references.length > 0 ? references.map(() => 1) : void 0,
|
|
3671
|
-
strength: references.length > 0 ? 1 : .35,
|
|
3672
|
-
width: size.width,
|
|
3673
|
-
height: size.height
|
|
4168
|
+
inputIds,
|
|
4169
|
+
attentionMask
|
|
3674
4170
|
};
|
|
3675
|
-
}
|
|
3676
|
-
if (!input.source) throw new Error("Outpaint requires a source image");
|
|
3677
|
-
if (!input.rect) throw new Error("Outpaint requires a source placement");
|
|
3678
|
-
return {
|
|
3679
|
-
...base,
|
|
3680
|
-
image: input.source.path,
|
|
3681
|
-
outpaint: {
|
|
3682
|
-
width: defaultImageSize,
|
|
3683
|
-
height: defaultImageSize,
|
|
3684
|
-
sourceRect: input.rect,
|
|
3685
|
-
autoCrop: true
|
|
3686
|
-
},
|
|
3687
|
-
strength: .8,
|
|
3688
|
-
width: defaultImageSize,
|
|
3689
|
-
height: defaultImageSize
|
|
3690
4171
|
};
|
|
3691
4172
|
}
|
|
3692
|
-
function
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
4173
|
+
function formatQwen3KleinPrompt(prompt) {
|
|
4174
|
+
return [
|
|
4175
|
+
"<|im_start|>user",
|
|
4176
|
+
`${prompt.replaceAll("[IMG]", "")}<|im_end|>`,
|
|
4177
|
+
"<|im_start|>assistant",
|
|
4178
|
+
"<think>",
|
|
4179
|
+
"",
|
|
4180
|
+
"</think>",
|
|
4181
|
+
""
|
|
4182
|
+
].join("\n");
|
|
4183
|
+
}
|
|
4184
|
+
function assertPositiveInteger(value, name) {
|
|
4185
|
+
if (!Number.isInteger(value) || value <= 0) throw new AIError("invalid_model", `${name} must be a positive integer`);
|
|
4186
|
+
}
|
|
4187
|
+
function assertUnitInterval(value, name) {
|
|
4188
|
+
if (!Number.isFinite(value) || value <= 0 || value > 1) throw new AIError("invalid_model", `${name} must be greater than 0 and at most 1`);
|
|
4189
|
+
}
|
|
4190
|
+
function filePathFromUri(value, name) {
|
|
4191
|
+
const path = value.trim();
|
|
4192
|
+
if (!path) throw new AIError("invalid_message", `${name} must not be empty`);
|
|
4193
|
+
if (!path.startsWith("file://")) return path;
|
|
4194
|
+
try {
|
|
4195
|
+
return decodeURIComponent(new URL(path).pathname);
|
|
4196
|
+
} catch (error) {
|
|
4197
|
+
throw new AIError("invalid_message", `Invalid ${name} file URI`, error);
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
function assertTokenArray(value, name) {
|
|
4201
|
+
if (value.length === 0) throw new AIError("invalid_message", `${name} must not be empty`);
|
|
4202
|
+
for (const token of value) if (!Number.isInteger(token) || token < 0) throw new AIError("invalid_message", `${name} must contain token ids`);
|
|
4203
|
+
}
|
|
4204
|
+
function imageResult(value, model, fallbackMetadata) {
|
|
4205
|
+
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");
|
|
4206
|
+
const result = value;
|
|
4207
|
+
const metadata = isRecord(result.metadata) ? result.metadata : {};
|
|
3708
4208
|
return {
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
4209
|
+
uri: value.uri,
|
|
4210
|
+
width: value.width,
|
|
4211
|
+
height: value.height,
|
|
4212
|
+
model,
|
|
4213
|
+
metadata: {
|
|
4214
|
+
...fallbackMetadata,
|
|
4215
|
+
model: metadata.model === model ? model : fallbackMetadata.model,
|
|
4216
|
+
width: typeof metadata.width === "number" ? metadata.width : value.width,
|
|
4217
|
+
height: typeof metadata.height === "number" ? metadata.height : value.height,
|
|
4218
|
+
steps: typeof metadata.steps === "number" ? metadata.steps : fallbackMetadata.steps,
|
|
4219
|
+
guidance: typeof metadata.guidance === "number" ? metadata.guidance : fallbackMetadata.guidance,
|
|
4220
|
+
strength: typeof metadata.strength === "number" ? metadata.strength : fallbackMetadata.strength,
|
|
4221
|
+
seed: typeof metadata.seed === "number" ? metadata.seed : fallbackMetadata.seed,
|
|
4222
|
+
elapsedMs: typeof metadata.elapsedMs === "number" ? metadata.elapsedMs : fallbackMetadata.elapsedMs,
|
|
4223
|
+
appliedLoRAs: Array.isArray(metadata.appliedLoRAs) ? metadata.appliedLoRAs.flatMap((item) => {
|
|
4224
|
+
if (!isRecord(item) || typeof item.path !== "string") return [];
|
|
4225
|
+
return [{
|
|
4226
|
+
id: typeof item.id === "string" ? item.id : void 0,
|
|
4227
|
+
path: item.path,
|
|
4228
|
+
scale: typeof item.scale === "number" ? item.scale : 1
|
|
4229
|
+
}];
|
|
4230
|
+
}) : fallbackMetadata.appliedLoRAs
|
|
3724
4231
|
}
|
|
3725
4232
|
};
|
|
3726
4233
|
}
|
|
3727
|
-
function
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
error
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
const maxRefs = options.maxRefs ?? 3;
|
|
3735
|
-
async function pickRefs() {
|
|
3736
|
-
setState((current) => ({
|
|
3737
|
-
...current,
|
|
3738
|
-
isPicking: true,
|
|
3739
|
-
error: null
|
|
3740
|
-
}));
|
|
3741
|
-
try {
|
|
3742
|
-
const refs = [...store.get().refs, ...await pickImages(options.inputRoot)].slice(0, maxRefs);
|
|
3743
|
-
setState((current) => ({
|
|
3744
|
-
...current,
|
|
3745
|
-
refs,
|
|
3746
|
-
isPicking: false
|
|
3747
|
-
}));
|
|
3748
|
-
} catch (error) {
|
|
3749
|
-
setState((current) => ({
|
|
3750
|
-
...current,
|
|
3751
|
-
error: errorFromUnknown(error),
|
|
3752
|
-
isPicking: false
|
|
3753
|
-
}));
|
|
3754
|
-
}
|
|
4234
|
+
async function withImageStage(stage, operation) {
|
|
4235
|
+
try {
|
|
4236
|
+
return await operation();
|
|
4237
|
+
} catch (error) {
|
|
4238
|
+
if (error instanceof ImageGenerationError) throw error;
|
|
4239
|
+
if (error instanceof AIError && error.code === "generation_canceled") throw error;
|
|
4240
|
+
throw new ImageGenerationError(stage, error instanceof Error ? error.message : String(error), error);
|
|
3755
4241
|
}
|
|
3756
|
-
return {
|
|
3757
|
-
mode: "text",
|
|
3758
|
-
...state,
|
|
3759
|
-
pickRefs,
|
|
3760
|
-
removeRef: (index) => setState((current) => removeRefFromState(current, index)),
|
|
3761
|
-
request: (prompt) => ({
|
|
3762
|
-
mode: "text",
|
|
3763
|
-
prompt,
|
|
3764
|
-
refs: store.get().refs
|
|
3765
|
-
}),
|
|
3766
|
-
clear: () => setState({
|
|
3767
|
-
refs: [],
|
|
3768
|
-
isPicking: false,
|
|
3769
|
-
error: null
|
|
3770
|
-
})
|
|
3771
|
-
};
|
|
3772
4242
|
}
|
|
3773
|
-
function
|
|
3774
|
-
const
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
isPicking: false,
|
|
3778
|
-
error: null
|
|
3779
|
-
});
|
|
3780
|
-
const [state, setState] = require_state.useAtom(store);
|
|
3781
|
-
const maxRefs = options.maxRefs ?? 2;
|
|
3782
|
-
async function pickSubject() {
|
|
3783
|
-
setState((current) => ({
|
|
3784
|
-
...current,
|
|
3785
|
-
isPicking: true,
|
|
3786
|
-
error: null
|
|
3787
|
-
}));
|
|
3788
|
-
try {
|
|
3789
|
-
const subject = await pickOneImage(options.inputRoot);
|
|
3790
|
-
setState((current) => ({
|
|
3791
|
-
...current,
|
|
3792
|
-
subject: subject ?? current.subject,
|
|
3793
|
-
isPicking: false
|
|
3794
|
-
}));
|
|
3795
|
-
} catch (error) {
|
|
3796
|
-
setState((current) => ({
|
|
3797
|
-
...current,
|
|
3798
|
-
error: errorFromUnknown(error),
|
|
3799
|
-
isPicking: false
|
|
3800
|
-
}));
|
|
3801
|
-
}
|
|
3802
|
-
}
|
|
3803
|
-
async function pickRefs() {
|
|
3804
|
-
setState((current) => ({
|
|
3805
|
-
...current,
|
|
3806
|
-
isPicking: true,
|
|
3807
|
-
error: null
|
|
3808
|
-
}));
|
|
3809
|
-
try {
|
|
3810
|
-
const refs = [...store.get().refs, ...await pickImages(options.inputRoot)].slice(0, maxRefs);
|
|
3811
|
-
setState((current) => ({
|
|
3812
|
-
...current,
|
|
3813
|
-
refs,
|
|
3814
|
-
isPicking: false
|
|
3815
|
-
}));
|
|
3816
|
-
} catch (error) {
|
|
3817
|
-
setState((current) => ({
|
|
3818
|
-
...current,
|
|
3819
|
-
error: errorFromUnknown(error),
|
|
3820
|
-
isPicking: false
|
|
3821
|
-
}));
|
|
3822
|
-
}
|
|
3823
|
-
}
|
|
4243
|
+
function imagegenProgress(value) {
|
|
4244
|
+
const currentStep = value.currentStep;
|
|
4245
|
+
const totalSteps = value.totalSteps;
|
|
4246
|
+
if (typeof currentStep !== "number" || !Number.isFinite(currentStep) || typeof totalSteps !== "number" || !Number.isFinite(totalSteps) || totalSteps <= 0) return;
|
|
3824
4247
|
return {
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
removeSubject: () => setState((current) => ({
|
|
3829
|
-
...current,
|
|
3830
|
-
subject: null
|
|
3831
|
-
})),
|
|
3832
|
-
pickRefs,
|
|
3833
|
-
removeRef: (index) => setState((current) => removeRefFromState(current, index)),
|
|
3834
|
-
request: (prompt) => {
|
|
3835
|
-
const current = store.get();
|
|
3836
|
-
return {
|
|
3837
|
-
mode: "subject",
|
|
3838
|
-
prompt,
|
|
3839
|
-
subject: current.subject,
|
|
3840
|
-
refs: current.refs
|
|
3841
|
-
};
|
|
3842
|
-
},
|
|
3843
|
-
clear: () => setState({
|
|
3844
|
-
subject: null,
|
|
3845
|
-
refs: [],
|
|
3846
|
-
isPicking: false,
|
|
3847
|
-
error: null
|
|
3848
|
-
})
|
|
4248
|
+
currentStep,
|
|
4249
|
+
totalSteps,
|
|
4250
|
+
fraction: Math.max(0, Math.min(1, currentStep / totalSteps))
|
|
3849
4251
|
};
|
|
3850
4252
|
}
|
|
3851
|
-
function
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
}
|
|
4253
|
+
function throwIfAborted(signal) {
|
|
4254
|
+
if (signal?.aborted) throw new AIError("generation_canceled", "Image generation canceled");
|
|
4255
|
+
}
|
|
4256
|
+
function nativeErrorStage(error) {
|
|
4257
|
+
const stage = (error instanceof Error ? error.message : String(error)).match(/^\[(download|tokenize|encode|denoise|decode|write)]\s/)?.[1] ?? (isRecord(error) && typeof error.stage === "string" ? error.stage : isRecord(error) && isRecord(error.userInfo) && typeof error.userInfo.stage === "string" ? error.userInfo.stage : void 0);
|
|
4258
|
+
return stage === "download" || stage === "tokenize" || stage === "encode" || stage === "denoise" || stage === "decode" || stage === "write" ? stage : void 0;
|
|
4259
|
+
}
|
|
4260
|
+
function nativeErrorMessage(error) {
|
|
4261
|
+
return (error instanceof Error ? error.message : String(error)).replace(/^\[(download|tokenize|encode|denoise|decode|write)]\s*/, "");
|
|
4262
|
+
}
|
|
4263
|
+
async function prepareImageModel(options = {}) {
|
|
4264
|
+
const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
|
|
4265
|
+
return prepareImageModelAtPath(definition, options, options.directory ? require_files.files.directory(options.directory, definition.manifest.directory).path : modelCacheRoot(definition.manifest).path);
|
|
4266
|
+
}
|
|
4267
|
+
async function prepareImageModelAtPath(definition, options, path) {
|
|
4268
|
+
const model = definition.manifest.model;
|
|
4269
|
+
const root = require_files.files.directory(path);
|
|
4270
|
+
await root.create({ recursive: true });
|
|
4271
|
+
const headers = options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : void 0;
|
|
4272
|
+
const total = definition.files.length;
|
|
4273
|
+
const bytesByFile = /* @__PURE__ */ new Map();
|
|
4274
|
+
const emitProgress = (progress) => {
|
|
4275
|
+
const file = definition.files.find((candidate) => candidate.destination === progress.file);
|
|
4276
|
+
const bytesWritten = file ? Math.min(progress.bytesWritten, file.bytes) : progress.bytesWritten;
|
|
4277
|
+
bytesByFile.set(progress.file, bytesWritten);
|
|
4278
|
+
const modelBytesWritten = Array.from(bytesByFile.values()).reduce((sum, bytes) => sum + bytes, 0);
|
|
4279
|
+
options.onProgress?.({
|
|
4280
|
+
...progress,
|
|
4281
|
+
bytesWritten,
|
|
4282
|
+
modelBytesWritten,
|
|
4283
|
+
totalModelBytes: definition.totalBytes,
|
|
4284
|
+
modelFraction: modelBytesWritten / definition.totalBytes
|
|
4285
|
+
});
|
|
4286
|
+
};
|
|
4287
|
+
const missingFiles = [];
|
|
4288
|
+
for (const [index, file] of definition.files.entries()) {
|
|
4289
|
+
const stat = await require_files.files.file(root.path, file.destination).stat();
|
|
4290
|
+
if (stat.type === "file" && stat.size === file.bytes) emitProgress({
|
|
4291
|
+
model,
|
|
4292
|
+
file: file.destination,
|
|
4293
|
+
index: index + 1,
|
|
4294
|
+
total,
|
|
4295
|
+
bytesWritten: file.bytes,
|
|
4296
|
+
totalBytes: file.bytes,
|
|
4297
|
+
fraction: 1
|
|
4298
|
+
});
|
|
4299
|
+
else missingFiles.push({
|
|
4300
|
+
file,
|
|
4301
|
+
index: index + 1
|
|
4302
|
+
});
|
|
3901
4303
|
}
|
|
4304
|
+
await runConcurrent(missingFiles, downloadConcurrency(options.concurrency), async ({ file, index }) => {
|
|
4305
|
+
await ensureModelFile({
|
|
4306
|
+
model,
|
|
4307
|
+
path: root.path,
|
|
4308
|
+
file,
|
|
4309
|
+
headers,
|
|
4310
|
+
index,
|
|
4311
|
+
total,
|
|
4312
|
+
onProgress: emitProgress
|
|
4313
|
+
});
|
|
4314
|
+
});
|
|
4315
|
+
await writePreparedManifest(root.path, definition.manifest);
|
|
3902
4316
|
return {
|
|
3903
|
-
|
|
3904
|
-
...
|
|
3905
|
-
|
|
3906
|
-
removeSource: () => setState((current) => ({
|
|
3907
|
-
...current,
|
|
3908
|
-
source: null
|
|
3909
|
-
})),
|
|
3910
|
-
pickRefs,
|
|
3911
|
-
removeRef: (index) => setState((current) => removeRefFromState(current, index)),
|
|
3912
|
-
request: (prompt) => {
|
|
3913
|
-
const current = store.get();
|
|
3914
|
-
return {
|
|
3915
|
-
mode: "edit",
|
|
3916
|
-
prompt,
|
|
3917
|
-
source: current.source,
|
|
3918
|
-
refs: current.refs
|
|
3919
|
-
};
|
|
3920
|
-
},
|
|
3921
|
-
clear: () => setState({
|
|
3922
|
-
source: null,
|
|
3923
|
-
refs: [],
|
|
3924
|
-
isPicking: false,
|
|
3925
|
-
error: null
|
|
3926
|
-
})
|
|
4317
|
+
model,
|
|
4318
|
+
...fluxImageModelPaths(root.path, definition.manifest),
|
|
4319
|
+
tokenizer: await createFluxTokenizer(root.path, definition.manifest)
|
|
3927
4320
|
};
|
|
3928
4321
|
}
|
|
3929
|
-
function
|
|
3930
|
-
const
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
4322
|
+
async function verifyImageModel(options = {}) {
|
|
4323
|
+
const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
|
|
4324
|
+
return imageModelState(definition.manifest.model, modelPathFromOptions(definition, options.path));
|
|
4325
|
+
}
|
|
4326
|
+
async function listImageModels(options = {}) {
|
|
4327
|
+
return [...await Promise.all([flux2Klein4B8BitAbliterated.model, ternaryBonsaiImage4B.model].map((model) => {
|
|
4328
|
+
const definition = imageModelDefinition(model);
|
|
4329
|
+
return imageModelState(model, options.directory ? require_files.files.directory(options.directory, definition.manifest.directory).path : modelPathFromOptions(definition, void 0));
|
|
4330
|
+
}))];
|
|
4331
|
+
}
|
|
4332
|
+
async function deleteImageModel(options = {}) {
|
|
4333
|
+
const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
|
|
4334
|
+
await require_files.files.directory(modelPathFromOptions(definition, options.path)).delete({ recursive: true });
|
|
4335
|
+
}
|
|
4336
|
+
async function imageMemoryTelemetry() {
|
|
4337
|
+
const value = await bridge().imageMemoryTelemetry?.();
|
|
4338
|
+
if (!value) throw new AIError("image_generation_failed", "Image service is unavailable");
|
|
4339
|
+
return value;
|
|
4340
|
+
}
|
|
4341
|
+
async function unloadImageModel() {
|
|
4342
|
+
return await bridge().unloadImageModel?.() ?? false;
|
|
4343
|
+
}
|
|
4344
|
+
async function upscaleImage(options) {
|
|
4345
|
+
throwIfAborted(options.signal);
|
|
4346
|
+
const scale = normalizeUpscaleScale(options.scale);
|
|
4347
|
+
const value = await bridge().upscaleImage?.({
|
|
4348
|
+
imagePath: filePathFromUri(options.image, "image"),
|
|
4349
|
+
scale
|
|
3935
4350
|
});
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
...current,
|
|
3940
|
-
isPicking: true,
|
|
3941
|
-
error: null
|
|
3942
|
-
}));
|
|
3943
|
-
try {
|
|
3944
|
-
const source = await pickOneImage(options.inputRoot);
|
|
3945
|
-
setState((current) => ({
|
|
3946
|
-
...current,
|
|
3947
|
-
source: source ?? current.source,
|
|
3948
|
-
rect: source ? initialOutpaintRect(source.size) : current.rect,
|
|
3949
|
-
isPicking: false
|
|
3950
|
-
}));
|
|
3951
|
-
} catch (error) {
|
|
3952
|
-
setState((current) => ({
|
|
3953
|
-
...current,
|
|
3954
|
-
error: errorFromUnknown(error),
|
|
3955
|
-
isPicking: false
|
|
3956
|
-
}));
|
|
3957
|
-
}
|
|
3958
|
-
}
|
|
3959
|
-
return {
|
|
3960
|
-
mode: "outpaint",
|
|
3961
|
-
...state,
|
|
3962
|
-
pickSource,
|
|
3963
|
-
removeSource: () => setState((current) => ({
|
|
3964
|
-
...current,
|
|
3965
|
-
source: null,
|
|
3966
|
-
rect: null
|
|
3967
|
-
})),
|
|
3968
|
-
setRect: (rect) => setState((current) => ({
|
|
3969
|
-
...current,
|
|
3970
|
-
rect
|
|
3971
|
-
})),
|
|
3972
|
-
request: (prompt) => {
|
|
3973
|
-
const current = store.get();
|
|
3974
|
-
return {
|
|
3975
|
-
mode: "outpaint",
|
|
3976
|
-
prompt,
|
|
3977
|
-
source: current.source,
|
|
3978
|
-
rect: current.rect
|
|
3979
|
-
};
|
|
3980
|
-
},
|
|
3981
|
-
clear: () => setState({
|
|
3982
|
-
source: null,
|
|
3983
|
-
rect: null,
|
|
3984
|
-
isPicking: false,
|
|
3985
|
-
error: null
|
|
3986
|
-
})
|
|
3987
|
-
};
|
|
4351
|
+
throwIfAborted(options.signal);
|
|
4352
|
+
if (!value || typeof value.uri !== "string" || !Number.isInteger(value.width) || !Number.isInteger(value.height) || value.scale !== scale) throw new AIError("image_generation_failed", "Native image upscaling returned an invalid result");
|
|
4353
|
+
return value;
|
|
3988
4354
|
}
|
|
3989
|
-
function
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
status: "Generating...",
|
|
4009
|
-
isGenerating: true,
|
|
4010
|
-
error: null
|
|
4011
|
-
}));
|
|
4012
|
-
try {
|
|
4013
|
-
const result = await generateImage(buildImageGenerationRequest(input, {
|
|
4014
|
-
...options,
|
|
4015
|
-
signal: abortController.signal,
|
|
4016
|
-
onModelProgress(progress) {
|
|
4017
|
-
setState((current) => ({
|
|
4018
|
-
...current,
|
|
4019
|
-
progress: progress.modelFraction >= 1 ? .15 : progress.modelFraction * .15,
|
|
4020
|
-
status: progress.modelFraction >= 1 ? "Generating..." : modelProgressStatus(progress)
|
|
4021
|
-
}));
|
|
4022
|
-
},
|
|
4023
|
-
onProgress(progress) {
|
|
4024
|
-
setState((current) => ({
|
|
4025
|
-
...current,
|
|
4026
|
-
progress: .15 + progress.currentStep / progress.totalSteps * .85,
|
|
4027
|
-
status: `Step ${progress.currentStep}/${progress.totalSteps}`
|
|
4028
|
-
}));
|
|
4029
|
-
}
|
|
4030
|
-
}));
|
|
4031
|
-
setState((current) => ({
|
|
4032
|
-
...current,
|
|
4033
|
-
imageUri: result.uri,
|
|
4034
|
-
result,
|
|
4035
|
-
progress: 1,
|
|
4036
|
-
status: `${result.metadata.width}×${result.metadata.height} • ${result.metadata.steps} steps`,
|
|
4037
|
-
isGenerating: false
|
|
4038
|
-
}));
|
|
4039
|
-
return result;
|
|
4040
|
-
} catch (error) {
|
|
4041
|
-
setState((current) => ({
|
|
4042
|
-
...current,
|
|
4043
|
-
status: error instanceof Error ? error.message : String(error),
|
|
4044
|
-
error: errorFromUnknown(error),
|
|
4045
|
-
isGenerating: false
|
|
4046
|
-
}));
|
|
4047
|
-
return null;
|
|
4048
|
-
} finally {
|
|
4049
|
-
if (abortControllerRef.current === abortController) abortControllerRef.current = null;
|
|
4050
|
-
}
|
|
4051
|
-
}
|
|
4052
|
-
function cancel() {
|
|
4053
|
-
setState((current) => ({
|
|
4054
|
-
...current,
|
|
4055
|
-
status: "Canceling..."
|
|
4056
|
-
}));
|
|
4057
|
-
abortControllerRef.current?.abort();
|
|
4058
|
-
}
|
|
4059
|
-
function clear() {
|
|
4060
|
-
setState({
|
|
4061
|
-
imageUri: null,
|
|
4062
|
-
result: null,
|
|
4063
|
-
progress: 0,
|
|
4064
|
-
status: "",
|
|
4065
|
-
isGenerating: false,
|
|
4066
|
-
error: null
|
|
4067
|
-
});
|
|
4355
|
+
async function prepareImageLoRA(lora, options = {}) {
|
|
4356
|
+
assertLoRADefinition(lora);
|
|
4357
|
+
const resolvedDefinition = await resolveHuggingFaceLoRA(lora, options);
|
|
4358
|
+
const id = safeCacheName(lora.id, "lora.id");
|
|
4359
|
+
const scale = assertLoRAScale(lora.scale ?? lora.metadata?.recommendedScale);
|
|
4360
|
+
if (resolvedDefinition.source.type === "local") {
|
|
4361
|
+
const path = filePathFromUri(resolvedDefinition.source.path, `lora ${id}`);
|
|
4362
|
+
const stat$1 = await require_files.files.file(path).stat();
|
|
4363
|
+
if (stat$1.type !== "file") throw new AIError("invalid_model", `Missing LoRA file: ${path}`);
|
|
4364
|
+
if (!hasSafetensorsHeader(stat$1.size)) throw new AIError("invalid_model", `Invalid LoRA safetensors file: ${path}`);
|
|
4365
|
+
return {
|
|
4366
|
+
id: lora.id,
|
|
4367
|
+
description: lora.description,
|
|
4368
|
+
path,
|
|
4369
|
+
uri: stat$1.uri,
|
|
4370
|
+
scale,
|
|
4371
|
+
source: resolvedDefinition.source,
|
|
4372
|
+
metadata: lora.metadata
|
|
4373
|
+
};
|
|
4068
4374
|
}
|
|
4375
|
+
const url = loraUrl(resolvedDefinition);
|
|
4376
|
+
if (!url) throw new AIError("invalid_model", `Unsupported LoRA source: ${lora.id}`);
|
|
4377
|
+
const root = imageLoRARoot().directory(id);
|
|
4378
|
+
await root.create({ recursive: true });
|
|
4379
|
+
const destination = root.file(safeCacheName(sourceFileName(resolvedDefinition), "lora filename"));
|
|
4380
|
+
const expectedBytes = loraExpectedBytes(resolvedDefinition);
|
|
4381
|
+
const stat = await destination.stat();
|
|
4382
|
+
if (stat.type !== "file" || !hasSafetensorsHeader(stat.size) || expectedBytes !== void 0 && stat.size !== expectedBytes) await destination.download(url, {
|
|
4383
|
+
headers: loraHeaders(resolvedDefinition, options.accessToken),
|
|
4384
|
+
expectedBytes,
|
|
4385
|
+
atomic: true,
|
|
4386
|
+
resume: true,
|
|
4387
|
+
signal: options.signal,
|
|
4388
|
+
onProgress(progress) {
|
|
4389
|
+
options.onProgress?.({
|
|
4390
|
+
lora: lora.id,
|
|
4391
|
+
file: destination.path,
|
|
4392
|
+
bytesWritten: progress.bytesWritten,
|
|
4393
|
+
totalBytes: progress.totalBytes,
|
|
4394
|
+
fraction: progress.fraction
|
|
4395
|
+
});
|
|
4396
|
+
}
|
|
4397
|
+
}).result;
|
|
4398
|
+
await root.file("manifest.json").write(JSON.stringify({
|
|
4399
|
+
formatVersion: 1,
|
|
4400
|
+
id: lora.id,
|
|
4401
|
+
file: destination.path.split("/").pop(),
|
|
4402
|
+
source: resolvedDefinition.source,
|
|
4403
|
+
metadata: lora.metadata
|
|
4404
|
+
}, null, 2));
|
|
4405
|
+
const preparedStat = await destination.stat();
|
|
4406
|
+
if (preparedStat.type !== "file" || !hasSafetensorsHeader(preparedStat.size)) throw new AIError("invalid_model", `Invalid LoRA safetensors file: ${destination.path}`);
|
|
4069
4407
|
return {
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4408
|
+
id: lora.id,
|
|
4409
|
+
description: lora.description,
|
|
4410
|
+
path: destination.path,
|
|
4411
|
+
uri: preparedStat.uri,
|
|
4412
|
+
scale,
|
|
4413
|
+
source: resolvedDefinition.source,
|
|
4414
|
+
metadata: lora.metadata
|
|
4074
4415
|
};
|
|
4075
4416
|
}
|
|
4076
|
-
function
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
error: null
|
|
4094
|
-
});
|
|
4095
|
-
return modelState ?? null;
|
|
4096
|
-
} catch (error) {
|
|
4097
|
-
setState((current) => ({
|
|
4098
|
-
...current,
|
|
4099
|
-
checking: false,
|
|
4100
|
-
error: errorFromUnknown(error)
|
|
4101
|
-
}));
|
|
4102
|
-
return null;
|
|
4417
|
+
async function verifyImageLoRA(lora) {
|
|
4418
|
+
assertLoRADefinition(lora);
|
|
4419
|
+
const scale = assertLoRAScale(lora.scale ?? lora.metadata?.recommendedScale);
|
|
4420
|
+
let resolvedDefinition = lora;
|
|
4421
|
+
if (lora.source.type === "huggingface" && !lora.source.path) {
|
|
4422
|
+
const manifestFile = imageLoRARoot().directory(safeCacheName(lora.id, "lora.id")).file("manifest.json");
|
|
4423
|
+
if ((await manifestFile.stat()).type === "file") {
|
|
4424
|
+
const manifest = await manifestFile.json();
|
|
4425
|
+
if (isRecord(manifest) && isRecord(manifest.source) && manifest.source.type === "huggingface" && manifest.source.repo === lora.source.repo && (manifest.source.revision ?? "main") === (lora.source.revision ?? "main") && typeof manifest.source.path === "string") resolvedDefinition = {
|
|
4426
|
+
...lora,
|
|
4427
|
+
source: {
|
|
4428
|
+
...lora.source,
|
|
4429
|
+
path: manifest.source.path,
|
|
4430
|
+
revision: typeof manifest.source.revision === "string" ? manifest.source.revision : lora.source.revision,
|
|
4431
|
+
bytes: typeof manifest.source.bytes === "number" ? manifest.source.bytes : lora.source.bytes
|
|
4432
|
+
}
|
|
4433
|
+
};
|
|
4103
4434
|
}
|
|
4104
4435
|
}
|
|
4436
|
+
const file = resolvedDefinition.source.type === "local" ? require_files.files.file(filePathFromUri(resolvedDefinition.source.path, `lora ${lora.id}`)) : cachedLoRAFile(resolvedDefinition);
|
|
4437
|
+
const stat = await file.stat();
|
|
4438
|
+
const expectedBytes = loraExpectedBytes(resolvedDefinition);
|
|
4439
|
+
const installed = stat.type === "file";
|
|
4440
|
+
const verified = installed && hasSafetensorsHeader(stat.size) && (expectedBytes === void 0 || stat.size === expectedBytes);
|
|
4105
4441
|
return {
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
const ref = useStableRef(null);
|
|
4117
|
-
ref.current ??= require_state.atom(initialValue);
|
|
4118
|
-
return ref.current;
|
|
4119
|
-
}
|
|
4120
|
-
function useStableRef(initialValue) {
|
|
4121
|
-
return (0, react.useRef)(initialValue);
|
|
4122
|
-
}
|
|
4123
|
-
function removeRefFromState(state, index) {
|
|
4124
|
-
return {
|
|
4125
|
-
...state,
|
|
4126
|
-
refs: state.refs.filter((_, i) => i !== index)
|
|
4442
|
+
id: lora.id,
|
|
4443
|
+
description: lora.description,
|
|
4444
|
+
path: file.path,
|
|
4445
|
+
uri: stat.uri,
|
|
4446
|
+
scale,
|
|
4447
|
+
source: resolvedDefinition.source,
|
|
4448
|
+
metadata: lora.metadata,
|
|
4449
|
+
installed,
|
|
4450
|
+
verified,
|
|
4451
|
+
size: stat.size
|
|
4127
4452
|
};
|
|
4128
4453
|
}
|
|
4129
|
-
function
|
|
4130
|
-
return
|
|
4131
|
-
}
|
|
4132
|
-
async function pickOneImage(inputRoot) {
|
|
4133
|
-
const [picked] = await require_files.files.pick({
|
|
4134
|
-
type: "image",
|
|
4135
|
-
destination: inputDestination(inputRoot)
|
|
4136
|
-
});
|
|
4137
|
-
if (!picked) return null;
|
|
4138
|
-
return Object.assign(picked, { size: await getImageSize(picked.uri) });
|
|
4139
|
-
}
|
|
4140
|
-
async function pickImages(inputRoot) {
|
|
4141
|
-
const picked = await require_files.files.pick({
|
|
4142
|
-
type: "image",
|
|
4143
|
-
multiple: true,
|
|
4144
|
-
destination: inputDestination(inputRoot)
|
|
4145
|
-
});
|
|
4146
|
-
return Promise.all(picked.map(async (image) => Object.assign(image, { size: await getImageSize(image.uri) })));
|
|
4147
|
-
}
|
|
4148
|
-
function getImageSize(uri) {
|
|
4149
|
-
return new Promise((resolve, reject) => {
|
|
4150
|
-
react_native.Image.getSize(uri, (width, height) => resolve({
|
|
4151
|
-
width,
|
|
4152
|
-
height
|
|
4153
|
-
}), reject);
|
|
4154
|
-
});
|
|
4155
|
-
}
|
|
4156
|
-
function errorFromUnknown(error) {
|
|
4157
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
4454
|
+
async function listImageLoRAs(loras) {
|
|
4455
|
+
return Promise.all(loras.map((lora) => verifyImageLoRA(lora)));
|
|
4158
4456
|
}
|
|
4159
|
-
function
|
|
4160
|
-
|
|
4457
|
+
async function deleteImageLoRA(lora) {
|
|
4458
|
+
const id = safeCacheName(typeof lora === "string" ? lora : lora.id, "lora.id");
|
|
4459
|
+
await imageLoRARoot().directory(id).delete({ recursive: true });
|
|
4161
4460
|
}
|
|
4162
|
-
function
|
|
4163
|
-
|
|
4164
|
-
const
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
"TB"
|
|
4169
|
-
];
|
|
4170
|
-
let size = value / 1024;
|
|
4171
|
-
let index = 0;
|
|
4172
|
-
while (size >= 1024 && index < units.length - 1) {
|
|
4173
|
-
size /= 1024;
|
|
4174
|
-
index += 1;
|
|
4461
|
+
async function prepareSelectedImageLoRAs(loras, model, options = {}) {
|
|
4462
|
+
const prepared = [];
|
|
4463
|
+
for (const lora of loras) {
|
|
4464
|
+
assertLoRADefinition(lora);
|
|
4465
|
+
assertLoRACompatibility(lora, model);
|
|
4466
|
+
prepared.push(await prepareImageLoRA(lora, options));
|
|
4175
4467
|
}
|
|
4176
|
-
return
|
|
4468
|
+
return prepared;
|
|
4177
4469
|
}
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4470
|
+
async function generateImage(options) {
|
|
4471
|
+
throwIfAborted(options.signal);
|
|
4472
|
+
const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
|
|
4473
|
+
const model = definition.manifest.model;
|
|
4474
|
+
const prompt = options.prompt.trim();
|
|
4475
|
+
if (!prompt) throw new AIError("invalid_message", "Expected prompt");
|
|
4476
|
+
const presetSize = options.aspectRatio ? imageSizeForPreset(options.aspectRatio) : void 0;
|
|
4477
|
+
const requestedSize = normalizeImageSize({
|
|
4478
|
+
width: options.width ?? presetSize?.width ?? 1024,
|
|
4479
|
+
height: options.height ?? presetSize?.height ?? 1024
|
|
4480
|
+
}, false);
|
|
4481
|
+
const width = requestedSize.width;
|
|
4482
|
+
const height = requestedSize.height;
|
|
4483
|
+
const outpaintSize = options.outpaint ? normalizeImageSize({
|
|
4484
|
+
width: options.outpaint.width,
|
|
4485
|
+
height: options.outpaint.height
|
|
4486
|
+
}, false) : void 0;
|
|
4487
|
+
if (options.outpaint && (outpaintSize?.width !== options.outpaint.width || outpaintSize?.height !== options.outpaint.height)) throw new AIError("invalid_model", "outpaint width and height must be multiples of 16");
|
|
4488
|
+
const steps = options.steps ?? 4;
|
|
4489
|
+
const guidance = options.guidance ?? 1;
|
|
4490
|
+
const seed = options.seed ?? Math.floor(Math.random() * 4294967296);
|
|
4491
|
+
const sourceImagePath = options.image ? filePathFromUri(options.image, "image") : void 0;
|
|
4492
|
+
const maskImagePath = options.mask ? filePathFromUri(options.mask, "mask") : void 0;
|
|
4493
|
+
const referenceImagePaths = (options.references ?? []).map((reference, index) => filePathFromUri(reference, `references[${index}]`));
|
|
4494
|
+
const isImageConditioned = Boolean(sourceImagePath) || referenceImagePaths.length > 0;
|
|
4495
|
+
const strength = options.strength ?? (isImageConditioned ? .8 : void 0);
|
|
4496
|
+
if (maskImagePath && !sourceImagePath) throw new AIError("invalid_message", "mask requires image");
|
|
4497
|
+
if (options.maskMode !== void 0 && options.maskMode !== "paint" && options.maskMode !== "preserve") throw new AIError("invalid_model", "maskMode must be paint or preserve");
|
|
4498
|
+
if (options.outpaint && !sourceImagePath) throw new AIError("invalid_message", "outpaint requires image");
|
|
4499
|
+
if (options.outpaint) {
|
|
4500
|
+
assertPositiveInteger(options.outpaint.width, "outpaint.width");
|
|
4501
|
+
assertPositiveInteger(options.outpaint.height, "outpaint.height");
|
|
4502
|
+
const rect = options.outpaint.sourceRect;
|
|
4503
|
+
if (!rect) throw new AIError("invalid_model", "outpaint.sourceRect is required");
|
|
4504
|
+
assertPositiveInteger(rect.width, "outpaint.sourceRect.width");
|
|
4505
|
+
assertPositiveInteger(rect.height, "outpaint.sourceRect.height");
|
|
4506
|
+
if (!Number.isInteger(rect.x) || !Number.isInteger(rect.y)) throw new AIError("invalid_model", "outpaint.sourceRect x and y must be integers");
|
|
4507
|
+
const canvasWidth = outpaintSize?.width ?? options.outpaint.width;
|
|
4508
|
+
const canvasHeight = outpaintSize?.height ?? options.outpaint.height;
|
|
4509
|
+
if (!(rect.x < canvasWidth && rect.y < canvasHeight && rect.x + rect.width > 0 && rect.y + rect.height > 0)) throw new AIError("invalid_model", "outpaint.sourceRect must intersect the canvas");
|
|
4510
|
+
if (!options.outpaint.autoCrop && (rect.x < 0 || rect.y < 0 || rect.x + rect.width > canvasWidth || rect.y + rect.height > canvasHeight)) throw new AIError("invalid_model", "outpaint.sourceRect must fit within the outpaint canvas");
|
|
4187
4511
|
}
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
if (
|
|
4197
|
-
if (
|
|
4198
|
-
|
|
4199
|
-
}
|
|
4200
|
-
if (Array.isArray(definition)) {
|
|
4201
|
-
if (!Array.isArray(value)) return { issues: [issue(`Expected ${path.join(".") || "value"} to be array`, path)] };
|
|
4202
|
-
const itemDefinition = definition[0];
|
|
4203
|
-
const issues$1 = [];
|
|
4204
|
-
const output$1 = [];
|
|
4205
|
-
for (const [index, item] of value.entries()) {
|
|
4206
|
-
const result = validateDefinition(itemDefinition, item, [...path, index]);
|
|
4207
|
-
if (result.issues !== void 0) issues$1.push(...result.issues);
|
|
4208
|
-
else output$1.push(result.value);
|
|
4209
|
-
}
|
|
4210
|
-
if (issues$1.length > 0) return { issues: issues$1 };
|
|
4211
|
-
return { value: output$1 };
|
|
4212
|
-
}
|
|
4213
|
-
if (!isRecord(value)) return { issues: [issue(`Expected ${path.join(".") || "value"} to be object`, path)] };
|
|
4214
|
-
const output = {};
|
|
4215
|
-
const issues = [];
|
|
4216
|
-
for (const [key, field] of Object.entries(definition)) {
|
|
4217
|
-
const result = validateDefinition(field, value[key], [...path, key]);
|
|
4218
|
-
if (result.issues !== void 0) issues.push(...result.issues);
|
|
4219
|
-
else output[key] = result.value;
|
|
4220
|
-
}
|
|
4221
|
-
if (issues.length > 0) return { issues };
|
|
4222
|
-
return { value: output };
|
|
4223
|
-
}
|
|
4224
|
-
function bridgeSchema(definition) {
|
|
4225
|
-
if (definition === "string" || definition === "number" || definition === "boolean") return { type: definition };
|
|
4226
|
-
if (Array.isArray(definition)) return {
|
|
4227
|
-
type: "array",
|
|
4228
|
-
items: bridgeSchema(definition[0])
|
|
4229
|
-
};
|
|
4230
|
-
const properties = {};
|
|
4231
|
-
const required = [];
|
|
4232
|
-
for (const [key, value] of Object.entries(definition)) {
|
|
4233
|
-
properties[key] = bridgeSchema(value);
|
|
4234
|
-
required.push(key);
|
|
4512
|
+
if (options.resizeMode !== void 0 && options.resizeMode !== "stretch" && options.resizeMode !== "fit" && options.resizeMode !== "crop") throw new AIError("invalid_model", "resizeMode must be stretch, fit, or crop");
|
|
4513
|
+
if (options.maxInputLongSide !== void 0 && options.maxInputLongSide !== false && (!Number.isInteger(options.maxInputLongSide) || options.maxInputLongSide < 64)) throw new AIError("invalid_model", "maxInputLongSide must be false or an integer of at least 64");
|
|
4514
|
+
assertPositiveInteger(width, "width");
|
|
4515
|
+
assertPositiveInteger(height, "height");
|
|
4516
|
+
assertPositiveInteger(steps, "steps");
|
|
4517
|
+
if (seed !== void 0 && (!Number.isInteger(seed) || seed < 0)) throw new AIError("invalid_model", "seed must be a non-negative integer");
|
|
4518
|
+
if (!Number.isFinite(guidance) || guidance <= 0) throw new AIError("invalid_model", "guidance must be a positive number");
|
|
4519
|
+
if (referenceImagePaths.length > 3) throw new AIError("invalid_model", "references must contain at most 3 images");
|
|
4520
|
+
if (options.referenceInfluences !== void 0) {
|
|
4521
|
+
if (options.referenceInfluences.length !== referenceImagePaths.length) throw new AIError("invalid_model", "referenceInfluences must match references length");
|
|
4522
|
+
for (const [index, influence] of options.referenceInfluences.entries()) if (!Number.isFinite(influence) || influence < 0) throw new AIError("invalid_model", `referenceInfluences[${index}] must be a non-negative number`);
|
|
4235
4523
|
}
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4524
|
+
if (strength !== void 0) assertUnitInterval(strength, "strength");
|
|
4525
|
+
const selectedLoRAs = options.loras?.length ? await withImageStage("download", () => prepareSelectedImageLoRAs(options.loras ?? [], model, {
|
|
4526
|
+
accessToken: options.accessToken,
|
|
4527
|
+
signal: options.signal
|
|
4528
|
+
})) : [];
|
|
4529
|
+
const sigmaSchedule = imageSigmaSchedule(selectedLoRAs.map((lora) => lora.metadata?.sigmaSchedule));
|
|
4530
|
+
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})`);
|
|
4531
|
+
const generationSteps = sigmaSchedule?.length ?? steps;
|
|
4532
|
+
const prepared = options.path && options.tokenizer ? void 0 : await withImageStage("download", () => prepareImageModelAtPath(definition, {
|
|
4533
|
+
model,
|
|
4534
|
+
accessToken: options.accessToken,
|
|
4535
|
+
concurrency: options.concurrency,
|
|
4536
|
+
onProgress: options.onModelProgress
|
|
4537
|
+
}, options.path ?? modelCacheRoot(definition.manifest).path));
|
|
4538
|
+
throwIfAborted(options.signal);
|
|
4539
|
+
const tokenizer = options.tokenizer ?? prepared?.tokenizer;
|
|
4540
|
+
const modelPath = options.path ?? prepared?.path;
|
|
4541
|
+
if (!tokenizer || !modelPath) throw new AIError("invalid_model", "Expected image model path and tokenizer");
|
|
4542
|
+
const tokenized = await withImageStage("tokenize", () => tokenizer({
|
|
4543
|
+
prompt,
|
|
4544
|
+
maxLength: 512
|
|
4545
|
+
}));
|
|
4546
|
+
throwIfAborted(options.signal);
|
|
4547
|
+
await withImageStage("tokenize", () => {
|
|
4548
|
+
assertTokenArray(tokenized.inputIds, "inputIds");
|
|
4549
|
+
assertTokenArray(tokenized.attentionMask, "attentionMask");
|
|
4550
|
+
if (tokenized.inputIds.length !== tokenized.attentionMask.length) throw new AIError("invalid_message", "inputIds and attentionMask must have the same length");
|
|
4551
|
+
});
|
|
4552
|
+
const paths = fluxImageModelPaths(modelPath, definition.manifest);
|
|
4553
|
+
const request = {
|
|
4554
|
+
model,
|
|
4555
|
+
prompt,
|
|
4556
|
+
sourceImagePath,
|
|
4557
|
+
maskImagePath,
|
|
4558
|
+
maskMode: options.maskMode,
|
|
4559
|
+
outpaintWidth: outpaintSize?.width,
|
|
4560
|
+
outpaintHeight: outpaintSize?.height,
|
|
4561
|
+
outpaintSourceX: options.outpaint?.sourceRect.x,
|
|
4562
|
+
outpaintSourceY: options.outpaint?.sourceRect.y,
|
|
4563
|
+
outpaintSourceWidth: options.outpaint?.sourceRect.width,
|
|
4564
|
+
outpaintSourceHeight: options.outpaint?.sourceRect.height,
|
|
4565
|
+
outpaintAutoCrop: options.outpaint?.autoCrop,
|
|
4566
|
+
resizeMode: options.resizeMode,
|
|
4567
|
+
maxInputLongSide: options.maxInputLongSide === false ? 0 : options.maxInputLongSide ?? defaultImageLongSide,
|
|
4568
|
+
referenceImagePaths,
|
|
4569
|
+
referenceInfluences: options.referenceInfluences,
|
|
4570
|
+
loraPaths: selectedLoRAs.map((lora) => lora.path),
|
|
4571
|
+
loraScales: selectedLoRAs.map((lora) => lora.scale),
|
|
4572
|
+
loraIds: selectedLoRAs.map((lora) => lora.id),
|
|
4573
|
+
sigmaSchedule,
|
|
4574
|
+
width,
|
|
4575
|
+
height,
|
|
4576
|
+
steps: generationSteps,
|
|
4577
|
+
guidance,
|
|
4578
|
+
strength,
|
|
4579
|
+
seed,
|
|
4580
|
+
inputIds: tokenized.inputIds,
|
|
4581
|
+
attentionMask: tokenized.attentionMask,
|
|
4582
|
+
modelRoot: paths.path,
|
|
4583
|
+
textEncoderPath: paths.textEncoderPath,
|
|
4584
|
+
transformerPath: paths.transformerPath,
|
|
4585
|
+
vaePath: paths.vaePath
|
|
4240
4586
|
};
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
"~standard": {
|
|
4256
|
-
version: 1,
|
|
4257
|
-
vendor: "bleam",
|
|
4258
|
-
validate(value) {
|
|
4259
|
-
return api.safeParse(value);
|
|
4260
|
-
}
|
|
4261
|
-
},
|
|
4262
|
-
parse(value) {
|
|
4263
|
-
const result = api.safeParse(value);
|
|
4264
|
-
if (result.issues !== void 0) throw new ShapeError("validation_failed", result.issues.map((nextIssue) => nextIssue.message).join("\n"), result.issues);
|
|
4265
|
-
return result.value;
|
|
4266
|
-
},
|
|
4267
|
-
safeParse(value) {
|
|
4268
|
-
return validateDefinition(definition, value);
|
|
4269
|
-
},
|
|
4270
|
-
toJSONSchema() {
|
|
4271
|
-
return bridgeSchema(definition);
|
|
4272
|
-
}
|
|
4587
|
+
const fallbackMetadata = {
|
|
4588
|
+
model,
|
|
4589
|
+
width: outpaintSize?.width ?? width,
|
|
4590
|
+
height: outpaintSize?.height ?? height,
|
|
4591
|
+
steps: generationSteps,
|
|
4592
|
+
guidance,
|
|
4593
|
+
strength,
|
|
4594
|
+
seed,
|
|
4595
|
+
appliedLoRAs: selectedLoRAs.map((lora) => ({
|
|
4596
|
+
id: lora.id,
|
|
4597
|
+
path: lora.path,
|
|
4598
|
+
scale: lora.scale
|
|
4599
|
+
})),
|
|
4600
|
+
sigmaSchedule
|
|
4273
4601
|
};
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
}
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
return JSON.stringify(value) ?? "undefined";
|
|
4293
|
-
}
|
|
4294
|
-
|
|
4295
|
-
//#endregion
|
|
4296
|
-
//#region src/ai/bonsai-generation.ts
|
|
4297
|
-
async function prepareBonsaiGeneration(input) {
|
|
4298
|
-
const state = await verifyBonsaiTextModel(input.model, { path: input.path });
|
|
4299
|
-
if (!state.verified) throw new AIError("model_unavailable", `${input.model.id} is not prepared. Call prepareTextModel() before generation.`);
|
|
4300
|
-
const tokenizer = new Tokenizer_default(await require_files.files.file(state.path, "tokenizer.json").json(), await require_files.files.file(state.path, "tokenizer_config.json").json());
|
|
4301
|
-
const inputIds = tokenizer.encode(formatMessages(messagesFromInput(input))).ids.map(Number);
|
|
4302
|
-
if (!inputIds.length || inputIds.some((token) => !Number.isInteger(token))) throw new AIError("invalid_message", "Tokenizer returned invalid token ids");
|
|
4303
|
-
return {
|
|
4304
|
-
request: {
|
|
4305
|
-
id: input.id,
|
|
4306
|
-
model: input.model.id,
|
|
4307
|
-
modelPath: state.path,
|
|
4308
|
-
inputIds,
|
|
4309
|
-
endTokenIds: endTokenIds(input.model.id),
|
|
4310
|
-
maxTokens: input.maxTokens,
|
|
4311
|
-
temperature: input.temperature,
|
|
4312
|
-
seed: input.seed
|
|
4313
|
-
},
|
|
4314
|
-
decode(tokenIds) {
|
|
4315
|
-
return tokenIds.length ? tokenizer.decode(tokenIds, { skip_special_tokens: true }) : "";
|
|
4602
|
+
try {
|
|
4603
|
+
const native = bridge();
|
|
4604
|
+
const cancelNative = () => {
|
|
4605
|
+
native.cancelImagegen?.();
|
|
4606
|
+
};
|
|
4607
|
+
const subscription = options.onProgress ? native.addListener?.("imagegenProgress", (event) => {
|
|
4608
|
+
const progress = imagegenProgress(event);
|
|
4609
|
+
if (progress) options.onProgress?.(progress);
|
|
4610
|
+
}) : void 0;
|
|
4611
|
+
try {
|
|
4612
|
+
options.signal?.addEventListener("abort", cancelNative, { once: true });
|
|
4613
|
+
throwIfAborted(options.signal);
|
|
4614
|
+
const value = await native.generateImage?.(request);
|
|
4615
|
+
throwIfAborted(options.signal);
|
|
4616
|
+
return imageResult(value, model, fallbackMetadata);
|
|
4617
|
+
} finally {
|
|
4618
|
+
subscription?.remove();
|
|
4619
|
+
options.signal?.removeEventListener("abort", cancelNative);
|
|
4316
4620
|
}
|
|
4317
|
-
}
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
if (
|
|
4323
|
-
|
|
4324
|
-
}
|
|
4325
|
-
if (!input.prompt?.trim()) throw new AIError("invalid_message", "Expected prompt");
|
|
4326
|
-
return [...input.system ? [{
|
|
4327
|
-
role: "system",
|
|
4328
|
-
content: input.system
|
|
4329
|
-
}] : [], {
|
|
4330
|
-
role: "user",
|
|
4331
|
-
content: input.prompt
|
|
4332
|
-
}];
|
|
4621
|
+
} catch (error) {
|
|
4622
|
+
if (error instanceof ImageGenerationError) throw error;
|
|
4623
|
+
if (error instanceof AIError) throw error;
|
|
4624
|
+
if (options.signal?.aborted) throw new AIError("generation_canceled", "Image generation canceled", error);
|
|
4625
|
+
const mapped = mapNativeError(error);
|
|
4626
|
+
if (mapped.code === "generation_failed" || mapped.code === "image_generation_failed") throw new ImageGenerationError(nativeErrorStage(error) ?? "denoise", nativeErrorMessage(error), error);
|
|
4627
|
+
throw mapped;
|
|
4628
|
+
}
|
|
4333
4629
|
}
|
|
4334
|
-
function
|
|
4335
|
-
|
|
4336
|
-
const
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4630
|
+
async function generateImages(options) {
|
|
4631
|
+
const { count,...imageOptions } = options;
|
|
4632
|
+
const results = [];
|
|
4633
|
+
for (const seed of batchImageSeeds(count, options.seed)) {
|
|
4634
|
+
throwIfAborted(options.signal);
|
|
4635
|
+
results.push(await generateImage({
|
|
4636
|
+
...imageOptions,
|
|
4637
|
+
seed
|
|
4638
|
+
}));
|
|
4639
|
+
}
|
|
4640
|
+
return results;
|
|
4343
4641
|
}
|
|
4344
|
-
|
|
4345
|
-
|
|
4642
|
+
const imageJobManagerKey = Symbol.for("bleam.ai.imageJobManager.v2");
|
|
4643
|
+
function imageJobManager() {
|
|
4644
|
+
const scope = globalThis;
|
|
4645
|
+
const manager = scope[imageJobManagerKey] ?? new ImageJobManager(new CollectionImageJobRepository(), generateImage);
|
|
4646
|
+
manager.setExecutor(generateImage);
|
|
4647
|
+
scope[imageJobManagerKey] = manager;
|
|
4648
|
+
return manager;
|
|
4649
|
+
}
|
|
4650
|
+
async function queueImage(options) {
|
|
4651
|
+
return imageJobManager().queue(options);
|
|
4652
|
+
}
|
|
4653
|
+
const image = Object.freeze({
|
|
4654
|
+
generate: generateImage,
|
|
4655
|
+
generateMany: generateImages,
|
|
4656
|
+
queue: queueImage,
|
|
4657
|
+
upscale: upscaleImage
|
|
4658
|
+
});
|
|
4659
|
+
const jobs = Object.freeze({
|
|
4660
|
+
get: (id) => imageJobManager().get(id),
|
|
4661
|
+
list: () => imageJobManager().list(),
|
|
4662
|
+
cancel: (id) => imageJobManager().cancel(id),
|
|
4663
|
+
retry: (id) => imageJobManager().retry(id),
|
|
4664
|
+
resume: (id) => imageJobManager().resume(id),
|
|
4665
|
+
remove: (id) => imageJobManager().remove(id)
|
|
4666
|
+
});
|
|
4667
|
+
function useJob(id) {
|
|
4668
|
+
return useManagedJob(imageJobManager(), id);
|
|
4669
|
+
}
|
|
4670
|
+
function useJobList() {
|
|
4671
|
+
return useManagedJobList(imageJobManager());
|
|
4346
4672
|
}
|
|
4347
4673
|
|
|
4348
4674
|
//#endregion
|
|
4349
|
-
//#region src/ai/
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
if (model) return (await availability(model, options)).available;
|
|
4360
|
-
try {
|
|
4361
|
-
return await bridge().isAvailable?.() ?? false;
|
|
4362
|
-
} catch {
|
|
4363
|
-
return false;
|
|
4364
|
-
}
|
|
4675
|
+
//#region src/ai/image-composer.tsx
|
|
4676
|
+
const defaultImageSize = defaultImageLongSide;
|
|
4677
|
+
const defaultStepCount = 4;
|
|
4678
|
+
const minOutpaintScale = .35;
|
|
4679
|
+
const maxOutpaintScale = .95;
|
|
4680
|
+
function roundImageDimension(value) {
|
|
4681
|
+
return normalizeImageSize({
|
|
4682
|
+
width: value,
|
|
4683
|
+
height: value
|
|
4684
|
+
}, false).width;
|
|
4365
4685
|
}
|
|
4366
|
-
|
|
4367
|
-
if (
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
};
|
|
4377
|
-
return { available: true };
|
|
4378
|
-
}
|
|
4379
|
-
try {
|
|
4380
|
-
return await bridge().availability?.() ?? {
|
|
4381
|
-
available: false,
|
|
4382
|
-
reason: "unknown"
|
|
4383
|
-
};
|
|
4384
|
-
} catch {
|
|
4385
|
-
return {
|
|
4386
|
-
available: false,
|
|
4387
|
-
reason: "unknown"
|
|
4388
|
-
};
|
|
4389
|
-
}
|
|
4686
|
+
function imageSizeForAspectRatio(size, maxSide = defaultImageSize) {
|
|
4687
|
+
if (size.width <= 0 || size.height <= 0) return {
|
|
4688
|
+
width: maxSide,
|
|
4689
|
+
height: maxSide
|
|
4690
|
+
};
|
|
4691
|
+
const aspect = size.width / size.height;
|
|
4692
|
+
return normalizeImageSize({
|
|
4693
|
+
width: aspect >= 1 ? maxSide : maxSide * aspect,
|
|
4694
|
+
height: aspect >= 1 ? maxSide / aspect : maxSide
|
|
4695
|
+
}, maxSide);
|
|
4390
4696
|
}
|
|
4391
|
-
function
|
|
4392
|
-
return
|
|
4697
|
+
function roundedImageSize(size) {
|
|
4698
|
+
return normalizeImageSize(size);
|
|
4393
4699
|
}
|
|
4394
|
-
function
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
if (names.has(tool.name)) throw new AIError("invalid_schema", `Duplicate tool name: ${tool.name}`);
|
|
4401
|
-
names.add(tool.name);
|
|
4402
|
-
}
|
|
4700
|
+
function clamp(value, min$1, max) {
|
|
4701
|
+
return Math.min(max, Math.max(min$1, value));
|
|
4702
|
+
}
|
|
4703
|
+
function clampOutpaintRect(rect) {
|
|
4704
|
+
const width = Math.min(defaultImageSize, Math.max(1, Math.round(rect.width)));
|
|
4705
|
+
const height = Math.min(defaultImageSize, Math.max(1, Math.round(rect.height)));
|
|
4403
4706
|
return {
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
maxTokens: options.maxTokens,
|
|
4409
|
-
outputSchema: options.output ? schemaToBridgeSchema(options.output) : void 0,
|
|
4410
|
-
tools: options.tools?.map((tool) => ({
|
|
4411
|
-
name: tool.name,
|
|
4412
|
-
description: tool.description,
|
|
4413
|
-
inputSchema: schemaToBridgeSchema(tool.input)
|
|
4414
|
-
}))
|
|
4707
|
+
x: Math.round(clamp(rect.x, 0, defaultImageSize - width)),
|
|
4708
|
+
y: Math.round(clamp(rect.y, 0, defaultImageSize - height)),
|
|
4709
|
+
width,
|
|
4710
|
+
height
|
|
4415
4711
|
};
|
|
4416
4712
|
}
|
|
4417
|
-
function
|
|
4418
|
-
|
|
4419
|
-
const
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
callId: event.callId,
|
|
4432
|
-
output: typeof output === "string" ? output : stableStringify(output)
|
|
4433
|
-
});
|
|
4434
|
-
} catch (error) {
|
|
4435
|
-
await native.resolveToolCall?.({
|
|
4436
|
-
generationId: id,
|
|
4437
|
-
callId: event.callId,
|
|
4438
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4439
|
-
});
|
|
4440
|
-
}
|
|
4441
|
-
})();
|
|
4442
|
-
});
|
|
4443
|
-
}
|
|
4444
|
-
async function generate(options) {
|
|
4445
|
-
const output = options.output;
|
|
4446
|
-
const id = generationId();
|
|
4447
|
-
const native = bridge();
|
|
4448
|
-
const toolSubscription = listenForTools(id, "tools" in options ? options.tools : void 0);
|
|
4449
|
-
const abort = () => void native.cancelTextGeneration?.(id);
|
|
4450
|
-
options.signal?.addEventListener("abort", abort, { once: true });
|
|
4451
|
-
try {
|
|
4452
|
-
if (options.signal?.aborted) throw new AIError("generation_canceled", "Generation canceled");
|
|
4453
|
-
if (options.model) {
|
|
4454
|
-
const prepared = await prepareBonsaiGeneration({
|
|
4455
|
-
id,
|
|
4456
|
-
model: options.model,
|
|
4457
|
-
path: options.path,
|
|
4458
|
-
system: options.system,
|
|
4459
|
-
prompt: options.prompt,
|
|
4460
|
-
messages: options.messages,
|
|
4461
|
-
temperature: options.temperature,
|
|
4462
|
-
maxTokens: options.maxTokens,
|
|
4463
|
-
seed: options.seed
|
|
4464
|
-
});
|
|
4465
|
-
if (options.signal?.aborted) throw new AIError("generation_canceled", "Generation canceled");
|
|
4466
|
-
const tokenIds = await native.generateLocalText?.(prepared.request);
|
|
4467
|
-
if (!tokenIds) throw new AIError("unsupported_runtime", "Local text generation is unavailable in this runtime");
|
|
4468
|
-
return prepared.decode(tokenIds);
|
|
4469
|
-
}
|
|
4470
|
-
const value = await native.generate?.(nativeRequest(options, id));
|
|
4471
|
-
if (output) {
|
|
4472
|
-
const result = output["~standard"].validate(value);
|
|
4473
|
-
if (result.issues !== void 0) throw new AIError("validation_failed", result.issues.map((nextIssue) => nextIssue.message).join("\n"), result.issues);
|
|
4474
|
-
return result.value;
|
|
4475
|
-
}
|
|
4476
|
-
if (typeof value !== "string") throw new AIError("generation_failed", "Expected generated text");
|
|
4477
|
-
return value;
|
|
4478
|
-
} catch (error) {
|
|
4479
|
-
if (error instanceof AIError) throw error;
|
|
4480
|
-
throw mapNativeError(error);
|
|
4481
|
-
} finally {
|
|
4482
|
-
options.signal?.removeEventListener("abort", abort);
|
|
4483
|
-
toolSubscription?.remove();
|
|
4484
|
-
}
|
|
4485
|
-
}
|
|
4486
|
-
async function* stream(options) {
|
|
4487
|
-
if (options.model) {
|
|
4488
|
-
yield* streamBonsai(options);
|
|
4489
|
-
return;
|
|
4490
|
-
}
|
|
4491
|
-
const id = generationId();
|
|
4492
|
-
const native = bridge();
|
|
4493
|
-
const updates = [];
|
|
4494
|
-
let wake;
|
|
4495
|
-
let settled = false;
|
|
4496
|
-
let failure;
|
|
4497
|
-
const updateSubscription = native.addListener?.("textGenerationUpdate", (event) => {
|
|
4498
|
-
if (event.id !== id) return;
|
|
4499
|
-
updates.push(event.content);
|
|
4500
|
-
wake?.();
|
|
4501
|
-
wake = void 0;
|
|
4502
|
-
});
|
|
4503
|
-
const toolSubscription = listenForTools(id, options.tools);
|
|
4504
|
-
const abort = () => void native.cancelTextGeneration?.(id);
|
|
4505
|
-
options.signal?.addEventListener("abort", abort, { once: true });
|
|
4506
|
-
const completion = native.streamGenerate?.(nativeRequest(options, id)).catch((error) => {
|
|
4507
|
-
failure = error;
|
|
4508
|
-
}).finally(() => {
|
|
4509
|
-
settled = true;
|
|
4510
|
-
wake?.();
|
|
4511
|
-
wake = void 0;
|
|
4713
|
+
function rectForSourceScale(sourceSize, scale, center) {
|
|
4714
|
+
const aspect = sourceSize.width / sourceSize.height;
|
|
4715
|
+
const longSide = defaultImageSize * clamp(scale, minOutpaintScale, maxOutpaintScale);
|
|
4716
|
+
const width = aspect >= 1 ? longSide : longSide * aspect;
|
|
4717
|
+
const height = aspect >= 1 ? longSide / aspect : longSide;
|
|
4718
|
+
const nextCenter = center ?? {
|
|
4719
|
+
x: defaultImageSize / 2,
|
|
4720
|
+
y: defaultImageSize / 2
|
|
4721
|
+
};
|
|
4722
|
+
return clampOutpaintRect({
|
|
4723
|
+
x: nextCenter.x - width / 2,
|
|
4724
|
+
y: nextCenter.y - height / 2,
|
|
4725
|
+
width,
|
|
4726
|
+
height
|
|
4512
4727
|
});
|
|
4513
|
-
try {
|
|
4514
|
-
if (!completion) throw new AIError("unsupported_runtime", "Streaming is unavailable in this runtime");
|
|
4515
|
-
while (!settled || updates.length > 0) if (updates.length > 0) yield updates.shift();
|
|
4516
|
-
else await new Promise((resolve) => {
|
|
4517
|
-
wake = resolve;
|
|
4518
|
-
});
|
|
4519
|
-
await completion;
|
|
4520
|
-
if (failure) throw mapNativeError(failure);
|
|
4521
|
-
} finally {
|
|
4522
|
-
if (!settled) await native.cancelTextGeneration?.(id);
|
|
4523
|
-
options.signal?.removeEventListener("abort", abort);
|
|
4524
|
-
updateSubscription?.remove();
|
|
4525
|
-
toolSubscription?.remove();
|
|
4526
|
-
}
|
|
4527
4728
|
}
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
const native = bridge();
|
|
4531
|
-
if (options.signal?.aborted) throw new AIError("generation_canceled", "Generation canceled");
|
|
4532
|
-
const prepared = await prepareBonsaiGeneration({
|
|
4533
|
-
id,
|
|
4534
|
-
model: options.model,
|
|
4535
|
-
path: options.path,
|
|
4536
|
-
system: options.system,
|
|
4537
|
-
prompt: options.prompt,
|
|
4538
|
-
messages: options.messages,
|
|
4539
|
-
temperature: options.temperature,
|
|
4540
|
-
maxTokens: options.maxTokens,
|
|
4541
|
-
seed: options.seed
|
|
4542
|
-
});
|
|
4543
|
-
if (options.signal?.aborted) throw new AIError("generation_canceled", "Generation canceled");
|
|
4544
|
-
const updates = [];
|
|
4545
|
-
let wake;
|
|
4546
|
-
let settled = false;
|
|
4547
|
-
let failure;
|
|
4548
|
-
const updateSubscription = native.addListener?.("localTextGenerationUpdate", (event) => {
|
|
4549
|
-
if (event.id !== id) return;
|
|
4550
|
-
updates.push(event.tokenIds);
|
|
4551
|
-
wake?.();
|
|
4552
|
-
wake = void 0;
|
|
4553
|
-
});
|
|
4554
|
-
const abort = () => void native.cancelTextGeneration?.(id);
|
|
4555
|
-
options.signal?.addEventListener("abort", abort, { once: true });
|
|
4556
|
-
const completion = native.streamLocalText?.(prepared.request).catch((error) => {
|
|
4557
|
-
failure = error;
|
|
4558
|
-
return [];
|
|
4559
|
-
}).finally(() => {
|
|
4560
|
-
settled = true;
|
|
4561
|
-
wake?.();
|
|
4562
|
-
wake = void 0;
|
|
4563
|
-
});
|
|
4564
|
-
try {
|
|
4565
|
-
if (!completion) throw new AIError("unsupported_runtime", "Local text streaming is unavailable in this runtime");
|
|
4566
|
-
while (!settled || updates.length > 0) if (updates.length > 0) yield prepared.decode(updates.shift());
|
|
4567
|
-
else await new Promise((resolve) => {
|
|
4568
|
-
wake = resolve;
|
|
4569
|
-
});
|
|
4570
|
-
await completion;
|
|
4571
|
-
if (failure) throw mapNativeError(failure);
|
|
4572
|
-
} finally {
|
|
4573
|
-
if (!settled) await native.cancelTextGeneration?.(id);
|
|
4574
|
-
options.signal?.removeEventListener("abort", abort);
|
|
4575
|
-
updateSubscription?.remove();
|
|
4576
|
-
}
|
|
4729
|
+
function initialOutpaintRect(sourceSize) {
|
|
4730
|
+
return rectForSourceScale(sourceSize, .72);
|
|
4577
4731
|
}
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
};
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
setState({
|
|
4597
|
-
text: "",
|
|
4598
|
-
isGenerating: true,
|
|
4599
|
-
error: null
|
|
4600
|
-
});
|
|
4601
|
-
let text = "";
|
|
4602
|
-
try {
|
|
4603
|
-
for await (const snapshot of stream({
|
|
4604
|
-
...options,
|
|
4605
|
-
signal: abortController.signal
|
|
4606
|
-
})) {
|
|
4607
|
-
text = snapshot;
|
|
4608
|
-
setState({
|
|
4609
|
-
text,
|
|
4610
|
-
isGenerating: true,
|
|
4611
|
-
error: null
|
|
4612
|
-
});
|
|
4613
|
-
}
|
|
4614
|
-
setState({
|
|
4615
|
-
text,
|
|
4616
|
-
isGenerating: false,
|
|
4617
|
-
error: null
|
|
4618
|
-
});
|
|
4619
|
-
return text;
|
|
4620
|
-
} catch (error) {
|
|
4621
|
-
setState({
|
|
4622
|
-
text,
|
|
4623
|
-
isGenerating: false,
|
|
4624
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
4625
|
-
});
|
|
4626
|
-
return null;
|
|
4627
|
-
} finally {
|
|
4628
|
-
options.signal?.removeEventListener("abort", abort);
|
|
4629
|
-
if (abortControllerRef.current === abortController) abortControllerRef.current = null;
|
|
4630
|
-
}
|
|
4732
|
+
function buildImageGenerationRequest(input, options = {}) {
|
|
4733
|
+
const base = {
|
|
4734
|
+
prompt: input.prompt,
|
|
4735
|
+
mode: input.mode,
|
|
4736
|
+
steps: defaultStepCount,
|
|
4737
|
+
guidance: 1,
|
|
4738
|
+
...options
|
|
4739
|
+
};
|
|
4740
|
+
if (input.mode === "text") {
|
|
4741
|
+
const references = (input.refs ?? []).map((image$1) => image$1.path);
|
|
4742
|
+
return {
|
|
4743
|
+
...base,
|
|
4744
|
+
references: references.length > 0 ? references : void 0,
|
|
4745
|
+
referenceInfluences: references.length > 0 ? references.map(() => 1) : void 0,
|
|
4746
|
+
strength: references.length > 0 ? 1 : void 0,
|
|
4747
|
+
width: defaultImageSize,
|
|
4748
|
+
height: defaultImageSize
|
|
4749
|
+
};
|
|
4631
4750
|
}
|
|
4632
|
-
|
|
4633
|
-
|
|
4751
|
+
if (input.mode === "subject") {
|
|
4752
|
+
if (!input.subject) throw new Error("Subject mode requires a subject image");
|
|
4753
|
+
const refs = input.refs ?? [];
|
|
4754
|
+
const references = [input.subject, ...refs].map((image$1) => image$1.path);
|
|
4755
|
+
const size = imageSizeForAspectRatio(input.subject.size);
|
|
4756
|
+
return {
|
|
4757
|
+
...base,
|
|
4758
|
+
references,
|
|
4759
|
+
referenceInfluences: references.map(() => 1),
|
|
4760
|
+
strength: 1,
|
|
4761
|
+
width: size.width,
|
|
4762
|
+
height: size.height
|
|
4763
|
+
};
|
|
4634
4764
|
}
|
|
4635
|
-
|
|
4636
|
-
if (
|
|
4637
|
-
|
|
4765
|
+
if (input.mode === "edit") {
|
|
4766
|
+
if (!input.source) throw new Error("Edit requires a source image");
|
|
4767
|
+
const references = (input.refs ?? []).map((image$1) => image$1.path);
|
|
4768
|
+
const size = roundedImageSize(input.source.size);
|
|
4769
|
+
return {
|
|
4770
|
+
...base,
|
|
4771
|
+
image: input.source.path,
|
|
4772
|
+
references: references.length > 0 ? references : void 0,
|
|
4773
|
+
referenceInfluences: references.length > 0 ? references.map(() => 1) : void 0,
|
|
4774
|
+
strength: references.length > 0 ? 1 : .35,
|
|
4775
|
+
width: size.width,
|
|
4776
|
+
height: size.height
|
|
4777
|
+
};
|
|
4638
4778
|
}
|
|
4779
|
+
if (!input.source) throw new Error("Outpaint requires a source image");
|
|
4780
|
+
if (!input.rect) throw new Error("Outpaint requires a source placement");
|
|
4639
4781
|
return {
|
|
4640
|
-
...
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4782
|
+
...base,
|
|
4783
|
+
image: input.source.path,
|
|
4784
|
+
outpaint: {
|
|
4785
|
+
width: defaultImageSize,
|
|
4786
|
+
height: defaultImageSize,
|
|
4787
|
+
sourceRect: input.rect,
|
|
4788
|
+
autoCrop: true
|
|
4789
|
+
},
|
|
4790
|
+
strength: .8,
|
|
4791
|
+
width: defaultImageSize,
|
|
4792
|
+
height: defaultImageSize
|
|
4644
4793
|
};
|
|
4645
4794
|
}
|
|
4646
4795
|
|
|
4647
4796
|
//#endregion
|
|
4648
4797
|
exports.AIError = AIError;
|
|
4649
4798
|
exports.ImageGenerationError = ImageGenerationError;
|
|
4650
|
-
exports.availability = availability;
|
|
4651
4799
|
exports.bonsai = bonsai;
|
|
4652
4800
|
exports.bonsaiTextModelCatalog = bonsaiTextModelCatalog;
|
|
4653
4801
|
exports.buildImageGenerationRequest = buildImageGenerationRequest;
|
|
4802
|
+
exports.chat = chat;
|
|
4654
4803
|
exports.clampOutpaintRect = clampOutpaintRect;
|
|
4655
4804
|
exports.defaultImageLongSide = defaultImageLongSide;
|
|
4656
4805
|
exports.deleteImageLoRA = deleteImageLoRA;
|
|
@@ -4658,18 +4807,14 @@ exports.deleteImageModel = deleteImageModel;
|
|
|
4658
4807
|
exports.deleteTextModel = deleteBonsaiTextModel;
|
|
4659
4808
|
exports.flux2Klein4B8BitAbliterated = flux2Klein4B8BitAbliterated;
|
|
4660
4809
|
exports.fluxImageModelPaths = fluxImageModelPaths;
|
|
4661
|
-
exports.
|
|
4662
|
-
exports.generateImage = generateImage;
|
|
4663
|
-
exports.generateImages = generateImages;
|
|
4664
|
-
exports.getImageGenerationJob = getImageGenerationJob;
|
|
4810
|
+
exports.image = image;
|
|
4665
4811
|
exports.imageMemoryTelemetry = imageMemoryTelemetry;
|
|
4666
4812
|
exports.imageModelManifest = imageModelManifest;
|
|
4667
4813
|
exports.imageModelManifests = imageModelManifests;
|
|
4668
4814
|
exports.imageSizeForAspectRatio = imageSizeForAspectRatio;
|
|
4669
4815
|
exports.imageSizeForPreset = imageSizeForPreset;
|
|
4670
4816
|
exports.initialOutpaintRect = initialOutpaintRect;
|
|
4671
|
-
exports.
|
|
4672
|
-
exports.listImageGenerationJobs = listImageGenerationJobs;
|
|
4817
|
+
exports.jobs = jobs;
|
|
4673
4818
|
exports.listImageLoRAs = listImageLoRAs;
|
|
4674
4819
|
exports.listImageModels = listImageModels;
|
|
4675
4820
|
exports.listTextModels = listBonsaiTextModels;
|
|
@@ -4680,16 +4825,12 @@ exports.prepareTextModel = prepareBonsaiTextModel;
|
|
|
4680
4825
|
exports.rectForSourceScale = rectForSourceScale;
|
|
4681
4826
|
exports.roundImageDimension = roundImageDimension;
|
|
4682
4827
|
exports.roundedImageSize = roundedImageSize;
|
|
4683
|
-
exports.scheduleImageGeneration = scheduleImageGeneration;
|
|
4684
|
-
exports.shape = shape;
|
|
4685
|
-
exports.stream = stream;
|
|
4686
4828
|
exports.ternaryBonsaiImage4B = ternaryBonsaiImage4B;
|
|
4687
4829
|
exports.unloadImageModel = unloadImageModel;
|
|
4688
|
-
exports.
|
|
4689
|
-
exports.
|
|
4690
|
-
exports.
|
|
4691
|
-
exports.
|
|
4692
|
-
exports.useTextGeneration = useTextGeneration;
|
|
4830
|
+
exports.useChat = useChat;
|
|
4831
|
+
exports.useChatList = useChatList;
|
|
4832
|
+
exports.useJob = useJob;
|
|
4833
|
+
exports.useJobList = useJobList;
|
|
4693
4834
|
exports.verifyImageLoRA = verifyImageLoRA;
|
|
4694
4835
|
exports.verifyImageModel = verifyImageModel;
|
|
4695
4836
|
exports.verifyTextModel = verifyBonsaiTextModel;
|