bleam 0.0.11 → 0.0.13

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