QuantumHawk42 commited on
Commit
7f7fbbf
·
verified ·
1 Parent(s): 498391f

Update js/agate.js

Browse files
Files changed (1) hide show
  1. js/agate.js +161 -31
js/agate.js CHANGED
@@ -24,6 +24,12 @@ export const VERSIONS = {
24
  const countUrl = (v) => `https://huggingface.co/Logolabs/agate-preview-${v}/resolve/main/config.json`;
25
  const FILES_T2 = ["tokenizer.json", "tokenizer_config.json", "text_encoder.onnx", "generator.onnx", "taesd_decoder.onnx"];
26
 
 
 
 
 
 
 
27
  export async function webgpuStatus() {
28
  if (!("gpu" in navigator)) return { ok: false, why: "This browser does not expose WebGPU (navigator.gpu is missing)." };
29
  try {
@@ -75,6 +81,7 @@ export async function hasCachedModel(version = "001") {
75
  export async function clearCache() {
76
  let ok = true;
77
  for (const v of Object.values(VERSIONS)) { try { ok = (await caches.delete(v.cache)) && ok; } catch { ok = false; } }
 
78
  return ok;
79
  }
80
 
@@ -244,6 +251,80 @@ export class Agate {
244
  return out;
245
  }
246
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  // SD3's resolution shift in Agate's convention (t = 0 noise), as agate/pipeline.py shift_t
248
  static shiftT(t, shift) {
249
  if (shift === 1) return t;
@@ -304,7 +385,16 @@ export class Agate {
304
 
305
  // onPreview({ step, steps, plan, x1, hw }) after every step, when given and the graph outputs the plan: plan =
306
  // the thinker output for the conditional branch (Float32Array 640 x 16 x 16), x1 = z + (1 - t) v (guided).
307
- async generate({ prompt, negative = "", seed = 0, steps = 50, cfg = 3.0, noise = null, watermark = true, onStep = () => {}, onPreview = null, previewReady = () => true, shouldStop = () => false }) {
 
 
 
 
 
 
 
 
 
308
  const C = this.manifest.latent_ch, D = this.manifest.ctx_dim;
309
  const rc = this.mr ? this.manifest.resolutions[String(this.resolution)] : { latent_hw: this.manifest.latent_hw, shift: 1 };
310
  const hw = rc.latent_hw, shift = Number(rc.shift || 1);
@@ -329,42 +419,82 @@ export class Agate {
329
  feeds.counts = new ort.Tensor("float32", cnt, [2, L]);
330
  }
331
 
332
- let z = noise ? Float32Array.from(noise) : Agate.gaussianNoise(seed, per);
333
- const grid = Array.from({ length: steps + 1 }, (_, i) => Agate.shiftT(i / steps, shift));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  t0 = performance.now();
335
  const stepMs = [];
336
- if (this.fast) {
 
 
337
  z = await this.sampleFast({ z, grid, cfg, hw, C, L, D, ctx, mask, cnt: feeds.counts?.data, onStep, onPreview, previewReady, shouldStop, stepMs });
338
  } else {
339
- const zz = new Float32Array(2 * per), tt = new Float32Array(2);
340
- for (let i = 0; i < steps; i++) {
341
- if (shouldStop()) throw new Error("stopped");
342
- const ts = performance.now();
343
- zz.set(z, 0); zz.set(z, per);
344
- tt[0] = tt[1] = grid[i];
345
- const want = onPreview && this.hasPlan ? ["v", "plan"] : ["v"];
346
- const out = await this.sessions.gen.run({ z: new ort.Tensor("float32", zz, [2, C, hw, hw]), t: new ort.Tensor("float32", tt, [2]), ...feeds }, want);
347
- const v = out.v.data;
348
- const dt = grid[i + 1] - grid[i];
349
- const zn = new Float32Array(per);
350
- let x1 = null;
351
- if (out.plan) x1 = new Float32Array(per);
352
- for (let k = 0; k < per; k++) {
353
- const vc = v[k], vu = v[per + k], g = vu + cfg * (vc - vu);
354
- zn[k] = z[k] + dt * g;
355
- if (x1) x1[k] = z[k] + (1 - grid[i]) * g;
 
 
 
 
 
 
 
356
  }
357
- out.v.dispose?.();
358
- if (out.plan) { const plan = out.plan.data.slice(); out.plan.dispose?.(); onPreview({ step: i + 1, steps, plan, x1, hw }); }
359
- z = zn;
360
- stepMs.push(performance.now() - ts);
361
- onStep(i + 1, steps, z);
362
- await new Promise((r) => setTimeout(r, 0)); // let the progress bar paint
363
- }
364
  }
365
  T.samplerMs = performance.now() - t0;
366
- T.firstStepMs = stepMs[0];
367
- T.stepMs = stepMs.length > 1 ? (T.samplerMs - stepMs[0]) / (stepMs.length - 1) : stepMs[0];
 
368
 
369
  t0 = performance.now();
370
  const img = await this.sessions.vae.run({ latent: new ort.Tensor("float32", z, [1, C, hw, hw]) });
@@ -385,4 +515,4 @@ export class Agate {
385
  T.bucket = L;
386
  return { rgba, width: W, height: H, latent: z, timings: T, prepared: { ...prep, countsNonzero }, watermarked: watermark };
387
  }
388
- }
 
24
  const countUrl = (v) => `https://huggingface.co/Logolabs/agate-preview-${v}/resolve/main/config.json`;
25
  const FILES_T2 = ["tokenizer.json", "tokenizer_config.json", "text_encoder.onnx", "generator.onnx", "taesd_decoder.onnx"];
26
 
27
+ // Image-to-image: the SD 1.5 VAE encoder lives in its own Cache Storage bucket. It must NOT share a model
28
+ // cache: Agate.load() deletes every entry of a release's cache that is not one of that release's files.
29
+ const IMG_ENCODER_URL = "./models/vae_encoder/model_sd15.onnx";
30
+ const IMG_ENCODER_CACHE = "agate-img-encoder";
31
+ const SD_LATENT_SCALE = 0.18215;
32
+
33
  export async function webgpuStatus() {
34
  if (!("gpu" in navigator)) return { ok: false, why: "This browser does not expose WebGPU (navigator.gpu is missing)." };
35
  try {
 
81
  export async function clearCache() {
82
  let ok = true;
83
  for (const v of Object.values(VERSIONS)) { try { ok = (await caches.delete(v.cache)) && ok; } catch { ok = false; } }
84
+ try { await caches.delete(IMG_ENCODER_CACHE); } catch { ok = false; }
85
  return ok;
86
  }
87
 
 
251
  return out;
252
  }
253
 
254
+ // Loads the SD 1.5 VAE encoder on first use (own cache bucket, see IMG_ENCODER_CACHE).
255
+ async loadImageEncoder() {
256
+ if (this.sessions.imgEncoder) return this.sessions.imgEncoder;
257
+ const key = new Request(new URL(IMG_ENCODER_URL, location.href));
258
+ let buf;
259
+ try {
260
+ ({ buf } = await fetchCached(IMG_ENCODER_CACHE, IMG_ENCODER_URL, key, 0, () => {}));
261
+ } catch (e) {
262
+ throw new Error(`Could not load the image encoder (${IMG_ENCODER_URL}): ${e.message || e}`);
263
+ }
264
+ this.sessions.imgEncoder = await ort.InferenceSession.create(buf, {
265
+ executionProviders: this.ep === "webgpu" ? ["webgpu", "wasm"] : ["wasm"],
266
+ });
267
+ return this.sessions.imgEncoder;
268
+ }
269
+
270
+ // File -> scaled SD-VAE latent (Float32Array, C * (res/8)^2) at the CURRENT resolution. Re-run it after a
271
+ // resolution switch: a 512 px latent is 64x64, a 256 px one is 32x32.
272
+ async encodeReferenceImage(file) {
273
+ if (!file) throw new Error("Please select a reference image.");
274
+ const encoder = await this.loadImageEncoder();
275
+
276
+ // Read the image and center-crop it to a square.
277
+ const bitmap = await createImageBitmap(file);
278
+ const size = this.resolution;
279
+ const canvas = document.createElement("canvas");
280
+ canvas.width = size;
281
+ canvas.height = size;
282
+
283
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
284
+ const scale = Math.max(size / bitmap.width, size / bitmap.height);
285
+ const sw = size / scale;
286
+ const sh = size / scale;
287
+ const sx = (bitmap.width - sw) / 2;
288
+ const sy = (bitmap.height - sh) / 2;
289
+
290
+ ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, size, size);
291
+ bitmap.close();
292
+
293
+ const pixels = ctx.getImageData(0, 0, size, size).data;
294
+ const plane = size * size;
295
+ const rgb = new Float32Array(3 * plane);
296
+
297
+ // RGB, channel-first, normalized to [-1, 1].
298
+ for (let i = 0; i < plane; i++) {
299
+ rgb[i] = pixels[i * 4] / 127.5 - 1;
300
+ rgb[plane + i] = pixels[i * 4 + 1] / 127.5 - 1;
301
+ rgb[2 * plane + i] = pixels[i * 4 + 2] / 127.5 - 1;
302
+ }
303
+
304
+ const inputName = encoder.inputNames[0];
305
+ const outputName = encoder.outputNames[0];
306
+ const result = await encoder.run({
307
+ [inputName]: new ort.Tensor("float32", rgb, [1, 3, size, size]),
308
+ });
309
+
310
+ const latent = result[outputName];
311
+ const C = this.manifest.latent_ch;
312
+ const perLatent = C * (size / 8) * (size / 8);
313
+ let data = latent.data;
314
+
315
+ // Some SD VAE exports return "moments" (mean + logvar, 2*C channels) instead of a sampled latent.
316
+ // The mean is the first C channels, which are contiguous in NCHW layout.
317
+ if (data.length === 2 * perLatent) data = data.subarray(0, perLatent);
318
+ if (data.length !== perLatent) {
319
+ throw new Error(`Unexpected encoder output: ${latent.dims.join(" × ")} (expected ${perLatent} values)`);
320
+ }
321
+
322
+ // Agate samples in scaled SD-VAE latent space.
323
+ const out = Float32Array.from(data, (value) => value * SD_LATENT_SCALE);
324
+ latent.dispose?.();
325
+ return out;
326
+ }
327
+
328
  // SD3's resolution shift in Agate's convention (t = 0 noise), as agate/pipeline.py shift_t
329
  static shiftT(t, shift) {
330
  if (shift === 1) return t;
 
385
 
386
  // onPreview({ step, steps, plan, x1, hw }) after every step, when given and the graph outputs the plan: plan =
387
  // the thinker output for the conditional branch (Float32Array 640 x 16 x 16), x1 = z + (1 - t) v (guided).
388
+ //
389
+ // Image-to-image: pass referenceLatent (from encodeReferenceImage) and denoise in [0, 1]. denoise = 1 is plain
390
+ // text-to-image noise; lower values start closer to the reference. The sampler then runs round(steps * denoise)
391
+ // steps (at least 1; none at all for denoise 0, which returns the reference latent decoded), so the step size and the cost per unit of change match a full text-to-image run.
392
+ async generate({
393
+ prompt, negative = "", seed = 0, steps = 50, cfg = 3.0,
394
+ noise = null, referenceLatent = null, denoise = 1.0,
395
+ watermark = true, onStep = () => {}, onPreview = null,
396
+ previewReady = () => true, shouldStop = () => false
397
+ }) {
398
  const C = this.manifest.latent_ch, D = this.manifest.ctx_dim;
399
  const rc = this.mr ? this.manifest.resolutions[String(this.resolution)] : { latent_hw: this.manifest.latent_hw, shift: 1 };
400
  const hw = rc.latent_hw, shift = Number(rc.shift || 1);
 
419
  feeds.counts = new ort.Tensor("float32", cnt, [2, L]);
420
  }
421
 
422
+ // ---- starting point: pure noise (text-to-image) or a noised reference latent (image-to-image) ----
423
+ const dn = Number(denoise);
424
+ denoise = Number.isFinite(dn) ? Math.max(0, Math.min(1, dn)) : 1;
425
+
426
+ if (referenceLatent && referenceLatent.length !== per) {
427
+ throw new Error(
428
+ `Reference latent has ${referenceLatent.length} values; expected ${per}. ` +
429
+ `Re-encode the reference image after changing resolution.`
430
+ );
431
+ }
432
+
433
+ const zNoise = noise
434
+ ? Float32Array.from(noise)
435
+ : Agate.gaussianNoise(seed, per);
436
+
437
+ let z = zNoise;
438
+ const startT = referenceLatent ? 1 - denoise : 0;
439
+
440
+ if (referenceLatent && denoise === 0) {
441
+ z = Float32Array.from(referenceLatent);
442
+ } else if (referenceLatent) {
443
+ const ts = Agate.shiftT(startT, shift);
444
+ z = new Float32Array(per);
445
+
446
+ for (let k = 0; k < per; k++) {
447
+ z[k] = (1 - ts) * zNoise[k] + ts * referenceLatent[k];
448
+ }
449
+ }
450
+
451
+ // denoise 0 means "return the reference as is": no sampling. Otherwise at least one step.
452
+ const nSteps = !referenceLatent ? steps : denoise === 0 ? 0 : Math.max(1, Math.round(steps * denoise));
453
+ const grid = nSteps === 0 ? [1] : Array.from(
454
+ { length: nSteps + 1 },
455
+ (_, i) => Agate.shiftT(
456
+ startT + (1 - startT) * i / nSteps,
457
+ shift
458
+ )
459
+ );
460
+
461
  t0 = performance.now();
462
  const stepMs = [];
463
+ if (nSteps === 0) {
464
+ // z is already the reference latent: skip the sampler
465
+ } else if (this.fast) {
466
  z = await this.sampleFast({ z, grid, cfg, hw, C, L, D, ctx, mask, cnt: feeds.counts?.data, onStep, onPreview, previewReady, shouldStop, stepMs });
467
  } else {
468
+ const zz = new Float32Array(2 * per), tt = new Float32Array(2);
469
+ for (let i = 0; i < nSteps; i++) {
470
+ if (shouldStop()) throw new Error("stopped");
471
+ const ts = performance.now();
472
+ zz.set(z, 0); zz.set(z, per);
473
+ tt[0] = tt[1] = grid[i];
474
+ const want = onPreview && this.hasPlan ? ["v", "plan"] : ["v"];
475
+ const out = await this.sessions.gen.run({ z: new ort.Tensor("float32", zz, [2, C, hw, hw]), t: new ort.Tensor("float32", tt, [2]), ...feeds }, want);
476
+ const v = out.v.data;
477
+ const dt = grid[i + 1] - grid[i];
478
+ const zn = new Float32Array(per);
479
+ let x1 = null;
480
+ if (out.plan) x1 = new Float32Array(per);
481
+ for (let k = 0; k < per; k++) {
482
+ const vc = v[k], vu = v[per + k], g = vu + cfg * (vc - vu);
483
+ zn[k] = z[k] + dt * g;
484
+ if (x1) x1[k] = z[k] + (1 - grid[i]) * g;
485
+ }
486
+ out.v.dispose?.();
487
+ if (out.plan) { const plan = out.plan.data.slice(); out.plan.dispose?.(); onPreview({ step: i + 1, steps: nSteps, plan, x1, hw }); }
488
+ z = zn;
489
+ stepMs.push(performance.now() - ts);
490
+ onStep(i + 1, nSteps, z);
491
+ await new Promise((r) => setTimeout(r, 0)); // let the progress bar paint
492
  }
 
 
 
 
 
 
 
493
  }
494
  T.samplerMs = performance.now() - t0;
495
+ T.steps = nSteps;
496
+ T.firstStepMs = stepMs[0] ?? 0;
497
+ T.stepMs = stepMs.length > 1 ? (T.samplerMs - stepMs[0]) / (stepMs.length - 1) : (stepMs[0] ?? 0);
498
 
499
  t0 = performance.now();
500
  const img = await this.sessions.vae.run({ latent: new ort.Tensor("float32", z, [1, C, hw, hw]) });
 
515
  T.bucket = L;
516
  return { rgba, width: W, height: H, latent: z, timings: T, prepared: { ...prep, countsNonzero }, watermarked: watermark };
517
  }
518
+ }