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