Stefatorus Claude Opus 5.5 commited on
Commit
2dc8d66
·
1 Parent(s): aaa2328

Preview 002 and 003 (512 px), version selector, Show thinker, AI-output marking

Browse files

- Model selector (003 default for a first visit, last choice remembered per browser); 003 at 512 or 256 px
with its prompt pipeline (normaliser, spelled-out quoted text, count code, "no X" -> negative) and SD3 shift.
- The ONNX files now load from each release's model repo (Logolabs/agate-preview-00X, webgpu/); the graphs
also output the thinker plan. models/ (the original 001 build) stays for ?models=./models/.
- Show thinker (default on, ~2-4% per step): the 16 x 16 plan (PCA colours, as the ComfyUI live preview) and
the expected image x1 at every step.
- Every image is marked: invisible-watermark dwtDctSvd (payload AGATE + release, bit-identical JS port), PNG
provenance text chunks on save (no prompt), and a visible "AI-generated" note.
- Parity fixtures for 002 and 003, and a 38-prompt golden test of the prompt pipeline (?golden=1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018EtDckYfNgMbkwqVmTsdha

.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.weights filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -12,42 +12,86 @@ custom_headers:
12
  cross-origin-opener-policy: same-origin
13
  cross-origin-resource-policy: cross-origin
14
  models:
 
 
15
  - Logolabs/agate-preview-001
16
  ---
17
 
18
  # Agate WebGPU
19
 
20
- Agate Preview 001, LogoLabs' 0.26B text-to-image model (a 191M flow generator with a 68M text encoder),
21
- running as a static page. The tokenizer, text encoder, 50-step flow sampler with classifier-free guidance
22
- and the TAESD decoder all run on your own GPU through WebGPU (onnxruntime-web).
23
 
24
  **Runs entirely in your browser — prompts never leave your machine.** There is no server and no API call.
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  ## Requirements
27
 
28
- - A browser with WebGPU: a recent Chrome or Edge (113+). Safari and Firefox may lack WebGPU or need it
29
- enabled in their settings; without it the page offers a WebAssembly (CPU) path that works but takes
30
- minutes per image.
31
- - First load downloads about **530 MB** of weights (generator 385 MB, text encoder 137 MB, decoder 5 MB,
32
- tokenizer 4 MB). They are kept in the browser's Cache Storage, so later visits load from disk.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- ## Speed and accuracy
 
 
 
 
35
 
36
- - About 6–7 s per 50-step 256×256 image on an RTX 4060, after roughly 10 s of one-time model
37
- setup per visit.
38
- - With identical initial noise, the browser output matches the PyTorch reference pipeline within 5/255
39
- per pixel (run the self-test with `?parity=1`).
40
- - Seeds use a JavaScript PRNG, so a seed gives the same image in every browser but not the same image as
41
- the PyTorch pipeline with that seed.
42
 
43
- Query parameters: `?autoload=1` loads the model immediately, `?ep=wasm` forces the CPU backend,
44
- `?parity=1` runs the parity self-test.
 
45
 
46
  ## Links
47
 
48
  - [logolabs.org](https://logolabs.org)
49
  - [huggingface.co/Logolabs](https://huggingface.co/Logolabs)
50
- - Model card: [Logolabs/agate-preview-001](https://huggingface.co/Logolabs/agate-preview-001)
51
 
52
  Licence: MIT.
53
 
 
12
  cross-origin-opener-policy: same-origin
13
  cross-origin-resource-policy: cross-origin
14
  models:
15
+ - Logolabs/agate-preview-003
16
+ - Logolabs/agate-preview-002
17
  - Logolabs/agate-preview-001
18
  ---
19
 
20
  # Agate WebGPU
21
 
22
+ LogoLabs' Agate text-to-image models (a ~0.19B flow generator with a 68M text encoder) running as a static page.
23
+ The tokenizer, text encoder, 50-step flow sampler with classifier-free guidance and the TAESD decoder all run
24
+ on your own GPU through WebGPU (onnxruntime-web).
25
 
26
  **Runs entirely in your browser — prompts never leave your machine.** There is no server and no API call.
27
 
28
+ ## Models
29
+
30
+ Pick a model at the top of the page. A first visit gets Preview 003; the page remembers your last choice in
31
+ this browser.
32
+
33
+ | model | resolution | notes | download |
34
+ |---|---|---|---|
35
+ | [Preview 003](https://huggingface.co/Logolabs/agate-preview-003) | 512 px (or 256) | multi-resolution model; prompt pipeline: normaliser, quoted text spelled out, object counts coded, "no X" moved to the negative prompt; SD3 timestep shift 2 at 512 | 531 MB (both resolutions share one weights file) |
36
+ | [Preview 002](https://huggingface.co/Logolabs/agate-preview-002) | 256 px | same network as 001, trained longer | 530 MB |
37
+ | [Preview 001](https://huggingface.co/Logolabs/agate-preview-001) | 256 px | the first preview | 530 MB |
38
+
39
+ Each model is downloaded once and kept in the browser's Cache Storage. The files live in each release's model repo
40
+ under `webgpu/` (e.g. [Logolabs/agate-preview-003/webgpu](https://huggingface.co/Logolabs/agate-preview-003/tree/main/webgpu)),
41
+ with a `manifest.json` holding sizes and sha256s (the cache keys). `models/` in this Space is the original 001 build
42
+ (without the plan output), kept for `?models=./models/`.
43
+
44
+ ## Show thinker
45
+
46
+ Agate's thinker lays the picture out on a 16 × 16 grid (the plan) that steers the renderer. With **Show thinker**
47
+ (on by default; it costs about 2-4% per step) the page shows, at every step, the plan of the conditional branch as
48
+ colours and the image the model currently expects (x₁ = z + (1 − t)·v, with the SD latent→RGB approximation) —
49
+ the same live preview as the [ComfyUI nodes](https://github.com/logolabs/agate-comfyui): the plan's cells are
50
+ projected on its top three principal components, fitted at the first step with the 2% / 98% quantiles fixed then,
51
+ so a colour keeps its meaning while the plan evolves.
52
+
53
  ## Requirements
54
 
55
+ A browser with WebGPU: a recent Chrome or Edge (113+). Safari and Firefox may lack WebGPU or need it enabled in
56
+ their settings; without it the page offers a WebAssembly (CPU) path that works but takes minutes per image.
57
+
58
+ ## Speed and accuracy (RTX 4060 laptop, Edge, WebGPU, warm)
59
+
60
+ | model | per step (CFG batch of 2) | 50-step image | model setup per visit |
61
+ |---|---|---|---|
62
+ | 003 at 512 px | ~275 ms | ~14 s | ~15 s |
63
+ | 003 at 256 px | ~120 ms | ~6 s | ~15 s |
64
+ | 002 / 001 | ~105 ms | ~5.3 s | ~14 s |
65
+
66
+ With identical initial noise, the browser output matches the PyTorch packages (TAESD decoder): 001 within
67
+ 5/255 per pixel, 002 within 14/255 (PSNR 52.8 dB), 003 within 29/255 at 512 px (PSNR 54.8 dB) and 47/255 at
68
+ 256 px (PSNR 48.9 dB), on a few pixels only (mean difference 0.1-0.2/255). The difference comes from storing the
69
+ weights as fp16 to halve the download; an fp32 build matches to 1/255. Run the self-test with `?parity=1`.
70
+ Seeds use a JavaScript PRNG, so a seed gives the same image in every browser, but not the same image as the
71
+ PyTorch pipeline with that seed. The Python packages decode with the full SD-VAE by default; this page uses TAESD.
72
+
73
+ ## AI-generated content marking
74
+
75
+ Every image the page makes is marked as AI-generated (EU AI Act Art. 50(2)), with the same marks as the Python
76
+ packages:
77
 
78
+ - an invisible watermark in the pixels: [invisible-watermark](https://github.com/ShieldMnt/invisible-watermark)'s
79
+ `dwtDctSvd` method, ported to JavaScript bit for bit, with the 64-bit payload `AGATE` + release
80
+ (`AGATE003`, ...). `agate.detect_watermark(img)` from the model packages reads it;
81
+ - in the saved PNG: text fields `ai_generated`, `generator`, `model` and `watermark`. The prompt is not written;
82
+ - a visible "AI-generated" note next to each result.
83
 
84
+ Neither mark is tamper-proof: screenshots and re-encodes drop the metadata, and heavy edits can remove the
85
+ watermark. If you publish images made here, label them as AI-generated.
 
 
 
 
86
 
87
+ Query parameters: `?v=001|002|003` picks a model, `?res=256` picks 003's resolution, `?autoload=1` loads the
88
+ model immediately, `?ep=wasm` forces the CPU backend, `?parity=1` runs the parity self-test, `?golden=1` checks
89
+ 003's prompt pipeline against the Python package's output (38 prompts).
90
 
91
  ## Links
92
 
93
  - [logolabs.org](https://logolabs.org)
94
  - [huggingface.co/Logolabs](https://huggingface.co/Logolabs)
 
95
 
96
  Licence: MIT.
97
 
css/style.css CHANGED
@@ -95,3 +95,38 @@ input[type=range] { width: 100%; margin-top: 16px; accent-color: var(--red); }
95
  .mast-meta .label.dim { display: none; }
96
  .progress-text { flex-wrap: wrap; }
97
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  .mast-meta .label.dim { display: none; }
96
  .progress-text { flex-wrap: wrap; }
97
  }
98
+
99
+ /* version selector, resolution, AI-generated note (2026-09-29) */
100
+ .versions { margin-top: 24px; }
101
+ .seg { display: flex; flex-wrap: wrap; gap: 0; margin-top: 8px; }
102
+ .seg button { font: 600 13px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; background: var(--paper); color: var(--ink);
103
+ border: var(--hair); margin-left: -1.5px; padding: 11px 16px; cursor: pointer; border-radius: 0; display: inline-flex; flex-direction: column; align-items: flex-start; gap: 5px; }
104
+ .seg button:first-child { margin-left: 0; }
105
+ .seg button small { font: 400 11px/1 var(--mono); letter-spacing: .06em; text-transform: none; color: var(--mute); }
106
+ .seg button[aria-checked="true"] { background: var(--ink); color: var(--paper); }
107
+ .seg button[aria-checked="true"] small { color: #c9c6bf; }
108
+ .seg button:hover:not([aria-checked="true"]):not(:disabled) { background: #fff; color: var(--red); }
109
+ .seg button:disabled { opacity: .45; cursor: default; }
110
+ .seg.small button { padding: 9px 12px; font-size: 12px; }
111
+ .ver-note { margin: 10px 0 0; max-width: 70ch; color: #2a2824; font-size: 15px; }
112
+ input[type=text] { width: 100%; font: 15px/1.45 var(--sans); color: var(--ink); background: #fff; border: var(--hair); border-radius: 0; padding: 8px 12px; margin: 8px 0 18px; }
113
+ .label .dim { color: var(--mute); text-transform: none; letter-spacing: .02em; }
114
+ .res-row { grid-template-columns: 1fr; }
115
+ .prepared { font: 12px/1.5 var(--mono); color: var(--mute); margin: 12px 0 0; overflow-wrap: anywhere; }
116
+ .ai-note { display: flex; gap: 10px; align-items: baseline; margin: 10px 0 0; font-size: 14px; line-height: 1.45; border-left: 3px solid var(--red); padding: 6px 0 6px 10px; background: #fff; }
117
+ .ai-note .sq { position: relative; top: 1px; }
118
+ .small-print { font-size: 13.5px; color: #2a2824; max-width: 90ch; }
119
+ .small-print code { font: 12.5px var(--mono); }
120
+ @media (max-width: 520px) {
121
+ #versions { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
122
+ #versions button { padding: 10px 8px; font-size: 12px; letter-spacing: .06em; }
123
+ }
124
+
125
+ /* thinker preview (plan + expected image), 2026-09-29 */
126
+ .toggles { display: inline-flex; gap: 14px; flex-wrap: wrap; justify-content: flex-end; }
127
+ .thinker { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px; }
128
+ .thinker figure { margin: 0; }
129
+ .thinker canvas { width: 100%; aspect-ratio: 1 / 1; display: block; border: var(--hair); image-rendering: pixelated; background: #fff; }
130
+ .thinker #pred-canvas { image-rendering: auto; }
131
+ .thinker figcaption { margin-top: 6px; }
132
+ .thinker-note { grid-column: 1 / -1; margin: 0; font-size: 13px; color: var(--mute); }
index.html CHANGED
@@ -3,8 +3,8 @@
3
  <head>
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Agate Preview 001 — in your browser</title>
7
- <meta name="description" content="Agate Preview 001, LogoLabs' 0.26B text-to-image model, running entirely in your browser on WebGPU.">
8
  <link rel="icon" href="assets/agate-a.svg" type="image/svg+xml">
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -17,8 +17,8 @@
17
  <div class="mast-left">
18
  <img class="lockup" src="assets/agate-lockup.svg" alt="Agate">
19
  <div class="mast-meta">
20
- <span class="label"><i class="sq"></i>Preview 001</span>
21
- <span class="label dim">0.26B · text-to-image · 256 px</span>
22
  </div>
23
  </div>
24
  <a class="imprint" href="https://logolabs.org" target="_blank" rel="noopener">
@@ -30,8 +30,14 @@
30
 
31
  <section class="intro">
32
  <h1>Agate, in your browser.</h1>
33
- <p>A 191M-parameter flow model with a 68M text encoder, running on your own GPU through WebGPU.
34
- The first visit downloads the weights once (<span id="dl-size">530 MB</span>); after that they load from the browser cache.</p>
 
 
 
 
 
 
35
  </section>
36
 
37
  <div id="nogpu" class="notice" hidden>
@@ -39,7 +45,7 @@
39
  <p id="nogpu-why"></p>
40
  <p>Agate needs WebGPU for usable speed: use a recent Chrome or Edge (113+) on Windows, macOS or ChromeOS,
41
  or Chrome on Android 121+; on Linux and in Firefox/Safari WebGPU may have to be enabled in the browser's flags.
42
- You can still run it on the CPU (WebAssembly) — correct, but several minutes per image.</p>
43
  <button id="use-wasm" class="btn ghost">Run on CPU instead</button>
44
  </div>
45
 
@@ -50,6 +56,8 @@
50
  <div class="examples" id="examples">
51
  <span class="label dim">Try</span>
52
  </div>
 
 
53
 
54
  <div class="row">
55
  <div class="field">
@@ -68,6 +76,12 @@
68
  <input id="cfg" type="range" min="1" max="8" step="0.5" value="3">
69
  </div>
70
  </div>
 
 
 
 
 
 
71
 
72
  <button id="go" class="btn primary" disabled>Load model</button>
73
 
@@ -75,19 +89,34 @@
75
  <div class="bar"><div id="bar-fill"></div></div>
76
  <div class="progress-text"><span id="status" class="label">Idle</span><span id="status-r" class="label dim"></span></div>
77
  </div>
 
78
  </section>
79
 
80
  <section class="panel output">
81
  <div class="out-head">
82
- <span class="label"><i class="sq"></i>Output · 256 × 256</span>
83
- <label class="label toggle"><input type="checkbox" id="crisp"> Crisp pixels</label>
 
 
 
84
  </div>
85
  <div class="frame" id="frame">
86
- <canvas id="canvas" width="256" height="256"></canvas>
87
  <div class="placeholder" id="placeholder">
88
  <img src="assets/agate-a.svg" alt="">
89
  </div>
90
  </div>
 
 
 
 
 
 
 
 
 
 
 
91
  <div class="out-foot">
92
  <dl class="timings" id="timings"></dl>
93
  <a id="save" class="btn ghost small" download="agate.png" hidden>Save PNG</a>
@@ -100,6 +129,9 @@
100
  <hr class="rule">
101
  <footer class="foot">
102
  <p class="strong">Runs entirely in your browser on WebGPU — nothing leaves your machine.</p>
 
 
 
103
  <p class="label dim">
104
  <a href="https://logolabs.org" target="_blank" rel="noopener">logolabs.org</a> ·
105
  <a href="https://huggingface.co/Logolabs" target="_blank" rel="noopener">huggingface.co/Logolabs</a> ·
 
3
  <head>
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Agate — in your browser</title>
7
+ <meta name="description" content="Agate Preview 001, 002 and 003, LogoLabs' 0.26B text-to-image models, running entirely in your browser on WebGPU.">
8
  <link rel="icon" href="assets/agate-a.svg" type="image/svg+xml">
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 
17
  <div class="mast-left">
18
  <img class="lockup" src="assets/agate-lockup.svg" alt="Agate">
19
  <div class="mast-meta">
20
+ <span class="label"><i class="sq"></i><span id="mast-version">Preview 003</span></span>
21
+ <span class="label dim" id="mast-spec">0.26B · text-to-image · 512 px</span>
22
  </div>
23
  </div>
24
  <a class="imprint" href="https://logolabs.org" target="_blank" rel="noopener">
 
30
 
31
  <section class="intro">
32
  <h1>Agate, in your browser.</h1>
33
+ <p>A 0.19B-parameter flow model with a 68M text encoder, running on your own GPU through WebGPU.
34
+ The first visit downloads the chosen model once (<span id="dl-size">about 530 MB</span>); after that it loads from the browser cache.</p>
35
+ </section>
36
+
37
+ <section class="versions" aria-labelledby="ver-label">
38
+ <span class="label" id="ver-label">Model</span>
39
+ <div class="seg" role="radiogroup" aria-labelledby="ver-label" id="versions"></div>
40
+ <p class="ver-note" id="ver-note"></p>
41
  </section>
42
 
43
  <div id="nogpu" class="notice" hidden>
 
45
  <p id="nogpu-why"></p>
46
  <p>Agate needs WebGPU for usable speed: use a recent Chrome or Edge (113+) on Windows, macOS or ChromeOS,
47
  or Chrome on Android 121+; on Linux and in Firefox/Safari WebGPU may have to be enabled in the browser's flags.
48
+ You can still run it on the CPU (WebAssembly) — correct, but several minutes per image (far longer at 512 px).</p>
49
  <button id="use-wasm" class="btn ghost">Run on CPU instead</button>
50
  </div>
51
 
 
56
  <div class="examples" id="examples">
57
  <span class="label dim">Try</span>
58
  </div>
59
+ <label class="label" for="negative">Avoid <span class="dim" id="neg-hint">(optional negative prompt)</span></label>
60
+ <input id="negative" type="text" spellcheck="false" placeholder="e.g. text, blur">
61
 
62
  <div class="row">
63
  <div class="field">
 
76
  <input id="cfg" type="range" min="1" max="8" step="0.5" value="3">
77
  </div>
78
  </div>
79
+ <div class="row res-row" id="res-row" hidden>
80
+ <div class="field">
81
+ <span class="label">Resolution</span>
82
+ <div class="seg small" role="radiogroup" id="res"></div>
83
+ </div>
84
+ </div>
85
 
86
  <button id="go" class="btn primary" disabled>Load model</button>
87
 
 
89
  <div class="bar"><div id="bar-fill"></div></div>
90
  <div class="progress-text"><span id="status" class="label">Idle</span><span id="status-r" class="label dim"></span></div>
91
  </div>
92
+ <p class="prepared" id="prepared" hidden></p>
93
  </section>
94
 
95
  <section class="panel output">
96
  <div class="out-head">
97
+ <span class="label"><i class="sq"></i><span id="out-label">Output · 512 × 512</span></span>
98
+ <span class="toggles">
99
+ <label class="label toggle" title="Show the thinker's 16 x 16 plan and the expected final image at every step"><input type="checkbox" id="show-thinker" checked> Show thinker</label>
100
+ <label class="label toggle"><input type="checkbox" id="crisp"> Crisp pixels</label>
101
+ </span>
102
  </div>
103
  <div class="frame" id="frame">
104
+ <canvas id="canvas" width="512" height="512"></canvas>
105
  <div class="placeholder" id="placeholder">
106
  <img src="assets/agate-a.svg" alt="">
107
  </div>
108
  </div>
109
+ <div class="thinker" id="thinker" hidden>
110
+ <figure><canvas id="plan-canvas" width="16" height="16"></canvas>
111
+ <figcaption class="label dim">Thinker plan · 16 × 16</figcaption></figure>
112
+ <figure><canvas id="pred-canvas" width="32" height="32"></canvas>
113
+ <figcaption class="label dim">Expected · <span id="thinker-step">0 / 0</span></figcaption></figure>
114
+ <p class="thinker-note">The thinker lays the picture out on a 16 × 16 grid before the renderer paints it.
115
+ Colours are the plan's three main directions (fixed at the first step, as in the ComfyUI nodes); the
116
+ expected image is x₁ = z + (1 − t)·v, shown with the SD latent→RGB approximation.</p>
117
+ </div>
118
+ <p class="ai-note" id="ai-note" hidden><i class="sq red"></i><span><b>AI-generated image.</b>
119
+ Made by <span id="ai-model">Agate Preview 003</span>; it carries an invisible watermark and, when saved, PNG provenance metadata (no prompt).</span></p>
120
  <div class="out-foot">
121
  <dl class="timings" id="timings"></dl>
122
  <a id="save" class="btn ghost small" download="agate.png" hidden>Save PNG</a>
 
129
  <hr class="rule">
130
  <footer class="foot">
131
  <p class="strong">Runs entirely in your browser on WebGPU — nothing leaves your machine.</p>
132
+ <p class="small-print">Every image made here is marked as AI-generated (EU AI Act Art. 50): an invisible watermark in the
133
+ pixels (invisible-watermark “dwtDctSvd”, payload AGATE + release) and, in the saved PNG, text fields
134
+ <code>ai_generated</code>, <code>generator</code>, <code>model</code>. Neither survives every edit; please label AI images you publish.</p>
135
  <p class="label dim">
136
  <a href="https://logolabs.org" target="_blank" rel="noopener">logolabs.org</a> ·
137
  <a href="https://huggingface.co/Logolabs" target="_blank" rel="noopener">huggingface.co/Logolabs</a> ·
js/agate.js CHANGED
@@ -1,16 +1,28 @@
1
- // Agate Preview 001 in the browser: tokenizer (tokenizers.js) + three ONNX graphs run by
2
- // onnxruntime-web (WebGPU, WASM fallback) + a 50-step Euler flow sampler with CFG.
3
- // Mirrors agate/pipeline.py (AgatePipeline) of Logolabs/agate-preview-001.
 
4
 
5
  import * as ort from "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.30.0/dist/ort.webgpu.bundle.min.mjs";
6
  import { Tokenizer } from "https://cdn.jsdelivr.net/npm/@huggingface/tokenizers@0.2.0/dist/tokenizers.mjs";
 
 
7
 
8
  export { ort };
9
 
10
- const BUCKETS = [64, 128, 256, 512];
11
- const CACHE_NAME = "agate-preview-001-web";
12
- const COUNT_URL = "https://huggingface.co/Logolabs/agate-preview-001/resolve/main/config.json";
13
- const MODEL_FILES = ["tokenizer.json", "tokenizer_config.json", "text_encoder.onnx", "generator.onnx", "taesd_decoder.onnx"];
 
 
 
 
 
 
 
 
 
14
 
15
  export async function webgpuStatus() {
16
  if (!("gpu" in navigator)) return { ok: false, why: "This browser does not expose WebGPU (navigator.gpu is missing)." };
@@ -26,12 +38,12 @@ export async function webgpuStatus() {
26
  }
27
 
28
  // ---- downloads with progress + Cache Storage ---------------------------------------------------
29
- async function openCache() {
30
- try { return await caches.open(CACHE_NAME); } catch { return null; } // file://, private mode, ...
31
  }
32
 
33
- async function fetchCached(url, key, expectBytes, onBytes) {
34
- const cache = await openCache();
35
  if (cache) {
36
  const hit = await cache.match(key);
37
  if (hit) { const buf = new Uint8Array(await hit.arrayBuffer()); onBytes(buf.byteLength, true); return { buf, cached: true }; }
@@ -56,49 +68,70 @@ async function fetchCached(url, key, expectBytes, onBytes) {
56
  return { buf, cached: false };
57
  }
58
 
59
- export async function hasCachedModel() {
60
- try { const c = await caches.open(CACHE_NAME); return (await c.keys()).length >= MODEL_FILES.length; } catch { return false; }
61
  }
62
 
63
- export async function clearCache() { try { return await caches.delete(CACHE_NAME); } catch { return false; } }
 
 
 
 
64
 
65
  // The Hub counts a model download only on a request for the model repo's root config.json; the ONNX files
66
- // come from this Space, so every model load reads it once -- the same per-load count a Python
67
- // from_pretrained() produces, cached weights or not.
68
- function countLoad() { fetch(COUNT_URL, { cache: "no-store" }).catch(() => {}); }
69
 
70
  // ---- the model ---------------------------------------------------------------------------------
71
  export class Agate {
72
- constructor({ base = "./models/", ep = "webgpu", variant = null, optLevel = "all" } = {}) {
 
73
  this.optLevel = optLevel;
74
- this.base = base.endsWith("/") ? base : base + "/";
 
 
 
75
  this.ep = ep;
76
- this.variant = variant; // e.g. "full16": fp16-compute generator (experimental)
 
 
 
 
 
 
 
 
 
 
77
  }
78
 
79
- // onProgress({loaded, total, file, phase})
80
- async load(onProgress = () => {}) {
81
  const manifest = await (await fetch(this.base + "manifest.json", { cache: "no-cache" })).json();
82
  this.manifest = manifest;
83
  const swap = (this.variant && manifest.variants?.[this.variant]) || {};
84
  const real = (f) => swap[f] || f;
85
- const total = MODEL_FILES.reduce((s, f) => s + manifest.files[real(f)].bytes, 0);
 
 
 
 
86
  let loaded = 0, fromCache = 0;
87
  const bufs = {};
88
  const t0 = performance.now();
89
- for (const f of MODEL_FILES) {
90
  const meta = manifest.files[real(f)];
91
- const key = new Request(`${this.base}${real(f)}?sha256=${meta.sha256}`);
92
- const { buf, cached } = await fetchCached(this.base + real(f), key, meta.bytes, (b, c) => {
93
  loaded += b; if (c) fromCache += b; onProgress({ phase: "download", file: f, loaded, total });
94
  });
95
  bufs[f] = buf;
96
  }
97
  this.stats = { downloadMB: total / 1e6, fromCacheMB: fromCache / 1e6, downloadMs: performance.now() - t0 };
98
- countLoad();
99
- // drop cached files of older model versions (keys carry the sha256)
100
  try {
101
- const cache = await openCache(), keep = new Set(MODEL_FILES.map((f) => new URL(`${this.base}${real(f)}?sha256=${manifest.files[real(f)].sha256}`, location.href).href));
102
  if (cache) for (const req of await cache.keys()) if (!keep.has(req.url)) await cache.delete(req);
103
  } catch (e) { console.warn("cache cleanup", e); }
104
  const dec = new TextDecoder();
@@ -107,20 +140,53 @@ export class Agate {
107
  if (this.ep === "wasm") {
108
  ort.env.wasm.numThreads = self.crossOriginIsolated ? Math.min(8, navigator.hardwareConcurrency || 4) : 1;
109
  }
110
- const opts = { executionProviders: [this.ep], graphOptimizationLevel: this.optLevel };
111
  const t1 = performance.now();
112
  this.sessions = {};
113
- for (const [name, f] of [["text", "text_encoder.onnx"], ["gen", "generator.onnx"], ["vae", "taesd_decoder.onnx"]]) {
114
  onProgress({ phase: "init", file: f, loaded: total, total });
115
  const ts = performance.now();
116
- this.sessions[name] = await ort.InferenceSession.create(bufs[f], opts);
117
  this.stats[`${name}SessionMs`] = performance.now() - ts;
118
  bufs[f] = null;
119
  }
 
 
 
 
 
 
 
 
 
120
  this.stats.sessionMs = performance.now() - t1;
121
  return this.stats;
122
  }
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  tokenize(text) {
125
  const max = this.manifest.text_max_len;
126
  let ids = this.tok.encode(text).ids; // [CLS] ... [SEP], as the HF tokenizer
@@ -139,7 +205,15 @@ export class Agate {
139
  const h = out.last_hidden_state;
140
  const data = h.data.slice(); // (1, n, 512) fp32
141
  h.dispose?.();
142
- return { data, n };
 
 
 
 
 
 
 
 
143
  }
144
 
145
  static gaussianNoise(seed, count) {
@@ -156,38 +230,67 @@ export class Agate {
156
  return out;
157
  }
158
 
159
- // -> { rgba: Uint8ClampedArray (256*256*4), latent: Float32Array, timings }
160
- async generate({ prompt, negative = "", seed = 0, steps = 50, cfg = 3.0, noise = null, onStep = () => {}, shouldStop = () => false }) {
161
- const hw = this.manifest.latent_hw, C = this.manifest.latent_ch, D = this.manifest.ctx_dim;
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  const per = C * hw * hw;
163
  const T = { };
164
  let t0 = performance.now();
165
- const c = await this.encode(prompt), u = await this.encode(negative);
 
166
  T.textMs = performance.now() - t0;
 
167
  const need = Math.max(c.n, u.n);
168
  const L = BUCKETS.find((b) => b >= need) ?? BUCKETS[BUCKETS.length - 1];
169
  const ctx = new Float32Array(2 * L * D), mask = new Float32Array(2 * L);
170
  ctx.set(c.data, 0); ctx.set(u.data, L * D); // zero padding, as F.pad in the pipeline
171
  mask.fill(1, 0, c.n); mask.fill(1, L, L + u.n);
172
- const ctxT = new ort.Tensor("float32", ctx, [2, L, D]), maskT = new ort.Tensor("float32", mask, [2, L]);
 
 
 
 
 
 
 
173
 
174
  let z = noise ? Float32Array.from(noise) : Agate.gaussianNoise(seed, per);
175
  const zz = new Float32Array(2 * per), tt = new Float32Array(2);
176
- const dt = 1 / steps;
177
  t0 = performance.now();
178
  const stepMs = [];
179
  for (let i = 0; i < steps; i++) {
180
  if (shouldStop()) throw new Error("stopped");
181
  const ts = performance.now();
182
  zz.set(z, 0); zz.set(z, per);
183
- tt[0] = tt[1] = i * dt;
184
- const out = await this.sessions.gen.run({
185
- z: new ort.Tensor("float32", zz, [2, C, hw, hw]), t: new ort.Tensor("float32", tt, [2]), ctx: ctxT, mask: maskT,
186
- });
187
  const v = out.v.data;
 
188
  const zn = new Float32Array(per);
189
- for (let k = 0; k < per; k++) { const vc = v[k], vu = v[per + k]; zn[k] = z[k] + dt * (vu + cfg * (vc - vu)); }
 
 
 
 
 
 
190
  out.v.dispose?.();
 
191
  z = zn;
192
  stepMs.push(performance.now() - ts);
193
  onStep(i + 1, steps, z);
@@ -209,8 +312,11 @@ export class Agate {
209
  rgba[p * 4 + 3] = 255;
210
  }
211
  T.decodeMs = performance.now() - t0;
212
- T.totalMs = T.textMs + T.samplerMs + T.decodeMs;
 
 
 
213
  T.bucket = L;
214
- return { rgba, width: W, height: H, latent: z, timings: T };
215
  }
216
  }
 
1
+ // Agate in the browser: tokenizer (tokenizers.js) + ONNX graphs run by onnxruntime-web (WebGPU, WASM
2
+ // fallback) + an Euler flow sampler with CFG. Mirrors agate/pipeline.py (AgatePipeline) of
3
+ // Logolabs/agate-preview-001 / -002 (fcdm_t2, 256 px) and Logolabs/agate-preview-003 (fcdm_t2mr: 512 or 256 px,
4
+ // SD3 timestep shift, prompt pipeline + count code, see prompt.js). Every image is marked (marking.js).
5
 
6
  import * as ort from "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.30.0/dist/ort.webgpu.bundle.min.mjs";
7
  import { Tokenizer } from "https://cdn.jsdelivr.net/npm/@huggingface/tokenizers@0.2.0/dist/tokenizers.mjs";
8
+ import { prepare, countVector } from "./prompt.js";
9
+ import { embedWatermark, marks } from "./marking.js";
10
 
11
  export { ort };
12
 
13
+ // The releases this page runs. remote: where the files live -- each release's model repo, under webgpu/ (the
14
+ // Space repo has a 1 GB storage limit; the graphs there also output the thinker plan). The Space's old models/
15
+ // (001 without the plan output) is used only through ?models=. dir: the layout under a local
16
+ // models base (?models=<url> or window.AGATE_MODEL_BASE given explicitly, e.g. for local testing). cache: Cache
17
+ // Storage name (001 keeps the name it always had, so returning visitors do not download it again).
18
+ const HUB = (v) => `https://huggingface.co/Logolabs/agate-preview-${v}/resolve/main/webgpu/`;
19
+ export const VERSIONS = {
20
+ "003": { remote: HUB("003"), dir: "003/", cache: "agate-preview-003-web", res: [512, 256], note: "Multi-resolution model, 512 px (or 256). Quoted text is spelled out, counts are coded, “no X” becomes a negative prompt." },
21
+ "002": { remote: HUB("002"), dir: "002/", cache: "agate-preview-002-web", res: [256], note: "Same network as 001, trained 38,710 steps longer before the same anneal. 256 px, plain prompts." },
22
+ "001": { remote: HUB("001"), dir: "", cache: "agate-preview-001-web", res: [256], note: "The first preview (2026-09-25). 256 px, plain prompts." },
23
+ };
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)." };
 
38
  }
39
 
40
  // ---- downloads with progress + Cache Storage ---------------------------------------------------
41
+ async function openCache(name) {
42
+ try { return await caches.open(name); } catch { return null; } // file://, private mode, ...
43
  }
44
 
45
+ async function fetchCached(cacheName, url, key, expectBytes, onBytes) {
46
+ const cache = await openCache(cacheName);
47
  if (cache) {
48
  const hit = await cache.match(key);
49
  if (hit) { const buf = new Uint8Array(await hit.arrayBuffer()); onBytes(buf.byteLength, true); return { buf, cached: true }; }
 
68
  return { buf, cached: false };
69
  }
70
 
71
+ export async function hasCachedModel(version = "001") {
72
+ try { const c = await caches.open(VERSIONS[version].cache); return (await c.keys()).length >= 5; } catch { return false; }
73
  }
74
 
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
 
81
  // The Hub counts a model download only on a request for the model repo's root config.json; the ONNX files
82
+ // come from this Space, so every model load reads the loaded release's config.json once -- the same per-load
83
+ // count a Python from_pretrained() produces, cached weights or not.
84
+ function countLoad(version) { fetch(countUrl(version), { cache: "no-store" }).catch(() => {}); }
85
 
86
  // ---- the model ---------------------------------------------------------------------------------
87
  export class Agate {
88
+ // base: the page's models folder (001); override: an explicit models base for every version (local layout)
89
+ constructor({ base = "./models/", override = null, version = "003", ep = "webgpu", variant = null, optLevel = "all" } = {}) {
90
  this.optLevel = optLevel;
91
+ const slash = (u) => (u.endsWith("/") ? u : u + "/");
92
+ this.version = version;
93
+ this.spec = VERSIONS[version];
94
+ this.base = override ? slash(override) + this.spec.dir : (this.spec.remote || slash(base) + this.spec.dir);
95
  this.ep = ep;
96
+ this.variant = variant; // e.g. "full16": fp16-compute generator (experimental, 001 only)
97
+ this.marks = marks(version);
98
+ }
99
+
100
+ get mr() { return this.manifest?.arch === "fcdm_t2mr"; }
101
+
102
+ fileKey(f) { return new Request(`${this.base}${f}?sha256=${this.manifest.files[f].sha256}`); }
103
+
104
+ async getFile(f, onBytes = () => {}) {
105
+ const meta = this.manifest.files[f];
106
+ return (await fetchCached(this.spec.cache, this.base + f, this.fileKey(f), meta.bytes, onBytes)).buf;
107
  }
108
 
109
+ // onProgress({loaded, total, file, phase}); resolution: 003 only (512 default, or 256)
110
+ async load(onProgress = () => {}, resolution = null) {
111
  const manifest = await (await fetch(this.base + "manifest.json", { cache: "no-cache" })).json();
112
  this.manifest = manifest;
113
  const swap = (this.variant && manifest.variants?.[this.variant]) || {};
114
  const real = (f) => swap[f] || f;
115
+ this.resolution = this.mr ? Number(resolution || manifest.resolution) : 256;
116
+ // 003: download everything (the two graphs are ~1 MB each and share one weights file), so a resolution
117
+ // switch needs no network
118
+ const need = this.mr ? Object.keys(manifest.files) : FILES_T2;
119
+ const total = need.reduce((s, f) => s + manifest.files[real(f)].bytes, 0);
120
  let loaded = 0, fromCache = 0;
121
  const bufs = {};
122
  const t0 = performance.now();
123
+ for (const f of need) {
124
  const meta = manifest.files[real(f)];
125
+ const { buf } = await fetchCached(this.spec.cache, this.base + real(f), this.fileKey(real(f)), meta.bytes, (b, c) => {
 
126
  loaded += b; if (c) fromCache += b; onProgress({ phase: "download", file: f, loaded, total });
127
  });
128
  bufs[f] = buf;
129
  }
130
  this.stats = { downloadMB: total / 1e6, fromCacheMB: fromCache / 1e6, downloadMs: performance.now() - t0 };
131
+ countLoad(this.version);
132
+ // drop cached files of older builds of this release (keys carry the sha256)
133
  try {
134
+ const cache = await openCache(this.spec.cache), keep = new Set(need.map((f) => new URL(this.fileKey(real(f)).url, location.href).href));
135
  if (cache) for (const req of await cache.keys()) if (!keep.has(req.url)) await cache.delete(req);
136
  } catch (e) { console.warn("cache cleanup", e); }
137
  const dec = new TextDecoder();
 
140
  if (this.ep === "wasm") {
141
  ort.env.wasm.numThreads = self.crossOriginIsolated ? Math.min(8, navigator.hardwareConcurrency || 4) : 1;
142
  }
 
143
  const t1 = performance.now();
144
  this.sessions = {};
145
+ for (const [name, f] of [["text", "text_encoder.onnx"], ["vae", "taesd_decoder.onnx"]]) {
146
  onProgress({ phase: "init", file: f, loaded: total, total });
147
  const ts = performance.now();
148
+ this.sessions[name] = await ort.InferenceSession.create(bufs[f], this.sessionOptions());
149
  this.stats[`${name}SessionMs`] = performance.now() - ts;
150
  bufs[f] = null;
151
  }
152
+ onProgress({ phase: "init", file: "generator", loaded: total, total });
153
+ const ts = performance.now();
154
+ if (this.mr) {
155
+ this.weights = bufs["generator.weights"]; // kept for a resolution switch (no re-read)
156
+ await this.createGenerator(this.resolution, bufs);
157
+ } else {
158
+ this.sessions.gen = await ort.InferenceSession.create(bufs["generator.onnx"], this.sessionOptions());
159
+ }
160
+ this.stats.genSessionMs = performance.now() - ts;
161
  this.stats.sessionMs = performance.now() - t1;
162
  return this.stats;
163
  }
164
 
165
+ sessionOptions(extra = {}) { return { executionProviders: [this.ep], graphOptimizationLevel: this.optLevel, ...extra }; }
166
+
167
+ // 003: the generator graph for one resolution; the weights file is shared by both graphs.
168
+ async createGenerator(res, bufs = {}) {
169
+ const r = this.manifest.resolutions[String(res)];
170
+ if (!r) throw new Error(`resolution ${res} not available`);
171
+ const graph = bufs[r.graph] || await this.getFile(r.graph);
172
+ const weights = this.weights || await this.getFile(r.external_data);
173
+ if (this.sessions.gen) { try { await this.sessions.gen.release(); } catch { /* */ } this.sessions.gen = null; }
174
+ this.sessions.gen = await ort.InferenceSession.create(graph, this.sessionOptions({ externalData: [{ path: r.external_data, data: weights }] }));
175
+ this.resolution = Number(res);
176
+ }
177
+
178
+ async setResolution(res) {
179
+ if (!this.mr || Number(res) === this.resolution) return 0;
180
+ const t0 = performance.now();
181
+ await this.createGenerator(res);
182
+ return performance.now() - t0;
183
+ }
184
+
185
+ async release() {
186
+ for (const s of Object.values(this.sessions || {})) { try { await s?.release(); } catch { /* */ } }
187
+ this.sessions = null; this.weights = null;
188
+ }
189
+
190
  tokenize(text) {
191
  const max = this.manifest.text_max_len;
192
  let ids = this.tok.encode(text).ids; // [CLS] ... [SEP], as the HF tokenizer
 
205
  const h = out.last_hidden_state;
206
  const data = h.data.slice(); // (1, n, 512) fp32
207
  h.dispose?.();
208
+ return { data, n, ids };
209
+ }
210
+
211
+ // The prompt as the model sees it: 003 runs its prompt pipeline (prompt.js), 001/002 use the prompt as typed.
212
+ prepare(prompt, negative = "") {
213
+ const pp = this.manifest.prompt_pipeline;
214
+ if (!pp) return { text: prompt, negative };
215
+ const [text, neg] = prepare(prompt, negative, pp.normalize, pp.spell);
216
+ return { text, negative: neg };
217
  }
218
 
219
  static gaussianNoise(seed, count) {
 
230
  return out;
231
  }
232
 
233
+ // SD3's resolution shift in Agate's convention (t = 0 noise), as agate/pipeline.py shift_t
234
+ static shiftT(t, shift) {
235
+ if (shift === 1) return t;
236
+ const s = 1 - t;
237
+ return 1 - shift * s / (1 + (shift - 1) * s);
238
+ }
239
+
240
+ // -> { rgba (watermarked unless watermark=false), width, height, latent, timings, prepared }
241
+ get hasPlan() { return !!this.sessions?.gen?.outputNames?.includes("plan"); }
242
+
243
+ // onPreview({ step, steps, plan, x1, hw }) after every step, when given and the graph outputs the plan: plan =
244
+ // the thinker output for the conditional branch (Float32Array 640 x 16 x 16), x1 = z + (1 - t) v (guided).
245
+ async generate({ prompt, negative = "", seed = 0, steps = 50, cfg = 3.0, noise = null, watermark = true, onStep = () => {}, onPreview = null, shouldStop = () => false }) {
246
+ const C = this.manifest.latent_ch, D = this.manifest.ctx_dim;
247
+ const rc = this.mr ? this.manifest.resolutions[String(this.resolution)] : { latent_hw: this.manifest.latent_hw, shift: 1 };
248
+ const hw = rc.latent_hw, shift = Number(rc.shift || 1);
249
  const per = C * hw * hw;
250
  const T = { };
251
  let t0 = performance.now();
252
+ const prep = this.prepare(prompt, negative);
253
+ const c = await this.encode(prep.text), u = await this.encode(prep.negative);
254
  T.textMs = performance.now() - t0;
255
+ const BUCKETS = this.manifest.buckets;
256
  const need = Math.max(c.n, u.n);
257
  const L = BUCKETS.find((b) => b >= need) ?? BUCKETS[BUCKETS.length - 1];
258
  const ctx = new Float32Array(2 * L * D), mask = new Float32Array(2 * L);
259
  ctx.set(c.data, 0); ctx.set(u.data, L * D); // zero padding, as F.pad in the pipeline
260
  mask.fill(1, 0, c.n); mask.fill(1, L, L + u.n);
261
+ const feeds = { ctx: new ort.Tensor("float32", ctx, [2, L, D]), mask: new ort.Tensor("float32", mask, [2, L]) };
262
+ let countsNonzero = 0;
263
+ if (this.mr) { // count code: conditional half only
264
+ const cnt = new Float32Array(2 * L);
265
+ if (this.manifest.prompt_pipeline?.count_code) cnt.set(countVector(this.tok, prep.text, c.ids, Math.min(L, c.n)), 0);
266
+ countsNonzero = cnt.reduce((s, v) => s + (v > 0), 0);
267
+ feeds.counts = new ort.Tensor("float32", cnt, [2, L]);
268
+ }
269
 
270
  let z = noise ? Float32Array.from(noise) : Agate.gaussianNoise(seed, per);
271
  const zz = new Float32Array(2 * per), tt = new Float32Array(2);
272
+ const grid = Array.from({ length: steps + 1 }, (_, i) => Agate.shiftT(i / steps, shift));
273
  t0 = performance.now();
274
  const stepMs = [];
275
  for (let i = 0; i < steps; i++) {
276
  if (shouldStop()) throw new Error("stopped");
277
  const ts = performance.now();
278
  zz.set(z, 0); zz.set(z, per);
279
+ tt[0] = tt[1] = grid[i];
280
+ const want = onPreview && this.hasPlan ? ["v", "plan"] : ["v"];
281
+ 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);
 
282
  const v = out.v.data;
283
+ const dt = grid[i + 1] - grid[i];
284
  const zn = new Float32Array(per);
285
+ let x1 = null;
286
+ if (out.plan) x1 = new Float32Array(per);
287
+ for (let k = 0; k < per; k++) {
288
+ const vc = v[k], vu = v[per + k], g = vu + cfg * (vc - vu);
289
+ zn[k] = z[k] + dt * g;
290
+ if (x1) x1[k] = z[k] + (1 - grid[i]) * g;
291
+ }
292
  out.v.dispose?.();
293
+ if (out.plan) { const plan = out.plan.data.slice(); out.plan.dispose?.(); onPreview({ step: i + 1, steps, plan, x1, hw }); }
294
  z = zn;
295
  stepMs.push(performance.now() - ts);
296
  onStep(i + 1, steps, z);
 
312
  rgba[p * 4 + 3] = 255;
313
  }
314
  T.decodeMs = performance.now() - t0;
315
+ t0 = performance.now();
316
+ if (watermark) embedWatermark(rgba, W, H, this.marks.payload);
317
+ T.markMs = performance.now() - t0;
318
+ T.totalMs = T.textMs + T.samplerMs + T.decodeMs + T.markMs;
319
  T.bucket = L;
320
+ return { rgba, width: W, height: H, latent: z, timings: T, prepared: { ...prep, countsNonzero }, watermarked: watermark };
321
  }
322
  }
js/app.js CHANGED
@@ -1,163 +1,326 @@
1
- import { Agate, webgpuStatus, clearCache, hasCachedModel } from "./agate.js";
2
-
3
- // Model location: ?models=<url> overrides; otherwise window.AGATE_MODEL_BASE (js/config.js); default ./models/
4
- const params = new URLSearchParams(location.search);
5
- const BASE = params.get("models") || window.AGATE_MODEL_BASE || "./models/";
6
- const FORCE_EP = params.get("ep"); // "wasm" forces the CPU path
7
- const PARITY = params.has("parity");
8
- const VARIANT = params.get("variant"); // "full16": experimental fp16-compute generator
9
-
10
- const EXAMPLES = [
11
- "a green teapot and a red cup on a table",
12
- "a minimalist logo of a fox head, orange, flat design, white background",
13
- "a dog sitting to the left of a cat",
14
- "a geometric mountain logo in blue and white",
15
- "a red cube on top of a blue sphere",
16
- ];
17
-
18
- const $ = (id) => document.getElementById(id);
19
- const ui = {
20
- prompt: $("prompt"), seed: $("seed"), steps: $("steps"), cfg: $("cfg"), go: $("go"), dice: $("dice"),
21
- fill: $("bar-fill"), status: $("status"), statusR: $("status-r"), canvas: $("canvas"), frame: $("frame"),
22
- placeholder: $("placeholder"), timings: $("timings"), save: $("save"), crisp: $("crisp"), backend: $("backend"),
23
- };
24
-
25
- let agate = null, ep = FORCE_EP || "webgpu", busy = false, stop = false;
26
- window.__agate = { state: "idle" }; // read by the automated browser test
27
-
28
- for (const p of EXAMPLES) {
29
- const b = document.createElement("button");
30
- b.className = "chip"; b.textContent = p; b.type = "button";
31
- b.onclick = () => { ui.prompt.value = p; };
32
- $("examples").appendChild(b);
33
- }
34
- ui.steps.oninput = () => { $("steps-v").textContent = ui.steps.value; };
35
- ui.cfg.oninput = () => { $("cfg-v").textContent = Number(ui.cfg.value).toFixed(1); };
36
- ui.dice.onclick = () => { ui.seed.value = Math.floor(Math.random() * 2 ** 31); };
37
- try { ui.crisp.checked = localStorage.getItem("agate-crisp") === "1"; } catch { /* storage blocked */ }
38
- const applyCrisp = () => { ui.frame.classList.toggle("crisp", ui.crisp.checked); try { localStorage.setItem("agate-crisp", ui.crisp.checked ? "1" : "0"); } catch { /* */ } };
39
- ui.crisp.onchange = applyCrisp; applyCrisp();
40
-
41
- const mb = (b) => (b / 1e6).toFixed(0);
42
- function setProgress(frac, left, right = "") {
43
- ui.fill.style.width = `${Math.max(0, Math.min(1, frac)) * 100}%`;
44
- ui.status.textContent = left; ui.statusR.textContent = right;
45
- }
46
- function showTimings(rows) {
47
- ui.timings.innerHTML = rows.map(([k, v]) => `<dt>${k}</dt><dd>${v}</dd>`).join("");
48
- }
49
-
50
- async function load() {
51
- busy = true; ui.go.disabled = true;
52
- window.__agate.state = "loading";
53
- agate = new Agate({ base: BASE, ep, variant: VARIANT, optLevel: params.get("opt") || "all" });
54
- try {
55
- const t0 = performance.now();
56
- const st = await agate.load(({ phase, file, loaded, total }) => {
57
- if (phase === "download") setProgress(loaded / total, `Downloading ${file}`, `${mb(loaded)} / ${mb(total)} MB`);
58
- else setProgress(1, `Preparing ${file} on ${ep === "webgpu" ? "WebGPU" : "CPU"}`, `${mb(total)} MB`);
59
- });
60
- const loadMs = performance.now() - t0;
61
- $("dl-size").textContent = `${st.downloadMB.toFixed(0)} MB`;
62
- ui.backend.textContent = `backend: ${ep === "webgpu" ? "WebGPU" : "WebAssembly (CPU)"}`;
63
- const src = st.fromCacheMB > st.downloadMB * 0.99 ? "from browser cache" : "downloaded";
64
- setProgress(0, "Ready", `${st.downloadMB.toFixed(0)} MB ${src} · ${(loadMs / 1000).toFixed(1)} s`);
65
- window.__agate = { state: "ready", load: { ...st, loadMs, ep } };
66
- ui.go.textContent = "Generate"; ui.go.disabled = false;
67
- } catch (e) {
68
- console.error(e);
69
- setProgress(0, "Load failed", String(e.message || e));
70
- window.__agate = { state: "error", error: String(e.message || e) };
71
- ui.go.textContent = "Retry load"; ui.go.disabled = false; agate = null;
72
- }
73
- busy = false;
74
- }
75
-
76
- async function generate(opts = {}) {
77
- busy = true; stop = false;
78
- ui.go.textContent = "Stop"; ui.frame.classList.add("busy");
79
- window.__agate.state = "generating";
80
- const steps = opts.steps ?? Number(ui.steps.value);
81
- try {
82
- setProgress(0, "Encoding prompt", "");
83
- const r = await agate.generate({
84
- prompt: opts.prompt ?? ui.prompt.value.trim(), seed: Number(ui.seed.value) >>> 0, steps,
85
- cfg: opts.cfg ?? Number(ui.cfg.value), noise: opts.noise ?? null,
86
- onStep: (i, n) => setProgress(i / n, `Step ${i} / ${n}`, ""),
87
- shouldStop: () => stop,
88
- });
89
- const ctx = ui.canvas.getContext("2d");
90
- ui.canvas.width = r.width; ui.canvas.height = r.height;
91
- ctx.putImageData(new ImageData(r.rgba, r.width, r.height), 0, 0);
92
- ui.placeholder.hidden = true;
93
- const T = r.timings;
94
- setProgress(1, "Done", `${(T.totalMs / 1000).toFixed(1)} s`);
95
- showTimings([
96
- ["text encode", `${T.textMs.toFixed(0)} ms (bucket ${T.bucket})`],
97
- ["per step", `${T.stepMs.toFixed(0)} ms × ${steps} (first ${T.firstStepMs.toFixed(0)} ms)`],
98
- ["decode", `${T.decodeMs.toFixed(0)} ms`],
99
- ["total", `${(T.totalMs / 1000).toFixed(2)} s`],
100
- ]);
101
- ui.save.href = ui.canvas.toDataURL("image/png"); ui.save.hidden = false;
102
- window.__agate = { ...window.__agate, state: "done", timings: T };
103
- return r;
104
- } catch (e) {
105
- if (String(e.message) === "stopped") setProgress(0, "Stopped", "");
106
- else { console.error(e); setProgress(0, "Error", String(e.message || e)); window.__agate.error = String(e.message || e); }
107
- window.__agate.state = "done";
108
- } finally {
109
- busy = false; ui.go.textContent = "Generate"; ui.frame.classList.remove("busy");
110
- }
111
- }
112
-
113
- ui.go.onclick = async () => {
114
- if (busy && agate?.sessions) { stop = true; return; }
115
- if (busy) return;
116
- if (!agate) return load();
117
- return generate();
118
- };
119
- ui.prompt.addEventListener("keydown", (e) => { if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && agate && !busy) generate(); });
120
-
121
- // Parity self-test: index.html?parity=1 runs the fixture exported by export/parity_full.py
122
- async function parity() {
123
- const fx = await (await fetch("test/parity.json")).json();
124
- const bin = async (f) => new Uint8Array(await (await fetch(`test/${f}`)).arrayBuffer());
125
- const noise = new Float32Array((await bin("parity_noise.bin")).buffer);
126
- const refZ = new Float32Array((await bin("parity_latent.bin")).buffer);
127
- const refImg = await bin("parity_image.bin"); // HWC uint8
128
- ui.prompt.value = fx.prompt; ui.steps.value = fx.steps; ui.cfg.value = fx.cfg;
129
- const r = await generate({ prompt: fx.prompt, steps: fx.steps, cfg: fx.cfg, noise });
130
- let dz = 0, dzs = 0, di = 0, dis = 0;
131
- for (let k = 0; k < refZ.length; k++) { const d = Math.abs(r.latent[k] - refZ[k]); dz = Math.max(dz, d); dzs += d; }
132
- for (let p = 0; p < refImg.length / 3; p++) for (let c = 0; c < 3; c++) {
133
- const d = Math.abs(r.rgba[p * 4 + c] - refImg[p * 3 + c]); di = Math.max(di, d); dis += d;
134
- }
135
- const res = { latentMaxAbs: dz, latentMeanAbs: dzs / refZ.length, imageMaxAbs: di, imageMeanAbs: dis / refImg.length, ep, timings: r.timings };
136
- const el = $("parity"); el.hidden = false;
137
- el.innerHTML = `<span class="label"><i class="sq red"></i>Parity vs PyTorch pipeline (${ep})</span>
138
- <p>final latent max |Δ| ${dz.toFixed(4)} (mean ${res.latentMeanAbs.toFixed(5)}) · image max |Δ| ${di}/255 (mean ${res.imageMeanAbs.toFixed(3)})</p>`;
139
- window.__agate = { ...window.__agate, parity: res, state: "parity-done" };
140
- }
141
-
142
- (async () => {
143
- if (!FORCE_EP) {
144
- const s = await webgpuStatus();
145
- if (!s.ok) {
146
- ep = "wasm";
147
- $("nogpu").hidden = false; $("nogpu-why").textContent = s.why;
148
- ui.go.disabled = true; ui.go.textContent = "WebGPU required";
149
- window.__agate = { state: "no-webgpu", why: s.why };
150
- $("use-wasm").onclick = () => { $("nogpu").hidden = true; ui.go.textContent = "Load model (CPU)"; ui.go.disabled = false; };
151
- return;
152
- }
153
- ui.backend.textContent = `backend: WebGPU${s.adapter ? " · " + s.adapter : ""}`;
154
- }
155
- ui.go.disabled = false;
156
- ui.go.textContent = "Load model";
157
- if (params.has("autoload") || PARITY || await hasCachedModel()) { // second visit: load straight from cache
158
- await load();
159
- if (PARITY && agate) await parity();
160
- }
161
- })();
162
-
163
- window.agateClearCache = clearCache;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Agate, VERSIONS, webgpuStatus, clearCache, hasCachedModel } from "./agate.js";
2
+ import { addPngText, readWatermark } from "./marking.js";
3
+ import { prepare as preparePrompt, countVector } from "./prompt.js";
4
+ import { ThinkerView } from "./thinker.js";
5
+
6
+ // Model location: 001 from window.AGATE_MODEL_BASE (js/config.js, default ./models/), 002 / 003 from their model
7
+ // repos (agate.js VERSIONS); ?models=<url> serves every version from one local-layout base instead.
8
+ const params = new URLSearchParams(location.search);
9
+ const BASE = window.AGATE_MODEL_BASE || "./models/"; // 001's files (002 / 003 come from their model repos)
10
+ const MODELS_OVERRIDE = params.get("models"); // every version from <url>/{,002/,003/} (local testing)
11
+ const FORCE_EP = params.get("ep"); // "wasm" forces the CPU path
12
+ const PARITY = params.has("parity");
13
+ const GOLDEN = params.has("golden"); // prompt-pipeline golden test (003)
14
+ const VARIANT = params.get("variant"); // "full16": experimental fp16-compute generator (001)
15
+
16
+ const store = {
17
+ get(k) { try { return localStorage.getItem(k); } catch { return null; } },
18
+ set(k, v) { try { localStorage.setItem(k, v); } catch { /* storage blocked */ } },
19
+ };
20
+ const DEFAULT_VERSION = "003";
21
+ let version = [params.get("v"), store.get("agate-version"), DEFAULT_VERSION].find((v) => v && VERSIONS[v]);
22
+ let resolution = Number(params.get("res") || store.get("agate-res-003") || 512);
23
+ if (!VERSIONS["003"].res.includes(resolution)) resolution = 512;
24
+
25
+ const EXAMPLES = {
26
+ common: [
27
+ "a green teapot and a red cup on a table",
28
+ "a minimalist logo of a fox head, orange, flat design, white background",
29
+ "a dog sitting to the left of a cat",
30
+ "a red cube on top of a blue sphere",
31
+ ],
32
+ "003": [
33
+ 'a shop sign that says "OPEN", three red apples, no people',
34
+ "a portrait of an old fisherman at golden hour, detailed",
35
+ 'a coffee cup with the text "Good Morning" on it',
36
+ ],
37
+ };
38
+
39
+ const $ = (id) => document.getElementById(id);
40
+ const ui = {
41
+ prompt: $("prompt"), negative: $("negative"), seed: $("seed"), steps: $("steps"), cfg: $("cfg"), go: $("go"), dice: $("dice"),
42
+ fill: $("bar-fill"), status: $("status"), statusR: $("status-r"), canvas: $("canvas"), frame: $("frame"),
43
+ placeholder: $("placeholder"), timings: $("timings"), save: $("save"), crisp: $("crisp"), backend: $("backend"),
44
+ versions: $("versions"), res: $("res"), resRow: $("res-row"), prepared: $("prepared"), aiNote: $("ai-note"),
45
+ showThinker: $("show-thinker"), thinker: $("thinker"), thinkerStep: $("thinker-step"),
46
+ };
47
+ const thinkerView = new ThinkerView($("plan-canvas"), $("pred-canvas"));
48
+ ui.showThinker.checked = store.get("agate-thinker") !== "0";
49
+ ui.showThinker.onchange = () => { store.set("agate-thinker", ui.showThinker.checked ? "1" : "0"); if (!ui.showThinker.checked) ui.thinker.hidden = true; };
50
+
51
+ let agate = null, ep = FORCE_EP || "webgpu", busy = false, stop = false, gpuOk = true, last = null;
52
+ window.__agate = { state: "idle", version }; // read by the automated browser test
53
+
54
+ const res = () => (version === "003" ? resolution : 256);
55
+
56
+ function renderExamples() {
57
+ const box = $("examples");
58
+ box.querySelectorAll(".chip").forEach((c) => c.remove());
59
+ for (const p of [...(EXAMPLES[version] || []), ...EXAMPLES.common].slice(0, 6)) {
60
+ const b = document.createElement("button");
61
+ b.className = "chip"; b.textContent = p; b.type = "button";
62
+ b.onclick = () => { ui.prompt.value = p; };
63
+ box.appendChild(b);
64
+ }
65
+ }
66
+
67
+ function seg(el, items, current, onPick) {
68
+ el.innerHTML = "";
69
+ for (const it of items) {
70
+ const b = document.createElement("button");
71
+ b.type = "button"; b.setAttribute("role", "radio"); b.setAttribute("aria-checked", String(it.value === current));
72
+ b.dataset.value = it.value;
73
+ b.innerHTML = `${it.label}${it.sub ? `<small>${it.sub}</small>` : ""}`;
74
+ b.onclick = () => { if (!busy && it.value !== current) onPick(it.value); };
75
+ el.appendChild(b);
76
+ }
77
+ }
78
+
79
+ function renderVersion() {
80
+ const spec = VERSIONS[version];
81
+ seg(ui.versions, Object.keys(VERSIONS).map((v) => ({ value: v, label: `Preview ${v}`, sub: `${Math.max(...VERSIONS[v].res)} px${v === DEFAULT_VERSION ? " · newest" : ""}` })),
82
+ version, pickVersion);
83
+ $("ver-note").textContent = spec.note;
84
+ $("mast-version").textContent = `Preview ${version}`;
85
+ $("mast-spec").textContent = `0.26B · text-to-image · ${spec.res.join(" / ")} px`;
86
+ $("ai-model").textContent = `Agate Preview ${version}`;
87
+ $("neg-hint").textContent = version === "003" ? "(optional; “no X” in the prompt is added automatically)" : "(optional negative prompt)";
88
+ ui.resRow.hidden = spec.res.length < 2;
89
+ if (spec.res.length > 1) seg(ui.res, spec.res.map((r) => ({ value: r, label: `${r} × ${r}`, sub: r === 512 ? "native" : "faster" })), resolution, pickRes);
90
+ $("out-label").textContent = `Output · ${res()} × ${res()}`;
91
+ renderExamples();
92
+ document.title = `Agate Preview ${version} — in your browser`;
93
+ }
94
+
95
+ async function pickVersion(v) {
96
+ version = v; store.set("agate-version", v);
97
+ window.__agate.version = v;
98
+ if (agate) { await agate.release(); agate = null; }
99
+ clearOutput();
100
+ renderVersion();
101
+ if (!gpuOk && !FORCE_EP) return;
102
+ ui.go.textContent = "Load model"; ui.go.disabled = false;
103
+ setProgress(0, "Idle", "");
104
+ if (await hasCachedModel(v)) await load();
105
+ }
106
+
107
+ async function pickRes(r) {
108
+ resolution = r; store.set("agate-res-003", String(r));
109
+ renderVersion();
110
+ if (agate?.sessions && agate.mr) {
111
+ busy = true; ui.go.disabled = true;
112
+ setProgress(1, `Preparing the ${r} px generator`, "");
113
+ try { const ms = await agate.setResolution(r); setProgress(0, "Ready", `${r} px · ${(ms / 1000).toFixed(1)} s`); }
114
+ catch (e) { console.error(e); setProgress(0, "Switch failed", String(e.message || e)); }
115
+ busy = false; ui.go.disabled = false;
116
+ }
117
+ }
118
+
119
+ ui.steps.oninput = () => { $("steps-v").textContent = ui.steps.value; };
120
+ ui.cfg.oninput = () => { $("cfg-v").textContent = Number(ui.cfg.value).toFixed(1); };
121
+ ui.dice.onclick = () => { ui.seed.value = Math.floor(Math.random() * 2 ** 31); };
122
+ ui.crisp.checked = store.get("agate-crisp") === "1";
123
+ const applyCrisp = () => { ui.frame.classList.toggle("crisp", ui.crisp.checked); store.set("agate-crisp", ui.crisp.checked ? "1" : "0"); };
124
+ ui.crisp.onchange = applyCrisp; applyCrisp();
125
+
126
+ const mb = (b) => (b / 1e6).toFixed(0);
127
+ function setProgress(frac, left, right = "") {
128
+ ui.fill.style.width = `${Math.max(0, Math.min(1, frac)) * 100}%`;
129
+ ui.status.textContent = left; ui.statusR.textContent = right;
130
+ }
131
+ function showTimings(rows) {
132
+ ui.timings.innerHTML = rows.map(([k, v]) => `<dt>${k}</dt><dd>${v}</dd>`).join("");
133
+ }
134
+ function clearOutput() {
135
+ ui.placeholder.hidden = false; ui.aiNote.hidden = true; ui.thinker.hidden = true; ui.save.hidden = true; ui.prepared.hidden = true;
136
+ showTimings([]); last = null;
137
+ if (ui.save.href?.startsWith("blob:")) URL.revokeObjectURL(ui.save.href);
138
+ }
139
+
140
+ async function load() {
141
+ busy = true; ui.go.disabled = true;
142
+ window.__agate.state = "loading";
143
+ agate = new Agate({ base: BASE, override: MODELS_OVERRIDE, version, ep, variant: VARIANT, optLevel: params.get("opt") || "all" });
144
+ try {
145
+ const t0 = performance.now();
146
+ const st = await agate.load(({ phase, file, loaded, total }) => {
147
+ if (phase === "download") setProgress(loaded / total, `Downloading ${file}`, `${mb(loaded)} / ${mb(total)} MB`);
148
+ else setProgress(1, `Preparing ${file} on ${ep === "webgpu" ? "WebGPU" : "CPU"}`, `${mb(total)} MB`);
149
+ }, res());
150
+ const loadMs = performance.now() - t0;
151
+ $("dl-size").textContent = `${st.downloadMB.toFixed(0)} MB`;
152
+ ui.backend.textContent = `backend: ${ep === "webgpu" ? "WebGPU" : "WebAssembly (CPU)"}`;
153
+ const src = st.fromCacheMB > st.downloadMB * 0.99 ? "from browser cache" : "downloaded";
154
+ setProgress(0, "Ready", `Preview ${version} · ${st.downloadMB.toFixed(0)} MB ${src} · ${(loadMs / 1000).toFixed(1)} s`);
155
+ window.__agate = { state: "ready", version, load: { ...st, loadMs, ep } };
156
+ ui.go.textContent = "Generate"; ui.go.disabled = false;
157
+ } catch (e) {
158
+ console.error(e);
159
+ setProgress(0, "Load failed", String(e.message || e));
160
+ window.__agate = { state: "error", version, error: String(e.message || e) };
161
+ ui.go.textContent = "Retry load"; ui.go.disabled = false; agate = null;
162
+ }
163
+ busy = false;
164
+ }
165
+
166
+ async function pngWithMetadata() {
167
+ const blob = await new Promise((r) => ui.canvas.toBlob(r, "image/png"));
168
+ const bytes = addPngText(new Uint8Array(await blob.arrayBuffer()), agate.marks.info);
169
+ return new Blob([bytes], { type: "image/png" });
170
+ }
171
+
172
+ function previewFn() {
173
+ thinkerView.reset();
174
+ let planMs = 0, n = 0;
175
+ window.__agate.thinker = { steps: 0, planMs: 0 };
176
+ return ({ step, steps, plan, x1, hw }) => {
177
+ const t0 = performance.now();
178
+ ui.thinker.hidden = false;
179
+ thinkerView.drawPlan(plan);
180
+ thinkerView.drawPrediction(x1, hw);
181
+ ui.thinkerStep.textContent = `${step} / ${steps}`;
182
+ planMs += performance.now() - t0; n++;
183
+ window.__agate.thinker = { steps: n, drawMsMean: planMs / n };
184
+ };
185
+ }
186
+
187
+ async function generate(opts = {}) {
188
+ busy = true; stop = false;
189
+ ui.go.textContent = "Stop"; ui.frame.classList.add("busy");
190
+ window.__agate.state = "generating";
191
+ const steps = opts.steps ?? Number(ui.steps.value);
192
+ try {
193
+ setProgress(0, "Encoding prompt", "");
194
+ const r = await agate.generate({
195
+ prompt: opts.prompt ?? ui.prompt.value.trim(), negative: opts.negative ?? ui.negative.value.trim(),
196
+ seed: Number(ui.seed.value) >>> 0, steps, cfg: opts.cfg ?? Number(ui.cfg.value), noise: opts.noise ?? null,
197
+ watermark: opts.watermark ?? true,
198
+ onStep: (i, n) => setProgress(i / n, `Step ${i} / ${n}`, ""),
199
+ onPreview: (opts.thinker ?? ui.showThinker.checked) ? previewFn() : null,
200
+ shouldStop: () => stop,
201
+ });
202
+ const ctx = ui.canvas.getContext("2d");
203
+ ui.canvas.width = r.width; ui.canvas.height = r.height;
204
+ ctx.putImageData(new ImageData(r.rgba, r.width, r.height), 0, 0);
205
+ ui.placeholder.hidden = true;
206
+ ui.aiNote.hidden = false;
207
+ const T = r.timings;
208
+ setProgress(1, "Done", `${(T.totalMs / 1000).toFixed(1)} s`);
209
+ showTimings([
210
+ ["text encode", `${T.textMs.toFixed(0)} ms (bucket ${T.bucket})`],
211
+ ["per step", `${T.stepMs.toFixed(0)} ms × ${steps} (first ${T.firstStepMs.toFixed(0)} ms)`],
212
+ ["decode + mark", `${T.decodeMs.toFixed(0)} + ${T.markMs.toFixed(0)} ms`],
213
+ ["total", `${(T.totalMs / 1000).toFixed(2)} s`],
214
+ ]);
215
+ const p = r.prepared;
216
+ const changed = agate.mr && (p.text !== (opts.prompt ?? ui.prompt.value.trim()) || p.negative || p.countsNonzero);
217
+ ui.prepared.hidden = !changed;
218
+ if (changed) ui.prepared.textContent = `Model sees: ${p.text}${p.negative ? ` · avoid: ${p.negative}` : ""}${p.countsNonzero ? ` · count code on ${p.countsNonzero} token(s)` : ""}`;
219
+ if (ui.save.href?.startsWith("blob:")) URL.revokeObjectURL(ui.save.href);
220
+ ui.save.href = URL.createObjectURL(await pngWithMetadata());
221
+ ui.save.download = `agate-${version}-${r.width}px-seed${Number(ui.seed.value) >>> 0}.png`;
222
+ ui.save.hidden = false;
223
+ last = r;
224
+ window.__agate = { ...window.__agate, state: "done", timings: T, prepared: p, size: [r.width, r.height], watermarked: r.watermarked,
225
+ thinkerShown: !ui.thinker.hidden };
226
+ return r;
227
+ } catch (e) {
228
+ if (String(e.message) === "stopped") setProgress(0, "Stopped", "");
229
+ else { console.error(e); setProgress(0, "Error", String(e.message || e)); window.__agate.error = String(e.message || e); }
230
+ window.__agate.state = "done";
231
+ } finally {
232
+ busy = false; ui.go.textContent = "Generate"; ui.frame.classList.remove("busy");
233
+ }
234
+ }
235
+
236
+ ui.go.onclick = async () => {
237
+ if (busy && agate?.sessions) { stop = true; return; }
238
+ if (busy) return;
239
+ if (!agate) return load();
240
+ return generate();
241
+ };
242
+ ui.prompt.addEventListener("keydown", (e) => { if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && agate && !busy) generate(); });
243
+
244
+ // Parity self-test: ?parity=1 runs the chosen version's fixture (export/parity_full*.py): same initial noise as the
245
+ // PyTorch package, watermark off (the fixture holds unmarked pixels); then checks the mark on the marked image.
246
+ async function parity() {
247
+ const dir = version === "001" ? "test/" : version === "002" ? "test/002/" : `test/003_${res()}/`;
248
+ const fx = await (await fetch(`${dir}parity.json`)).json();
249
+ const bin = async (f) => new Uint8Array(await (await fetch(`${dir}${f}`)).arrayBuffer());
250
+ const noise = new Float32Array((await bin("parity_noise.bin")).buffer);
251
+ const refZ = new Float32Array((await bin("parity_latent.bin")).buffer);
252
+ const refImg = await bin("parity_image.bin"); // HWC uint8
253
+ ui.prompt.value = fx.prompt; ui.steps.value = fx.steps; ui.cfg.value = fx.cfg; ui.negative.value = "";
254
+ const r = await generate({ prompt: fx.prompt, negative: "", steps: fx.steps, cfg: fx.cfg, noise, watermark: false });
255
+ let dz = 0, dzs = 0, di = 0, dis = 0, se = 0;
256
+ for (let k = 0; k < refZ.length; k++) { const d = Math.abs(r.latent[k] - refZ[k]); dz = Math.max(dz, d); dzs += d; }
257
+ for (let p = 0; p < refImg.length / 3; p++) for (let c = 0; c < 3; c++) {
258
+ const d = Math.abs(r.rgba[p * 4 + c] - refImg[p * 3 + c]); di = Math.max(di, d); dis += d; se += d * d;
259
+ }
260
+ const psnr = 10 * Math.log10(255 * 255 / Math.max(1e-9, se / refImg.length));
261
+ const prepOk = fx.prepared === undefined || (r.prepared.text === fx.prepared && r.prepared.negative === fx.negative);
262
+ // the same image marked, read back in the page
263
+ const { embedWatermark } = await import("./marking.js");
264
+ const marked = Uint8ClampedArray.from(r.rgba);
265
+ embedWatermark(marked, r.width, r.height, agate.marks.payload);
266
+ const rb = readWatermark(marked, r.width, r.height, agate.marks.payload);
267
+ const res_ = { version, resolution: res(), latentMaxAbs: dz, latentMeanAbs: dzs / refZ.length, imageMaxAbs: di, imageMeanAbs: dis / refImg.length,
268
+ psnr, preparedMatchesPython: prepOk, watermarkReadback: rb.text, watermarkBitAcc: rb.bitAccuracy, ep, timings: r.timings };
269
+ const el = $("parity"); el.hidden = false;
270
+ el.innerHTML = `<span class="label"><i class="sq red"></i>Parity vs PyTorch package · Preview ${version} · ${res()} px (${ep})</span>
271
+ <p>final latent max |Δ| ${dz.toFixed(4)} (mean ${res_.latentMeanAbs.toFixed(5)}) · image max |Δ| ${di}/255 (mean ${res_.imageMeanAbs.toFixed(3)}, PSNR ${psnr.toFixed(1)} dB)
272
+ · prompt pipeline ${prepOk ? "identical" : "DIFFERENT"} · watermark read back: ${rb.text} (${(rb.bitAccuracy * 100).toFixed(0)}% bits)</p>`;
273
+ window.__agate = { ...window.__agate, parity: res_, state: "parity-done" };
274
+ }
275
+
276
+ // Golden test of the prompt pipeline in this browser (tokenizers.js + prompt.js) against the Python package.
277
+ async function golden() {
278
+ const G = await (await fetch("test/prompt_golden.json")).json();
279
+ const tok = agate.tok, fails = [];
280
+ const enc = (s) => agate.tokenize(s).map(Number);
281
+ for (const c of G.cases) {
282
+ const [text, neg] = preparePrompt(c.prompt);
283
+ const ids = enc(text);
284
+ const cv = countVector(tok, text, ids, ids.length), counts = {};
285
+ cv.forEach((v, j) => { if (v) counts[String(j)] = v; });
286
+ const ok = text === c.prepared && neg === c.negative && JSON.stringify(ids) === JSON.stringify(c.ids)
287
+ && JSON.stringify(enc(neg)) === JSON.stringify(c.neg_ids) && JSON.stringify(counts) === JSON.stringify(c.counts);
288
+ if (!ok) fails.push(c.prompt);
289
+ }
290
+ const res_ = { cases: G.cases.length, identical: G.cases.length - fails.length, fails };
291
+ const el = $("parity"); el.hidden = false;
292
+ el.innerHTML += `<span class="label"><i class="sq red"></i>Prompt pipeline golden test</span><p>${res_.identical} / ${res_.cases} prompts identical to the Python package (prompt, negative, token ids, count code)</p>`;
293
+ window.__agate = { ...window.__agate, golden: res_ };
294
+ }
295
+
296
+ // For the automated test: the PNG exactly as "Save PNG" gives it, base64
297
+ window.__agateSavedPng = async () => {
298
+ const b = new Uint8Array(await (await pngWithMetadata()).arrayBuffer());
299
+ let s = ""; for (let i = 0; i < b.length; i += 0x8000) s += String.fromCharCode(...b.subarray(i, i + 0x8000));
300
+ return btoa(s);
301
+ };
302
+ window.agateClearCache = clearCache;
303
+
304
+ (async () => {
305
+ renderVersion();
306
+ if (!FORCE_EP) {
307
+ const s = await webgpuStatus();
308
+ if (!s.ok) {
309
+ gpuOk = false; ep = "wasm";
310
+ $("nogpu").hidden = false; $("nogpu-why").textContent = s.why;
311
+ ui.go.disabled = true; ui.go.textContent = "WebGPU required";
312
+ window.__agate = { state: "no-webgpu", why: s.why, version };
313
+ $("use-wasm").onclick = () => { $("nogpu").hidden = true; gpuOk = true; ui.go.textContent = "Load model (CPU)"; ui.go.disabled = false; };
314
+ return;
315
+ }
316
+ ui.backend.textContent = `backend: WebGPU${s.adapter ? " · " + s.adapter : ""}`;
317
+ }
318
+ ui.go.disabled = false;
319
+ ui.go.textContent = "Load model";
320
+ if (params.has("autoload") || PARITY || GOLDEN || await hasCachedModel(version)) { // second visit: load straight from cache
321
+ await load();
322
+ if (GOLDEN && agate && version === "003") await golden();
323
+ if (PARITY && agate) await parity();
324
+ if (GOLDEN && !PARITY) window.__agate.state = "golden-done";
325
+ }
326
+ })();
js/marking.js ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // AI-generated content marking for the images this page makes (EU AI Act Art. 50(2)), the same marks as the
2
+ // Python packages (agate/marking.py of Logolabs/agate-preview-002 / -003):
3
+ // * an invisible watermark in the pixels: invisible-watermark's (MIT) 'dwtDctSvd' method, fixed 64-bit payload
4
+ // "AGATE" + release ("AGATE001" / "AGATE002" / "AGATE003"), ported from the library with its exact conventions
5
+ // (OpenCV 8-bit YUV, U channel only, Haar LL band, 4x4 DCT + SVD, s0 quantised to 36, the library's swapped
6
+ // detail order in the inverse DWT, numpy's truncating uint8 cast). agate.detect_watermark() in Python reads it.
7
+ // * provenance text chunks (tEXt) in the downloaded PNG: ai_generated, generator, model, watermark -- the keys
8
+ // and values the Python packages write. The prompt is NOT written.
9
+ // Neither mark is tamper-proof; see the model cards.
10
+
11
+ export const METHOD = "dwtDctSvd";
12
+ const SCALE = 36, BLOCK = 4;
13
+ const S2 = 0.7071067811865476; // PyWavelets' Haar tap, 1/sqrt(2)
14
+
15
+ export function marks(release) {
16
+ const payload = "AGATE" + release; // 8 ASCII bytes = 64 bits
17
+ return {
18
+ payload,
19
+ info: {
20
+ ai_generated: "true",
21
+ generator: `Agate Preview ${release} (LogoLabs)`,
22
+ model: `Logolabs/agate-preview-${release}`,
23
+ watermark: `invisible-watermark ${METHOD}, payload ${payload}`,
24
+ },
25
+ };
26
+ }
27
+
28
+ const desc = (x) => (x + 8192) >> 14; // OpenCV CV_DESCALE(x, 14)
29
+ const sat = (x) => (x < 0 ? 0 : x > 255 ? 255 : x);
30
+ const u8unsafe = (x) => { const t = Math.trunc(x) % 256; return t < 0 ? t + 256 : t; }; // numpy float -> uint8
31
+
32
+ // orthonormal 4-point DCT-II matrix (cv2.dct on a 4x4 block = C X C^T)
33
+ const C = [];
34
+ for (let k = 0; k < BLOCK; k++) {
35
+ C.push([]);
36
+ for (let n = 0; n < BLOCK; n++) C[k].push(Math.sqrt(k === 0 ? 1 / BLOCK : 2 / BLOCK) * Math.cos(Math.PI * (2 * n + 1) * k / (2 * BLOCK)));
37
+ }
38
+ function mul(A, B) { const R = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]; for (let i = 0; i < 4; i++) for (let j = 0; j < 4; j++) { let s = 0; for (let k = 0; k < 4; k++) s += A[i][k] * B[k][j]; R[i][j] = s; } return R; }
39
+ const T = (A) => A[0].map((_, j) => A.map((r) => r[j]));
40
+ const CT = T(C);
41
+
42
+ // largest singular value of a 4x4 matrix and its singular vectors (Jacobi on D^T D)
43
+ function topSVD(D) {
44
+ const M = mul(T(D), D);
45
+ const V = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]];
46
+ for (let sweep = 0; sweep < 30; sweep++) {
47
+ let off = 0;
48
+ for (let p = 0; p < 4; p++) for (let q = p + 1; q < 4; q++) off += M[p][q] * M[p][q];
49
+ if (off < 1e-30) break;
50
+ for (let p = 0; p < 4; p++) for (let q = p + 1; q < 4; q++) {
51
+ if (Math.abs(M[p][q]) < 1e-300) continue;
52
+ const th = (M[q][q] - M[p][p]) / (2 * M[p][q]);
53
+ const t = Math.sign(th || 1) / (Math.abs(th) + Math.sqrt(th * th + 1));
54
+ const c = 1 / Math.sqrt(t * t + 1), s = t * c;
55
+ for (let k = 0; k < 4; k++) { const a = M[k][p], b = M[k][q]; M[k][p] = c * a - s * b; M[k][q] = s * a + c * b; }
56
+ for (let k = 0; k < 4; k++) { const a = M[p][k], b = M[q][k]; M[p][k] = c * a - s * b; M[q][k] = s * a + c * b; }
57
+ for (let k = 0; k < 4; k++) { const a = V[k][p], b = V[k][q]; V[k][p] = c * a - s * b; V[k][q] = s * a + c * b; }
58
+ }
59
+ }
60
+ let best = 0;
61
+ for (let i = 1; i < 4; i++) if (M[i][i] > M[best][best]) best = i;
62
+ const s0 = Math.sqrt(Math.max(0, M[best][best]));
63
+ let v = V.map((r) => r[best]), u;
64
+ if (s0 < 1e-12) { u = [1, 0, 0, 0]; v = [1, 0, 0, 0]; }
65
+ else u = D.map((r) => (r[0] * v[0] + r[1] * v[1] + r[2] * v[2] + r[3] * v[3]) / s0);
66
+ return { s0, u, v };
67
+ }
68
+
69
+ function payloadBits(payload) {
70
+ const bytes = new TextEncoder().encode(payload), bits = [];
71
+ for (const b of bytes) for (let k = 7; k >= 0; k--) bits.push((b >> k) & 1);
72
+ return bits;
73
+ }
74
+
75
+ // U channel (OpenCV BGR2YUV, 8 bit) of an RGBA buffer -> Int32Array(W*H), plus Y and V for the inverse
76
+ function toYUV(rgba, W, H) {
77
+ const Y = new Int32Array(W * H), U = new Int32Array(W * H), V = new Int32Array(W * H);
78
+ for (let p = 0; p < W * H; p++) {
79
+ const r = rgba[4 * p], g = rgba[4 * p + 1], b = rgba[4 * p + 2];
80
+ const y = desc(b * 1868 + g * 9617 + r * 4899);
81
+ Y[p] = y; U[p] = sat(desc((b - y) * 8061 + (128 << 14))); V[p] = sat(desc((r - y) * 14369 + (128 << 14)));
82
+ }
83
+ return { Y, U, V };
84
+ }
85
+
86
+ // LL band of the Haar DWT of U over the (H//4*4, W//4*4) region; -> {ca, ch, cv, cd} as Float64Array (h2 x w2)
87
+ function haar(U, W, R, Cc) {
88
+ const h2 = R / 2, w2 = Cc / 2, n = h2 * w2;
89
+ const ca = new Float64Array(n), ch = new Float64Array(n), cv = new Float64Array(n), cd = new Float64Array(n);
90
+ for (let i = 0; i < h2; i++) for (let j = 0; j < w2; j++) {
91
+ const a = U[(2 * i) * W + 2 * j], b = U[(2 * i) * W + 2 * j + 1], c = U[(2 * i + 1) * W + 2 * j], d = U[(2 * i + 1) * W + 2 * j + 1];
92
+ const k = i * w2 + j;
93
+ // PyWavelets' exact float order: pairs along rows first (a|c, b|d), then along columns
94
+ const l0 = a * S2 + c * S2, l1 = b * S2 + d * S2, h0 = a * S2 - c * S2, h1 = b * S2 - d * S2;
95
+ ca[k] = l0 * S2 + l1 * S2; cv[k] = l0 * S2 - l1 * S2; ch[k] = h0 * S2 + h1 * S2; cd[k] = h0 * S2 - h1 * S2;
96
+ }
97
+ return { ca, ch, cv, cd, h2, w2 };
98
+ }
99
+
100
+ function blockAt(ca, w2, bi, bj) { const B = []; for (let r = 0; r < 4; r++) { B.push([]); for (let c = 0; c < 4; c++) B[r].push(ca[(bi * 4 + r) * w2 + bj * 4 + c]); } return B; }
101
+
102
+ // Embed `payload` in place into rgba (Uint8ClampedArray / Uint8Array, W*H*4). Needs W*H >= 256*256.
103
+ export function embedWatermark(rgba, W, H, payload) {
104
+ if (W * H < 256 * 256) throw new Error("watermark: image too small (needs at least 256 x 256)");
105
+ const bits = payloadBits(payload);
106
+ const { Y, U, V } = toYUV(rgba, W, H);
107
+ const R = Math.floor(H / 4) * 4, Cc = Math.floor(W / 4) * 4;
108
+ const { ca, ch, cv, cd, h2, w2 } = haar(U, W, R, Cc);
109
+ const nbi = Math.floor(h2 / 4), nbj = Math.floor(w2 / 4);
110
+ let num = 0;
111
+ for (let bi = 0; bi < nbi; bi++) for (let bj = 0; bj < nbj; bj++, num++) {
112
+ const D = mul(mul(C, blockAt(ca, w2, bi, bj)), CT);
113
+ const { s0, u, v } = topSVD(D);
114
+ const s1 = (Math.floor(s0 / SCALE) + 0.25 + 0.5 * bits[num % bits.length]) * SCALE;
115
+ const Dn = D.map((row, i) => row.map((x, j) => x + (s1 - s0) * u[i] * v[j]));
116
+ const Bn = mul(mul(CT, Dn), C);
117
+ for (let r = 0; r < 4; r++) for (let c = 0; c < 4; c++) ca[(bi * 4 + r) * w2 + bj * 4 + c] = Bn[r][c];
118
+ }
119
+ // inverse Haar with the library's swapped details (cv as H, ch as V), truncating uint8 cast
120
+ for (let i = 0; i < h2; i++) for (let j = 0; j < w2; j++) {
121
+ // pywt.idwt2((ca, (cv, ch, cd))): the library's swapped details; PyWavelets' float order (columns, then rows)
122
+ const k = i * w2 + j, A = ca[k], hIn = cv[k], vIn = ch[k], d = cd[k];
123
+ const L0 = A * S2 + vIn * S2, L1 = A * S2 - vIn * S2, H0 = hIn * S2 + d * S2, H1 = hIn * S2 - d * S2;
124
+ U[(2 * i) * W + 2 * j] = u8unsafe(L0 * S2 + H0 * S2);
125
+ U[(2 * i) * W + 2 * j + 1] = u8unsafe(L1 * S2 + H1 * S2);
126
+ U[(2 * i + 1) * W + 2 * j] = u8unsafe(L0 * S2 - H0 * S2);
127
+ U[(2 * i + 1) * W + 2 * j + 1] = u8unsafe(L1 * S2 - H1 * S2);
128
+ }
129
+ for (let p = 0; p < W * H; p++) { // OpenCV YUV2BGR, 8 bit
130
+ const y = Y[p], uu = U[p] - 128, vq = V[p] - 128;
131
+ rgba[4 * p + 2] = sat(y + desc(uu * 33292));
132
+ rgba[4 * p + 1] = sat(y + desc(uu * -6472 + vq * -9519));
133
+ rgba[4 * p] = sat(y + desc(vq * 18678));
134
+ }
135
+ return rgba;
136
+ }
137
+
138
+ // -> {bits, text, bitAccuracy} against `payload` (a self-check; the reference detector is the Python one)
139
+ export function readWatermark(rgba, W, H, payload) {
140
+ const want = payloadBits(payload), n = want.length;
141
+ const { U } = toYUV(rgba, W, H);
142
+ const R = Math.floor(H / 4) * 4, Cc = Math.floor(W / 4) * 4;
143
+ const { ca, h2, w2 } = haar(U, W, R, Cc);
144
+ const nbi = Math.floor(h2 / 4), nbj = Math.floor(w2 / 4);
145
+ const sum = new Float64Array(n), cnt = new Float64Array(n);
146
+ let num = 0;
147
+ for (let bi = 0; bi < nbi; bi++) for (let bj = 0; bj < nbj; bj++, num++) {
148
+ const { s0 } = topSVD(mul(mul(C, blockAt(ca, w2, bi, bj)), CT));
149
+ sum[num % n] += (s0 % SCALE) > SCALE * 0.5 ? 1 : 0; cnt[num % n] += 1;
150
+ }
151
+ const bits = Array.from(sum, (s, k) => (s / cnt[k]) * 255 > 127 ? 1 : 0);
152
+ let same = 0; bits.forEach((b, k) => { if (b === want[k]) same++; });
153
+ const bytes = []; for (let k = 0; k < n; k += 8) { let b = 0; for (let q = 0; q < 8; q++) b = (b << 1) | bits[k + q]; bytes.push(b); }
154
+ return { bits, text: String.fromCharCode(...bytes), bitAccuracy: same / n };
155
+ }
156
+
157
+ // ---- PNG tEXt chunks ---------------------------------------------------------------------------
158
+ let CRC_TABLE = null;
159
+ function crc32(bytes) {
160
+ if (!CRC_TABLE) { CRC_TABLE = new Uint32Array(256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; CRC_TABLE[n] = c >>> 0; } }
161
+ let c = 0xffffffff;
162
+ for (const b of bytes) c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8);
163
+ return (c ^ 0xffffffff) >>> 0;
164
+ }
165
+
166
+ // PNG bytes -> PNG bytes with one tEXt chunk per entry of `info` (Latin-1 keys/values), inserted before IEND.
167
+ export function addPngText(png, info) {
168
+ const latin1 = (s) => Uint8Array.from(s, (ch) => { const c = ch.charCodeAt(0); return c < 256 ? c : 63; });
169
+ const chunks = [];
170
+ for (const [k, v] of Object.entries(info)) {
171
+ const data = new Uint8Array([...latin1(k), 0, ...latin1(String(v))]);
172
+ const typeData = new Uint8Array([116, 69, 88, 116, ...data]); // "tEXt"
173
+ const out = new Uint8Array(12 + data.length), dv = new DataView(out.buffer);
174
+ dv.setUint32(0, data.length); out.set(typeData, 4); dv.setUint32(8 + data.length, crc32(typeData));
175
+ chunks.push(out);
176
+ }
177
+ // find IEND (the last chunk): 12 bytes from the end in every well-formed PNG
178
+ const iend = png.length - 12;
179
+ const extra = chunks.reduce((s, c) => s + c.length, 0);
180
+ const res = new Uint8Array(png.length + extra);
181
+ res.set(png.subarray(0, iend), 0);
182
+ let o = iend;
183
+ for (const c of chunks) { res.set(c, o); o += c.length; }
184
+ res.set(png.subarray(iend), o);
185
+ return res;
186
+ }
js/prompt.js ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Agate Preview 003 prompt pipeline, a line-by-line port of agate/prompt_norm.py (+ AgatePipeline.prepare
2
+ // and prompt_norm.count_tensor) of Logolabs/agate-preview-003. Checked against the Python package by
3
+ // test/prompt_golden.json (tools: export/prompt_golden.py writes it, test/prompt_golden.mjs or ?golden=1 checks it).
4
+ //
5
+ // Python-compatibility notes (the reason for the helpers below):
6
+ // * Python's \s, \d, \b and str.isalnum/isspace are Unicode-aware; JS's are ASCII. PY_WS, \p{Nd} and the
7
+ // Unicode word-boundary WB mirror Python's definitions (\w = letters, numbers, underscore).
8
+ // * Python counts code points; JS strings count UTF-16 units. Lengths that gate behaviour use cpLen().
9
+ // * Token offsets come from the HF fast tokenizer in Python; here they are rebuilt from the byte-level BPE
10
+ // tokens (tokenOffsets), in UTF-16 units, which gives the same overlaps as Python's code-point offsets.
11
+
12
+ const PY_WS = "\\t\\n\\x0b\\x0c\\r\\x1c-\\x20\\x85\\xa0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000";
13
+ const S = `[${PY_WS}]`; // Python's \s
14
+ const NS = `[^${PY_WS}]`; // Python's \S
15
+ const W = "[\\p{L}\\p{N}_]"; // Python's \w (str patterns)
16
+ const WB = `(?:(?<=${W})(?!${W})|(?<!${W})(?=${W}))`; // Python's \b
17
+ const D = "\\p{Nd}"; // Python's \d
18
+ const re = (src, flags = "") => new RegExp(src, flags.includes("u") ? flags : flags + "u");
19
+
20
+ const NUM_LIST = "zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty".split(" ");
21
+ const NUM_WORDS = new Map(NUM_LIST.map((w, i) => [w, i]));
22
+ NUM_WORDS.set("dozen", 12);
23
+ const WORD_OF = new Map(NUM_LIST.map((w, i) => [i, w]));
24
+ const NOT_COUNT_NEXT = new Set(["pm", "am", "o'clock", "percent", "%", "years", "year", "hours", "hour", "minutes", "minute",
25
+ "seconds", "second", "times", "px", "cm", "mm", "m", "km", "kg", "g", "inch", "inches", "feet",
26
+ "degrees", "x", "d", "k", "th", "st", "nd", "rd", "of", "o"]);
27
+ const ONE_PRONOUN_PREV = new Set(["the", "this", "that", "no", "each", "every", "any", "some", "which", "another", "someone",
28
+ "everyone", "anyone", "only"]);
29
+ export const SPELL_SEP = " || spell: ";
30
+ const SPELL_MAX_FRAC = 0.6, SPELL_MAX_CHARS = 40, SPELL_MAX_WORDS = 6, SPELL_MAX_SPANS = 3;
31
+
32
+ const QUOTE_SRC = '"[^"]*"|“[^”]*”';
33
+ const cpLen = (s) => { let n = 0; for (const _ of s) n++; return n; }; // len() in Python
34
+ const isSpace = (ch) => re(`^${S}$`).test(ch);
35
+ const pyStrip = (s, chars = null) => {
36
+ const f = chars === null ? isSpace : (c) => chars.includes(c);
37
+ let a = 0, b = s.length;
38
+ while (a < b && f(s[a])) a++;
39
+ while (b > a && f(s[b - 1])) b--;
40
+ return s.slice(a, b);
41
+ };
42
+ const pySplit = (s) => s.split(re(`${S}+`)).filter((x) => x.length > 0); // str.split()
43
+ const isAlnum = (ch) => /^[\p{L}\p{N}]$/u.test(ch);
44
+ const isAlpha = (w) => w.length > 0 && /^\p{L}+$/u.test(w);
45
+ const isUpper = (w) => w !== w.toLowerCase() && w === w.toUpperCase(); // ASCII words only reach these
46
+ const isLower = (w) => w !== w.toUpperCase() && w === w.toLowerCase();
47
+ const isDigits = (w) => /^\p{Nd}+$/u.test(w);
48
+
49
+ function digitValue(cp) { // value of one \p{Nd} code point: Nd code points come in runs of ten
50
+ let start = cp;
51
+ while (/\p{Nd}/u.test(String.fromCodePoint(start - 1))) start--;
52
+ return (cp - start) % 10;
53
+ }
54
+ function pyInt(w) { // int() of a \p{Nd}+ string
55
+ let n = 0;
56
+ for (const ch of w) n = n * 10 + digitValue(ch.codePointAt(0));
57
+ return n;
58
+ }
59
+
60
+ function outsideQuotes(text) {
61
+ const spans = [];
62
+ let last = 0;
63
+ for (const m of text.matchAll(re(QUOTE_SRC, "g"))) { spans.push([last, m.index]); last = m.index + m[0].length; }
64
+ spans.push([last, text.length]);
65
+ return spans.filter(([a, b]) => b > a);
66
+ }
67
+
68
+ function mapOutside(text, fn) {
69
+ let out = "", last = 0;
70
+ for (const [a, b] of outsideQuotes(text)) { out += text.slice(last, a) + fn(text.slice(a, b)); last = b; }
71
+ return out + text.slice(last);
72
+ }
73
+
74
+ function fixCase(words) {
75
+ const long2 = words.filter((w) => cpLen(w) >= 2 && isAlpha(w));
76
+ if (long2.length && long2.filter(isUpper).length / long2.length >= 0.6) return "shout";
77
+ const long3 = words.filter((w) => cpLen(w) >= 3 && isAlpha(w));
78
+ if (long3.length >= 3) {
79
+ const t = long3.filter((w) => { const c = [...w]; return isUpper(c[0]) && isLower(c.slice(1).join("")); }).length;
80
+ if (t / long3.length >= 0.7) return "title";
81
+ }
82
+ return null;
83
+ }
84
+
85
+ export function isJsonCaption(text) {
86
+ const s = pyStrip(text.split(SPELL_SEP)[0]);
87
+ if (!(s.startsWith("{") && s.endsWith("}"))) return false;
88
+ try { const v = JSON.parse(s); return v !== null && typeof v === "object" && !Array.isArray(v); } catch { return false; }
89
+ }
90
+
91
+ function isCountContext(seg, end) {
92
+ const nxt = re(`^${S}*([A-Za-z%']+)`).exec(seg.slice(end));
93
+ return !!nxt && !NOT_COUNT_NEXT.has(nxt[1].toLowerCase());
94
+ }
95
+
96
+ function canonNumbers(seg) {
97
+ return seg.replace(re(`${WB}[A-Za-z]+${WB}|${WB}${D}+${WB}`, "g"), (w, off) => {
98
+ const lw = w.toLowerCase();
99
+ if (NUM_WORDS.has(lw)) return lw;
100
+ if (isDigits(w)) { const n = pyInt(w); if (n >= 1 && n <= 20 && isCountContext(seg, off + w.length)) return WORD_OF.get(n); }
101
+ return w;
102
+ });
103
+ }
104
+
105
+ export function normalize(text) {
106
+ if (isJsonCaption(text)) return text;
107
+ text = pyStrip(text.replace(re(`${S}+`, "g"), " "));
108
+ const words = [];
109
+ for (const [a, b] of outsideQuotes(text)) for (const m of text.slice(a, b).matchAll(re(`[A-Za-z][A-Za-z'\\-]*|${D}+`, "g"))) words.push(m[0]);
110
+ const mode = fixCase(words);
111
+ if (mode === "shout") text = mapOutside(text, (s) => s.toLowerCase());
112
+ else if (mode === "title") {
113
+ const first = re(`^${S}*${NS}+`).exec(text);
114
+ const head = first ? first[0].length : 0;
115
+ text = text.slice(0, head) + mapOutside(text.slice(head), (s) => s.replace(re(`${WB}[A-Z][a-z]*${WB}`, "g"), (m) => m.toLowerCase()));
116
+ }
117
+ return mapOutside(text, canonNumbers);
118
+ }
119
+
120
+ export function findCounts(text) {
121
+ const out = [];
122
+ if (isJsonCaption(text)) return out;
123
+ text = text.split(SPELL_SEP)[0];
124
+ for (const [a, b] of outsideQuotes(text)) {
125
+ const seg = text.slice(a, b);
126
+ const pat = re(`${WB}(?:a${S}+)?(dozen|pair(?=${S}+of${WB}))${WB}|${WB}([A-Za-z]+|${D}+)${WB}`, "gd");
127
+ for (const m of seg.matchAll(pat)) {
128
+ if (m[1] !== undefined) {
129
+ const [s1, e1] = m.indices[1];
130
+ out.push([a + s1, a + e1, m[1].toLowerCase() === "dozen" ? 12 : 2]);
131
+ continue;
132
+ }
133
+ const w = m[2], lw = w.toLowerCase();
134
+ const [s2, e2] = m.indices[2];
135
+ let n;
136
+ if (isDigits(lw)) { n = pyInt(lw); if (!(n >= 1 && n <= 99)) continue; }
137
+ else if (NUM_WORDS.has(lw) && lw !== "zero" && lw !== "dozen") n = NUM_WORDS.get(lw);
138
+ else continue;
139
+ if (!isCountContext(seg, m.index + m[0].length)) continue;
140
+ const prev = seg.slice(0, m.index).match(/[A-Za-z]+/g) || [];
141
+ if (lw === "one" && prev.length && ONE_PRONOUN_PREV.has(prev[prev.length - 1].toLowerCase())) continue;
142
+ out.push([a + s2, a + e2, n]);
143
+ }
144
+ }
145
+ return out;
146
+ }
147
+
148
+ export function textSpans(text) {
149
+ const body = text.split(SPELL_SEP)[0];
150
+ const out = [];
151
+ for (const m of body.matchAll(re(QUOTE_SRC, "g"))) {
152
+ const c = pyStrip(m[0].slice(1, -1));
153
+ if (c && [...c].some(isAlnum) && cpLen(c) <= SPELL_MAX_FRAC * cpLen(body) && cpLen(c) <= SPELL_MAX_CHARS && pySplit(c).length <= SPELL_MAX_WORDS) out.push(c);
154
+ }
155
+ return out.slice(0, SPELL_MAX_SPANS);
156
+ }
157
+
158
+ export function spell(content) {
159
+ const words = pySplit(content).map((w) => [...w].filter((ch) => isAlnum(ch) || "&!?'-".includes(ch)));
160
+ return words.filter((w) => w.length).map((w) => w.join(" ")).join(" / ");
161
+ }
162
+
163
+ export function addSpelling(text) {
164
+ if (text.includes(SPELL_SEP) || isJsonCaption(text)) return text;
165
+ const spans = textSpans(text);
166
+ if (!spans.length) return text;
167
+ return text + SPELL_SEP + spans.map(spell).join(" ; ");
168
+ }
169
+
170
+ export function splitNegatives(text) {
171
+ const negs = [];
172
+ if (isJsonCaption(text)) return [text, negs];
173
+ const pat = re(`${S}*,?${S}*${WB}(?:with${S}+)?(?:without|no)${S}+(?:any${S}+)?(?:a${S}+|an${S}+|the${S}+)?([^,.;"“]+?)(?=${S}+and${S}+|[,.;]|$)`, "gi");
174
+ const cut = (seg) => seg.replace(pat, (...g) => { negs.push(pyStrip(g[1])); return ""; });
175
+ let pos = mapOutside(text, cut);
176
+ pos = pyStrip(pos.replace(re(`${S}+([,.;])`, "g"), "$1").replace(re(`${S}+`, "g"), " "), " ,;");
177
+ return [pos, negs];
178
+ }
179
+
180
+ // AgatePipeline.prepare: -> [prompt as the text encoder sees it, negative prompt]
181
+ export function prepare(prompt, negativePrompt = "", normalizeOn = true, spellOn = true) {
182
+ let negs = [];
183
+ if (normalizeOn) [prompt, negs] = splitNegatives(normalize(prompt));
184
+ if (spellOn) prompt = addSpelling(prompt);
185
+ const negative = [pyStrip(negativePrompt), ...negs].filter((n) => n).join(", ");
186
+ return [prompt, negative];
187
+ }
188
+
189
+ // ---- token offsets for a byte-level BPE tokenizer (tokenizers.js has no offset mapping) -----------
190
+ let BYTE_OF = null; // GPT-2 bytes_to_unicode, inverted: char -> byte
191
+ function byteDecoder() {
192
+ if (BYTE_OF) return BYTE_OF;
193
+ const bs = [];
194
+ for (let b = 33; b <= 126; b++) bs.push(b);
195
+ for (let b = 161; b <= 172; b++) bs.push(b);
196
+ for (let b = 174; b <= 255; b++) bs.push(b);
197
+ const cs = bs.slice();
198
+ let n = 0;
199
+ for (let b = 0; b < 256; b++) if (!bs.includes(b)) { bs.push(b); cs.push(256 + n); n++; }
200
+ BYTE_OF = new Map(bs.map((b, i) => [String.fromCodePoint(cs[i]), b]));
201
+ return BYTE_OF;
202
+ }
203
+
204
+ // ids: the token ids of `text` ([CLS] ... [SEP]). -> [[start, end], ...] in UTF-16 units of text, [0, 0] for
205
+ // special tokens: Python's offset_mapping (code points) mapped to JS string indices.
206
+ export function tokenOffsets(tok, text, ids) {
207
+ const dec = byteDecoder();
208
+ const nfc = text.normalize("NFC");
209
+ const enc = new TextEncoder();
210
+ // byte index -> [UTF-16 start, UTF-16 end] of the character that byte belongs to
211
+ const charOfByte = [];
212
+ let u = 0;
213
+ for (const ch of nfc) { const nb = enc.encode(ch).length; for (let k = 0; k < nb; k++) charOfByte.push([u, u + ch.length]); u += ch.length; }
214
+ const added = tok.get_added_tokens_decoder?.() ?? new Map();
215
+ const out = [];
216
+ let pos = 0;
217
+ for (const id of ids) {
218
+ const at = added.get(Number(id));
219
+ let nbytes;
220
+ if (at && at.special) { out.push([0, 0]); continue; }
221
+ if (at) nbytes = enc.encode(at.content).length;
222
+ else {
223
+ const s = tok.id_to_token(Number(id));
224
+ nbytes = 0;
225
+ for (const ch of s) nbytes += dec.has(ch) ? 1 : enc.encode(ch).length;
226
+ }
227
+ if (nbytes === 0 || pos >= charOfByte.length) { out.push([0, 0]); continue; }
228
+ const a = charOfByte[pos][0], b = charOfByte[Math.min(pos + nbytes, charOfByte.length) - 1][1];
229
+ out.push([a, b]);
230
+ pos += nbytes;
231
+ }
232
+ // text that NFC changed: offsets refer to the NFC string; the count spans are found in the NFC string too
233
+ return out;
234
+ }
235
+
236
+ // prompt_norm.count_tensor for one prompt: Float32Array(length), the count value at the tokens that spell one.
237
+ export function countVector(tok, text, ids, length) {
238
+ const out = new Float32Array(length);
239
+ const nfc = text.normalize("NFC");
240
+ const spans = findCounts(nfc);
241
+ if (!spans.length) return out;
242
+ const offs = tokenOffsets(tok, nfc, ids);
243
+ for (let j = 0; j < Math.min(length, offs.length); j++) {
244
+ const [a, b] = offs[j];
245
+ if (b <= a) continue;
246
+ for (const [s, e, n] of spans) if (a < e && b > s) out[j] = n;
247
+ }
248
+ return out;
249
+ }
js/thinker.js ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // "Show thinker": the live previews of the ComfyUI nodes (agate-comfyui nodes.py, _progress, live_preview
2
+ // "side_by_side"), computed the same way so the two frontends show the same thing:
3
+ // * the plan: the thinker's output for the conditional branch, 640 channels on a 16 x 16 grid. Its cells are
4
+ // projected on the top 3 principal components (fitted once, at the first step, centred over the cells; each
5
+ // component's sign fixed so its largest-magnitude loading is positive) and mapped to RGB with that step's 2% /
6
+ // 98% quantiles -- a fixed basis, so a colour keeps its meaning while the plan evolves;
7
+ // * the image the model expects at the end, x1 = z + (1 - t) v (guided velocity), shown with ComfyUI's SD 1.5
8
+ // latent -> RGB factors (its "latent2rgb" preview).
9
+
10
+ const RGB_FACTORS = [[0.3512, 0.2297, 0.3227], [0.3250, 0.4974, 0.2350], [-0.2829, 0.1762, 0.2721], [-0.2120, -0.2616, -0.7177]];
11
+
12
+ // top-k eigenvectors of the (d x d) Gram matrix X^T X of centred X (n x d), as torch.linalg.eigh gives them (sign
13
+ // aside), via the smaller (n x n) X X^T: block (subspace) iteration with a 12-vector block + Rayleigh-Ritz, which
14
+ // separates near-equal eigenvalues that plain power iteration mixes; then v = X^T u / |X^T u|.
15
+ function jacobiEig(A, m) { // symmetric m x m (Float64Array) -> {vals, vecs (column-major rows)}
16
+ const a = Float64Array.from(A), V = new Float64Array(m * m);
17
+ for (let i = 0; i < m; i++) V[i * m + i] = 1;
18
+ for (let sweep = 0; sweep < 60; sweep++) {
19
+ let off = 0;
20
+ for (let p = 0; p < m; p++) for (let q = p + 1; q < m; q++) off += a[p * m + q] ** 2;
21
+ if (off < 1e-24) break;
22
+ for (let p = 0; p < m; p++) for (let q = p + 1; q < m; q++) {
23
+ const apq = a[p * m + q];
24
+ if (Math.abs(apq) < 1e-300) continue;
25
+ const th = (a[q * m + q] - a[p * m + p]) / (2 * apq);
26
+ const t = Math.sign(th || 1) / (Math.abs(th) + Math.sqrt(th * th + 1)), c = 1 / Math.sqrt(t * t + 1), s = t * c;
27
+ for (let k = 0; k < m; k++) { const x = a[k * m + p], y = a[k * m + q]; a[k * m + p] = c * x - s * y; a[k * m + q] = s * x + c * y; }
28
+ for (let k = 0; k < m; k++) { const x = a[p * m + k], y = a[q * m + k]; a[p * m + k] = c * x - s * y; a[q * m + k] = s * x + c * y; }
29
+ for (let k = 0; k < m; k++) { const x = V[k * m + p], y = V[k * m + q]; V[k * m + p] = c * x - s * y; V[k * m + q] = s * x + c * y; }
30
+ }
31
+ }
32
+ return { vals: Array.from({ length: m }, (_, i) => a[i * m + i]), V };
33
+ }
34
+
35
+ function topComponents(X, n, d, k = 3, iters = 60, block = 12) {
36
+ const G = new Float64Array(n * n);
37
+ for (let i = 0; i < n; i++) for (let j = i; j < n; j++) {
38
+ let s = 0; const ri = i * d, rj = j * d;
39
+ for (let a = 0; a < d; a++) s += X[ri + a] * X[rj + a];
40
+ G[i * n + j] = G[j * n + i] = s;
41
+ }
42
+ const m = Math.min(block, n);
43
+ let Q = Array.from({ length: m }, (_, c) => Float64Array.from({ length: n }, (_, i) => Math.sin(1.7 * i + 0.9 * c + 1) + 0.01 * c));
44
+ const orth = (B) => { // modified Gram-Schmidt, in place
45
+ for (let c = 0; c < B.length; c++) {
46
+ for (let e = 0; e < c; e++) { let p = 0; for (let i = 0; i < n; i++) p += B[c][i] * B[e][i]; for (let i = 0; i < n; i++) B[c][i] -= p * B[e][i]; }
47
+ let nrm = 0; for (let i = 0; i < n; i++) nrm += B[c][i] ** 2; nrm = Math.sqrt(nrm) || 1;
48
+ for (let i = 0; i < n; i++) B[c][i] /= nrm;
49
+ }
50
+ return B;
51
+ };
52
+ const mul = (q) => { const w = new Float64Array(n); for (let i = 0; i < n; i++) { let s = 0; const gi = i * n; for (let j = 0; j < n; j++) s += G[gi + j] * q[j]; w[i] = s; } return w; };
53
+ orth(Q);
54
+ for (let it = 0; it < iters; it++) Q = orth(Q.map(mul));
55
+ const GQ = Q.map(mul), T = new Float64Array(m * m); // Rayleigh-Ritz on the block
56
+ for (let a = 0; a < m; a++) for (let b = 0; b < m; b++) { let s = 0; for (let i = 0; i < n; i++) s += Q[a][i] * GQ[b][i]; T[a * m + b] = s; }
57
+ for (let a = 0; a < m; a++) for (let b = 0; b < a; b++) T[a * m + b] = T[b * m + a] = (T[a * m + b] + T[b * m + a]) / 2;
58
+ const { vals, V } = jacobiEig(T, m);
59
+ const order = vals.map((v, i) => [v, i]).sort((x, y) => y[0] - x[0]).slice(0, k).map((x) => x[1]);
60
+ return order.map((col) => {
61
+ const u = new Float64Array(n);
62
+ for (let a = 0; a < m; a++) { const w = V[a * m + col]; for (let i = 0; i < n; i++) u[i] += w * Q[a][i]; }
63
+ const v = new Float64Array(d);
64
+ for (let i = 0; i < n; i++) { const ui = u[i], ri = i * d; if (ui) for (let a = 0; a < d; a++) v[a] += ui * X[ri + a]; }
65
+ let nrm = 0; for (let a = 0; a < d; a++) nrm += v[a] * v[a]; nrm = Math.sqrt(nrm) || 1;
66
+ let piv = 0;
67
+ for (let a = 0; a < d; a++) { v[a] /= nrm; if (Math.abs(v[a]) > Math.abs(v[piv])) piv = a; }
68
+ if (v[piv] < 0) for (let a = 0; a < d; a++) v[a] = -v[a];
69
+ return v;
70
+ });
71
+ }
72
+
73
+ function quantile(sorted, q) { // torch.quantile, linear interpolation
74
+ const pos = (sorted.length - 1) * q, lo = Math.floor(pos), hi = Math.ceil(pos);
75
+ return sorted[lo] + (sorted[hi] - sorted[lo]) * (pos - lo);
76
+ }
77
+
78
+ export class ThinkerView {
79
+ constructor(planCanvas, predCanvas) { this.planCanvas = planCanvas; this.predCanvas = predCanvas; this.reset(); }
80
+
81
+ reset() { this.basis = null; this.lo = null; this.hi = null; }
82
+
83
+ // plan: Float32Array (C * g * g), channel-major (1, C, g, g)
84
+ drawPlan(plan, C = 640, g = 16) {
85
+ const n = g * g, X = new Float64Array(n * C);
86
+ for (let c = 0; c < C; c++) for (let p = 0; p < n; p++) X[p * C + c] = plan[c * n + p];
87
+ for (let c = 0; c < C; c++) { let m = 0; for (let p = 0; p < n; p++) m += X[p * C + c]; m /= n; for (let p = 0; p < n; p++) X[p * C + c] -= m; }
88
+ if (!this.basis) this.basis = topComponents(X, n, C);
89
+ const Y = this.basis.map((v) => { const y = new Float64Array(n); for (let p = 0; p < n; p++) { let s = 0; const r = p * C; for (let c = 0; c < C; c++) s += X[r + c] * v[c]; y[p] = s; } return y; });
90
+ if (!this.lo) {
91
+ this.lo = Y.map((y) => quantile(Float64Array.from(y).sort(), 0.02));
92
+ this.hi = Y.map((y) => quantile(Float64Array.from(y).sort(), 0.98));
93
+ }
94
+ const img = new ImageData(g, g);
95
+ for (let p = 0; p < n; p++) {
96
+ for (let k = 0; k < 3; k++) img.data[p * 4 + k] = Math.round(Math.min(1, Math.max(0, (Y[k][p] - this.lo[k]) / (this.hi[k] - this.lo[k] + 1e-8))) * 255);
97
+ img.data[p * 4 + 3] = 255;
98
+ }
99
+ const cv = this.planCanvas; cv.width = g; cv.height = g;
100
+ cv.getContext("2d").putImageData(img, 0, 0);
101
+ }
102
+
103
+ // x1: Float32Array (4 * hw * hw) in the model's (scaled SD-VAE) latent space
104
+ drawPrediction(x1, hw) {
105
+ const n = hw * hw, img = new ImageData(hw, hw);
106
+ for (let p = 0; p < n; p++) {
107
+ for (let k = 0; k < 3; k++) {
108
+ let s = 0; for (let c = 0; c < 4; c++) s += x1[c * n + p] * RGB_FACTORS[c][k];
109
+ img.data[p * 4 + k] = Math.floor(Math.min(1, Math.max(0, (s + 1) / 2)) * 255);
110
+ }
111
+ img.data[p * 4 + 3] = 255;
112
+ }
113
+ const cv = this.predCanvas; cv.width = hw; cv.height = hw;
114
+ cv.getContext("2d").putImageData(img, 0, 0);
115
+ }
116
+ }
test/002/parity.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "prompt": "a green teapot and a red cup on a table",
3
+ "steps": 50,
4
+ "cfg": 3.0,
5
+ "seed": 0,
6
+ "source": "release AgatePipeline, CPU fp32, fast_vae (TAESD), torch.randn replaced by parity_noise.bin",
7
+ "onnx_cpu_latent_max_abs": 0.10324227809906006,
8
+ "onnx_cpu_image_max_abs": 14
9
+ }
test/002/parity_image.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:91cbf77d3ba00b0a6517adc736296ebefbd5fd69070d76ddacfedff32d461b1b
3
+ size 196608
test/002/parity_latent.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:de43bcd6e7416d9bcb997b22da8c19d5401a07c0fa651185ec6d49372dba5f89
3
+ size 16384
test/002/parity_noise.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3deff7f51dd573fad149014c76a44c4b6f6251fbaaa9b8e78b794e805871773f
3
+ size 16384
test/003_256/parity.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "prompt": "a shop sign that says \"OPEN\", three red apples, no people",
3
+ "prepared": "a shop sign that says \"OPEN\", three red apples || spell: O P E N",
4
+ "negative": "people",
5
+ "counts_nonzero": {
6
+ "9": 3.0
7
+ },
8
+ "resolution": 256,
9
+ "steps": 50,
10
+ "cfg": 3.0,
11
+ "seed": 0,
12
+ "shift": 1.0,
13
+ "source": "release AgatePipeline (003), CPU fp32, fast_vae (TAESD), watermark off, torch.randn replaced by parity_noise.bin",
14
+ "onnx_cpu_latent_max_abs": 0.1917351484298706,
15
+ "onnx_cpu_image_max_abs": 47,
16
+ "onnx_cpu_image_mean_abs": 0.21456400553385416,
17
+ "onnx_cpu_psnr_db": 48.9241802774759
18
+ }
test/003_256/parity_image.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ded558a14b1d2af78f387ec7c8f807dfaa0b82bc298c6519f4976e4f1a406260
3
+ size 196608
test/003_256/parity_latent.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3d68c5f46abf7b82ca9bcabd397a0765c52ae28c2e3443583c78ee115005d2bb
3
+ size 16384
test/003_256/parity_noise.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3deff7f51dd573fad149014c76a44c4b6f6251fbaaa9b8e78b794e805871773f
3
+ size 16384
test/003_512/parity.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "prompt": "a shop sign that says \"OPEN\", three red apples, no people",
3
+ "prepared": "a shop sign that says \"OPEN\", three red apples || spell: O P E N",
4
+ "negative": "people",
5
+ "counts_nonzero": {
6
+ "9": 3.0
7
+ },
8
+ "resolution": 512,
9
+ "steps": 50,
10
+ "cfg": 3.0,
11
+ "seed": 0,
12
+ "shift": 2.0,
13
+ "source": "release AgatePipeline (003), CPU fp32, fast_vae (TAESD), watermark off, torch.randn replaced by parity_noise.bin",
14
+ "onnx_cpu_latent_max_abs": 0.11000287532806396,
15
+ "onnx_cpu_image_max_abs": 28,
16
+ "onnx_cpu_image_mean_abs": 0.11280314127604167,
17
+ "onnx_cpu_psnr_db": 54.79136477697858
18
+ }
test/003_512/parity_image.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fafe740a618a3a4343bfe8ddf9819f353f9064004b8dd06c9676bd70f05c1621
3
+ size 786432
test/003_512/parity_latent.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9627da8e372d20fd5d2f60166d61eebc6ad104a29f17dd2d96aae6a0c917a204
3
+ size 65536
test/003_512/parity_noise.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b60eb3b5b14ff459a5517d7921ec74e7f82a23caaf9d5894e5e4a3f16ee9fd40
3
+ size 65536
test/prompt_golden.json ADDED
@@ -0,0 +1,1251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "source": "agate-preview-003/agate (prompt_norm.py, AgatePipeline.prepare)",
3
+ "cases": [
4
+ {
5
+ "prompt": "a red cube on top of a blue sphere",
6
+ "prepared": "a red cube on top of a blue sphere",
7
+ "negative": "",
8
+ "ids": [
9
+ 50281,
10
+ 66,
11
+ 2502,
12
+ 23636,
13
+ 327,
14
+ 1755,
15
+ 273,
16
+ 247,
17
+ 4797,
18
+ 15269,
19
+ 50282
20
+ ],
21
+ "neg_ids": [
22
+ 50281,
23
+ 50282
24
+ ],
25
+ "counts": {},
26
+ "find_counts": []
27
+ },
28
+ {
29
+ "prompt": "a green teapot and a red cup on a table",
30
+ "prepared": "a green teapot and a red cup on a table",
31
+ "negative": "",
32
+ "ids": [
33
+ 50281,
34
+ 66,
35
+ 4759,
36
+ 716,
37
+ 522,
38
+ 302,
39
+ 285,
40
+ 247,
41
+ 2502,
42
+ 5500,
43
+ 327,
44
+ 247,
45
+ 2829,
46
+ 50282
47
+ ],
48
+ "neg_ids": [
49
+ 50281,
50
+ 50282
51
+ ],
52
+ "counts": {},
53
+ "find_counts": []
54
+ },
55
+ {
56
+ "prompt": "three cats sitting on a sofa",
57
+ "prepared": "three cats sitting on a sofa",
58
+ "negative": "",
59
+ "ids": [
60
+ 50281,
61
+ 13524,
62
+ 16581,
63
+ 7063,
64
+ 327,
65
+ 247,
66
+ 30929,
67
+ 50282
68
+ ],
69
+ "neg_ids": [
70
+ 50281,
71
+ 50282
72
+ ],
73
+ "counts": {
74
+ "1": 3.0
75
+ },
76
+ "find_counts": [
77
+ [
78
+ 0,
79
+ 5,
80
+ 3
81
+ ]
82
+ ]
83
+ },
84
+ {
85
+ "prompt": "3 dogs and 2 birds in a park",
86
+ "prepared": "three dogs and two birds in a park",
87
+ "negative": "",
88
+ "ids": [
89
+ 50281,
90
+ 13524,
91
+ 9097,
92
+ 285,
93
+ 767,
94
+ 11260,
95
+ 275,
96
+ 247,
97
+ 5603,
98
+ 50282
99
+ ],
100
+ "neg_ids": [
101
+ 50281,
102
+ 50282
103
+ ],
104
+ "counts": {
105
+ "1": 3.0,
106
+ "4": 2.0
107
+ },
108
+ "find_counts": [
109
+ [
110
+ 0,
111
+ 5,
112
+ 3
113
+ ],
114
+ [
115
+ 15,
116
+ 18,
117
+ 2
118
+ ]
119
+ ]
120
+ },
121
+ {
122
+ "prompt": "A PHOTO OF THREE RED APPLES ON A WOODEN TABLE",
123
+ "prepared": "a photo of three red apples on a wooden table",
124
+ "negative": "",
125
+ "ids": [
126
+ 50281,
127
+ 66,
128
+ 7512,
129
+ 273,
130
+ 1264,
131
+ 2502,
132
+ 28580,
133
+ 327,
134
+ 247,
135
+ 14872,
136
+ 2829,
137
+ 50282
138
+ ],
139
+ "neg_ids": [
140
+ 50281,
141
+ 50282
142
+ ],
143
+ "counts": {
144
+ "4": 3.0
145
+ },
146
+ "find_counts": [
147
+ [
148
+ 11,
149
+ 16,
150
+ 3
151
+ ]
152
+ ]
153
+ },
154
+ {
155
+ "prompt": "A Photo Of Two Cats Sleeping On A Bed",
156
+ "prepared": "A photo of two cats sleeping on a bed",
157
+ "negative": "",
158
+ "ids": [
159
+ 50281,
160
+ 34,
161
+ 7512,
162
+ 273,
163
+ 767,
164
+ 16581,
165
+ 14343,
166
+ 327,
167
+ 247,
168
+ 3722,
169
+ 50282
170
+ ],
171
+ "neg_ids": [
172
+ 50281,
173
+ 50282
174
+ ],
175
+ "counts": {
176
+ "4": 2.0
177
+ },
178
+ "find_counts": [
179
+ [
180
+ 11,
181
+ 14,
182
+ 2
183
+ ]
184
+ ]
185
+ },
186
+ {
187
+ "prompt": "Paris at night, the Eiffel Tower lit up",
188
+ "prepared": "Paris at night, the Eiffel Tower lit up",
189
+ "negative": "",
190
+ "ids": [
191
+ 50281,
192
+ 36062,
193
+ 387,
194
+ 2360,
195
+ 13,
196
+ 253,
197
+ 444,
198
+ 1648,
199
+ 293,
200
+ 20610,
201
+ 6195,
202
+ 598,
203
+ 50282
204
+ ],
205
+ "neg_ids": [
206
+ 50281,
207
+ 50282
208
+ ],
209
+ "counts": {},
210
+ "find_counts": []
211
+ },
212
+ {
213
+ "prompt": "a shop sign that says \"OPEN\", three red apples, no people",
214
+ "prepared": "a shop sign that says \"OPEN\", three red apples || spell: O P E N",
215
+ "negative": "people",
216
+ "ids": [
217
+ 50281,
218
+ 66,
219
+ 8979,
220
+ 861,
221
+ 326,
222
+ 2296,
223
+ 346,
224
+ 28652,
225
+ 995,
226
+ 1264,
227
+ 2502,
228
+ 28580,
229
+ 2785,
230
+ 15368,
231
+ 27,
232
+ 473,
233
+ 367,
234
+ 444,
235
+ 427,
236
+ 50282
237
+ ],
238
+ "neg_ids": [
239
+ 50281,
240
+ 13174,
241
+ 50282
242
+ ],
243
+ "counts": {
244
+ "9": 3.0
245
+ },
246
+ "find_counts": [
247
+ [
248
+ 30,
249
+ 35,
250
+ 3
251
+ ]
252
+ ]
253
+ },
254
+ {
255
+ "prompt": "a coffee cup with the text \"Good Morning\" on it",
256
+ "prepared": "a coffee cup with the text \"Good Morning\" on it || spell: G o o d / M o r n i n g",
257
+ "negative": "",
258
+ "ids": [
259
+ 50281,
260
+ 66,
261
+ 8574,
262
+ 5500,
263
+ 342,
264
+ 253,
265
+ 2505,
266
+ 346,
267
+ 8620,
268
+ 25565,
269
+ 3,
270
+ 327,
271
+ 352,
272
+ 2785,
273
+ 15368,
274
+ 27,
275
+ 443,
276
+ 258,
277
+ 258,
278
+ 277,
279
+ 1227,
280
+ 353,
281
+ 258,
282
+ 391,
283
+ 295,
284
+ 891,
285
+ 295,
286
+ 305,
287
+ 50282
288
+ ],
289
+ "neg_ids": [
290
+ 50281,
291
+ 50282
292
+ ],
293
+ "counts": {},
294
+ "find_counts": []
295
+ },
296
+ {
297
+ "prompt": "a poster reading \"SALE 50% OFF\" in bold letters",
298
+ "prepared": "a poster reading \"SALE 50% OFF\" in bold letters || spell: S A L E / 5 0 / O F F",
299
+ "negative": "",
300
+ "ids": [
301
+ 50281,
302
+ 66,
303
+ 20731,
304
+ 4361,
305
+ 346,
306
+ 52,
307
+ 23502,
308
+ 2456,
309
+ 6,
310
+ 21890,
311
+ 3,
312
+ 275,
313
+ 13433,
314
+ 4876,
315
+ 2785,
316
+ 15368,
317
+ 27,
318
+ 322,
319
+ 329,
320
+ 418,
321
+ 444,
322
+ 1227,
323
+ 608,
324
+ 470,
325
+ 1227,
326
+ 473,
327
+ 401,
328
+ 401,
329
+ 50282
330
+ ],
331
+ "neg_ids": [
332
+ 50281,
333
+ 50282
334
+ ],
335
+ "counts": {},
336
+ "find_counts": []
337
+ },
338
+ {
339
+ "prompt": "a neon sign “Luma Studio” on a brick wall",
340
+ "prepared": "a neon sign “Luma Studio” on a brick wall || spell: L u m a / S t u d i o",
341
+ "negative": "",
342
+ "ids": [
343
+ 50281,
344
+ 66,
345
+ 16772,
346
+ 861,
347
+ 773,
348
+ 45,
349
+ 9307,
350
+ 17203,
351
+ 668,
352
+ 327,
353
+ 247,
354
+ 20098,
355
+ 3402,
356
+ 2785,
357
+ 15368,
358
+ 27,
359
+ 418,
360
+ 1484,
361
+ 278,
362
+ 247,
363
+ 1227,
364
+ 322,
365
+ 246,
366
+ 1484,
367
+ 277,
368
+ 891,
369
+ 258,
370
+ 50282
371
+ ],
372
+ "neg_ids": [
373
+ 50281,
374
+ 50282
375
+ ],
376
+ "counts": {},
377
+ "find_counts": []
378
+ },
379
+ {
380
+ "prompt": "a book cover titled \"The Old Man and the Sea\"",
381
+ "prepared": "a book cover titled \"The Old Man and the Sea\" || spell: T h e / O l d / M a n / a n d / t h e / S e a",
382
+ "negative": "",
383
+ "ids": [
384
+ 50281,
385
+ 66,
386
+ 1984,
387
+ 3835,
388
+ 18879,
389
+ 346,
390
+ 510,
391
+ 8937,
392
+ 3083,
393
+ 285,
394
+ 253,
395
+ 11936,
396
+ 3,
397
+ 2785,
398
+ 15368,
399
+ 27,
400
+ 308,
401
+ 288,
402
+ 299,
403
+ 1227,
404
+ 473,
405
+ 298,
406
+ 277,
407
+ 1227,
408
+ 353,
409
+ 247,
410
+ 295,
411
+ 1227,
412
+ 247,
413
+ 295,
414
+ 277,
415
+ 1227,
416
+ 246,
417
+ 288,
418
+ 299,
419
+ 1227,
420
+ 322,
421
+ 299,
422
+ 247,
423
+ 50282
424
+ ],
425
+ "neg_ids": [
426
+ 50281,
427
+ 50282
428
+ ],
429
+ "counts": {},
430
+ "find_counts": []
431
+ },
432
+ {
433
+ "prompt": "a logo for \"Q&A Labs!\" with a fox, without text shadow",
434
+ "prepared": "a logo for \"Q&A Labs!\" with a fox || spell: Q & A / L a b s !",
435
+ "negative": "text shadow",
436
+ "ids": [
437
+ 50281,
438
+ 66,
439
+ 18479,
440
+ 323,
441
+ 346,
442
+ 50,
443
+ 7,
444
+ 34,
445
+ 40840,
446
+ 1476,
447
+ 342,
448
+ 247,
449
+ 30013,
450
+ 2785,
451
+ 15368,
452
+ 27,
453
+ 1165,
454
+ 708,
455
+ 329,
456
+ 1227,
457
+ 418,
458
+ 247,
459
+ 270,
460
+ 256,
461
+ 2195,
462
+ 50282
463
+ ],
464
+ "neg_ids": [
465
+ 50281,
466
+ 1156,
467
+ 12195,
468
+ 50282
469
+ ],
470
+ "counts": {},
471
+ "find_counts": []
472
+ },
473
+ {
474
+ "prompt": "a landscape without any people and no buildings",
475
+ "prepared": "a landscape and",
476
+ "negative": "people, buildings",
477
+ "ids": [
478
+ 50281,
479
+ 66,
480
+ 13016,
481
+ 285,
482
+ 50282
483
+ ],
484
+ "neg_ids": [
485
+ 50281,
486
+ 13174,
487
+ 13,
488
+ 9195,
489
+ 50282
490
+ ],
491
+ "counts": {},
492
+ "find_counts": []
493
+ },
494
+ {
495
+ "prompt": "a bowl of fruit with no bananas",
496
+ "prepared": "a bowl of fruit",
497
+ "negative": "bananas",
498
+ "ids": [
499
+ 50281,
500
+ 66,
501
+ 11636,
502
+ 273,
503
+ 9279,
504
+ 50282
505
+ ],
506
+ "neg_ids": [
507
+ 50281,
508
+ 5568,
509
+ 24288,
510
+ 50282
511
+ ],
512
+ "counts": {},
513
+ "find_counts": []
514
+ },
515
+ {
516
+ "prompt": "a cat, no dog, and a mouse",
517
+ "prepared": "a cat, and a mouse",
518
+ "negative": "dog",
519
+ "ids": [
520
+ 50281,
521
+ 66,
522
+ 5798,
523
+ 13,
524
+ 285,
525
+ 247,
526
+ 6521,
527
+ 50282
528
+ ],
529
+ "neg_ids": [
530
+ 50281,
531
+ 21428,
532
+ 50282
533
+ ],
534
+ "counts": {},
535
+ "find_counts": []
536
+ },
537
+ {
538
+ "prompt": "the one and only unicorn",
539
+ "prepared": "the one and only unicorn",
540
+ "negative": "",
541
+ "ids": [
542
+ 50281,
543
+ 783,
544
+ 581,
545
+ 285,
546
+ 760,
547
+ 440,
548
+ 41257,
549
+ 50282
550
+ ],
551
+ "neg_ids": [
552
+ 50281,
553
+ 50282
554
+ ],
555
+ "counts": {},
556
+ "find_counts": []
557
+ },
558
+ {
559
+ "prompt": "no one is here, an empty street",
560
+ "prepared": "an empty street",
561
+ "negative": "one is here",
562
+ "ids": [
563
+ 50281,
564
+ 266,
565
+ 6325,
566
+ 6406,
567
+ 50282
568
+ ],
569
+ "neg_ids": [
570
+ 50281,
571
+ 531,
572
+ 310,
573
+ 1060,
574
+ 50282
575
+ ],
576
+ "counts": {},
577
+ "find_counts": []
578
+ },
579
+ {
580
+ "prompt": "a dozen eggs in a carton",
581
+ "prepared": "a dozen eggs in a carton",
582
+ "negative": "",
583
+ "ids": [
584
+ 50281,
585
+ 66,
586
+ 13365,
587
+ 11624,
588
+ 275,
589
+ 247,
590
+ 7281,
591
+ 251,
592
+ 50282
593
+ ],
594
+ "neg_ids": [
595
+ 50281,
596
+ 50282
597
+ ],
598
+ "counts": {
599
+ "2": 12.0
600
+ },
601
+ "find_counts": [
602
+ [
603
+ 2,
604
+ 7,
605
+ 12
606
+ ]
607
+ ]
608
+ },
609
+ {
610
+ "prompt": "a pair of shoes on the floor",
611
+ "prepared": "a pair of shoes on the floor",
612
+ "negative": "",
613
+ "ids": [
614
+ 50281,
615
+ 66,
616
+ 4667,
617
+ 273,
618
+ 12682,
619
+ 327,
620
+ 253,
621
+ 5254,
622
+ 50282
623
+ ],
624
+ "neg_ids": [
625
+ 50281,
626
+ 50282
627
+ ],
628
+ "counts": {
629
+ "2": 2.0
630
+ },
631
+ "find_counts": [
632
+ [
633
+ 2,
634
+ 6,
635
+ 2
636
+ ]
637
+ ]
638
+ },
639
+ {
640
+ "prompt": "a clock showing 3 pm on a wall",
641
+ "prepared": "a clock showing 3 pm on a wall",
642
+ "negative": "",
643
+ "ids": [
644
+ 50281,
645
+ 66,
646
+ 8886,
647
+ 4645,
648
+ 495,
649
+ 12920,
650
+ 327,
651
+ 247,
652
+ 3402,
653
+ 50282
654
+ ],
655
+ "neg_ids": [
656
+ 50281,
657
+ 50282
658
+ ],
659
+ "counts": {},
660
+ "find_counts": []
661
+ },
662
+ {
663
+ "prompt": "24 hours of rain over 5 houses",
664
+ "prepared": "24 hours of rain over five houses",
665
+ "negative": "",
666
+ "ids": [
667
+ 50281,
668
+ 1348,
669
+ 3038,
670
+ 273,
671
+ 9313,
672
+ 689,
673
+ 2620,
674
+ 9910,
675
+ 50282
676
+ ],
677
+ "neg_ids": [
678
+ 50281,
679
+ 50282
680
+ ],
681
+ "counts": {
682
+ "6": 5.0
683
+ },
684
+ "find_counts": [
685
+ [
686
+ 22,
687
+ 26,
688
+ 5
689
+ ]
690
+ ]
691
+ },
692
+ {
693
+ "prompt": "twelve roses in a\tvase\n",
694
+ "prepared": "twelve roses in a vase",
695
+ "negative": "",
696
+ "ids": [
697
+ 50281,
698
+ 7553,
699
+ 11071,
700
+ 38910,
701
+ 275,
702
+ 247,
703
+ 362,
704
+ 511,
705
+ 50282
706
+ ],
707
+ "neg_ids": [
708
+ 50281,
709
+ 50282
710
+ ],
711
+ "counts": {
712
+ "1": 12.0,
713
+ "2": 12.0
714
+ },
715
+ "find_counts": [
716
+ [
717
+ 0,
718
+ 6,
719
+ 12
720
+ ]
721
+ ]
722
+ },
723
+ {
724
+ "prompt": "a cake with 7 candles and 21 balloons",
725
+ "prepared": "a cake with seven candles and 21 balloons",
726
+ "negative": "",
727
+ "ids": [
728
+ 50281,
729
+ 66,
730
+ 15221,
731
+ 342,
732
+ 5093,
733
+ 36555,
734
+ 285,
735
+ 3127,
736
+ 50210,
737
+ 50282
738
+ ],
739
+ "neg_ids": [
740
+ 50281,
741
+ 50282
742
+ ],
743
+ "counts": {
744
+ "4": 7.0,
745
+ "7": 21.0
746
+ },
747
+ "find_counts": [
748
+ [
749
+ 12,
750
+ 17,
751
+ 7
752
+ ],
753
+ [
754
+ 30,
755
+ 32,
756
+ 21
757
+ ]
758
+ ]
759
+ },
760
+ {
761
+ "prompt": "one apple, two pears and four plums",
762
+ "prepared": "one apple, two pears and four plums",
763
+ "negative": "",
764
+ "ids": [
765
+ 50281,
766
+ 531,
767
+ 19126,
768
+ 13,
769
+ 767,
770
+ 268,
771
+ 6364,
772
+ 285,
773
+ 1740,
774
+ 499,
775
+ 7640,
776
+ 50282
777
+ ],
778
+ "neg_ids": [
779
+ 50281,
780
+ 50282
781
+ ],
782
+ "counts": {
783
+ "1": 1.0,
784
+ "4": 2.0,
785
+ "8": 4.0
786
+ },
787
+ "find_counts": [
788
+ [
789
+ 0,
790
+ 3,
791
+ 1
792
+ ],
793
+ [
794
+ 11,
795
+ 14,
796
+ 2
797
+ ],
798
+ [
799
+ 25,
800
+ 29,
801
+ 4
802
+ ]
803
+ ]
804
+ },
805
+ {
806
+ "prompt": " leading and trailing spaces ",
807
+ "prepared": "leading and trailing spaces",
808
+ "negative": "",
809
+ "ids": [
810
+ 50281,
811
+ 16378,
812
+ 285,
813
+ 28417,
814
+ 8470,
815
+ 50282
816
+ ],
817
+ "neg_ids": [
818
+ 50281,
819
+ 50282
820
+ ],
821
+ "counts": {},
822
+ "find_counts": []
823
+ },
824
+ {
825
+ "prompt": "{\"high_level_description\": \"a flat logo\", \"text\": \"no cast shadow\"}",
826
+ "prepared": "{\"high_level_description\": \"a flat logo\", \"text\": \"no cast shadow\"}",
827
+ "negative": "",
828
+ "ids": [
829
+ 50281,
830
+ 9819,
831
+ 8656,
832
+ 64,
833
+ 5251,
834
+ 64,
835
+ 10008,
836
+ 1381,
837
+ 346,
838
+ 66,
839
+ 6507,
840
+ 18479,
841
+ 995,
842
+ 346,
843
+ 1156,
844
+ 1381,
845
+ 346,
846
+ 2369,
847
+ 5248,
848
+ 12195,
849
+ 986,
850
+ 50282
851
+ ],
852
+ "neg_ids": [
853
+ 50281,
854
+ 50282
855
+ ],
856
+ "counts": {},
857
+ "find_counts": []
858
+ },
859
+ {
860
+ "prompt": "a sign \"unclosed quote with three words",
861
+ "prepared": "a sign \"unclosed quote with three words",
862
+ "negative": "",
863
+ "ids": [
864
+ 50281,
865
+ 66,
866
+ 861,
867
+ 346,
868
+ 328,
869
+ 13784,
870
+ 14430,
871
+ 342,
872
+ 1264,
873
+ 3000,
874
+ 50282
875
+ ],
876
+ "neg_ids": [
877
+ 50281,
878
+ 50282
879
+ ],
880
+ "counts": {
881
+ "8": 3.0
882
+ },
883
+ "find_counts": [
884
+ [
885
+ 28,
886
+ 33,
887
+ 3
888
+ ]
889
+ ]
890
+ },
891
+ {
892
+ "prompt": "a sticker that says \"hi\" and a sticker that says \"bye\" and one that says \"ok\" and \"four\"",
893
+ "prepared": "a sticker that says \"hi\" and a sticker that says \"bye\" and one that says \"ok\" and \"four\" || spell: h i ; b y e ; o k",
894
+ "negative": "",
895
+ "ids": [
896
+ 50281,
897
+ 66,
898
+ 7356,
899
+ 254,
900
+ 326,
901
+ 2296,
902
+ 346,
903
+ 5801,
904
+ 3,
905
+ 285,
906
+ 247,
907
+ 7356,
908
+ 254,
909
+ 326,
910
+ 2296,
911
+ 346,
912
+ 17034,
913
+ 3,
914
+ 285,
915
+ 581,
916
+ 326,
917
+ 2296,
918
+ 346,
919
+ 536,
920
+ 3,
921
+ 285,
922
+ 346,
923
+ 12496,
924
+ 3,
925
+ 2785,
926
+ 15368,
927
+ 27,
928
+ 288,
929
+ 891,
930
+ 3706,
931
+ 270,
932
+ 340,
933
+ 299,
934
+ 3706,
935
+ 258,
936
+ 465,
937
+ 50282
938
+ ],
939
+ "neg_ids": [
940
+ 50281,
941
+ 50282
942
+ ],
943
+ "counts": {
944
+ "19": 1.0
945
+ },
946
+ "find_counts": [
947
+ [
948
+ 59,
949
+ 62,
950
+ 1
951
+ ]
952
+ ]
953
+ },
954
+ {
955
+ "prompt": "a café in Zürich with 2 bicycles",
956
+ "prepared": "a café in Zürich with two bicycles",
957
+ "negative": "",
958
+ "ids": [
959
+ 50281,
960
+ 66,
961
+ 36241,
962
+ 275,
963
+ 1503,
964
+ 3090,
965
+ 5969,
966
+ 342,
967
+ 767,
968
+ 19151,
969
+ 5005,
970
+ 50282
971
+ ],
972
+ "neg_ids": [
973
+ 50281,
974
+ 50282
975
+ ],
976
+ "counts": {
977
+ "8": 2.0
978
+ },
979
+ "find_counts": [
980
+ [
981
+ 22,
982
+ 25,
983
+ 2
984
+ ]
985
+ ]
986
+ },
987
+ {
988
+ "prompt": "an emoji-style icon of a rocket 🚀 with three stars",
989
+ "prepared": "an emoji-style icon of a rocket 🚀 with three stars",
990
+ "negative": "",
991
+ "ids": [
992
+ 50281,
993
+ 266,
994
+ 802,
995
+ 80,
996
+ 8020,
997
+ 14,
998
+ 4826,
999
+ 10651,
1000
+ 273,
1001
+ 247,
1002
+ 21912,
1003
+ 22692,
1004
+ 237,
1005
+ 211,
1006
+ 342,
1007
+ 1264,
1008
+ 6114,
1009
+ 50282
1010
+ ],
1011
+ "neg_ids": [
1012
+ 50281,
1013
+ 50282
1014
+ ],
1015
+ "counts": {
1016
+ "15": 3.0
1017
+ },
1018
+ "find_counts": [
1019
+ [
1020
+ 39,
1021
+ 44,
1022
+ 3
1023
+ ]
1024
+ ]
1025
+ },
1026
+ {
1027
+ "prompt": "a banner \"Ünïcödé Tëxt\" above five flags",
1028
+ "prepared": "a banner \"Ünïcödé Tëxt\" above five flags || spell: Ü n ï c ö d é / T ë x t",
1029
+ "negative": "",
1030
+ "ids": [
1031
+ 50281,
1032
+ 66,
1033
+ 29751,
1034
+ 346,
1035
+ 127,
1036
+ 239,
1037
+ 79,
1038
+ 12589,
1039
+ 68,
1040
+ 25873,
1041
+ 860,
1042
+ 308,
1043
+ 10093,
1044
+ 633,
1045
+ 3,
1046
+ 1840,
1047
+ 2620,
1048
+ 12201,
1049
+ 2785,
1050
+ 15368,
1051
+ 27,
1052
+ 37995,
1053
+ 295,
1054
+ 1325,
1055
+ 109,
1056
+ 260,
1057
+ 16919,
1058
+ 277,
1059
+ 3974,
1060
+ 1227,
1061
+ 308,
1062
+ 41254,
1063
+ 1269,
1064
+ 246,
1065
+ 50282
1066
+ ],
1067
+ "neg_ids": [
1068
+ 50281,
1069
+ 50282
1070
+ ],
1071
+ "counts": {
1072
+ "16": 5.0
1073
+ },
1074
+ "find_counts": [
1075
+ [
1076
+ 30,
1077
+ 34,
1078
+ 5
1079
+ ]
1080
+ ]
1081
+ },
1082
+ {
1083
+ "prompt": "ten ten ten",
1084
+ "prepared": "ten ten ten",
1085
+ "negative": "",
1086
+ "ids": [
1087
+ 50281,
1088
+ 1866,
1089
+ 3578,
1090
+ 3578,
1091
+ 50282
1092
+ ],
1093
+ "neg_ids": [
1094
+ 50281,
1095
+ 50282
1096
+ ],
1097
+ "counts": {
1098
+ "1": 10.0,
1099
+ "2": 10.0
1100
+ },
1101
+ "find_counts": [
1102
+ [
1103
+ 0,
1104
+ 3,
1105
+ 10
1106
+ ],
1107
+ [
1108
+ 4,
1109
+ 7,
1110
+ 10
1111
+ ]
1112
+ ]
1113
+ },
1114
+ {
1115
+ "prompt": "Two Red Apples And A Green Pear On A Plate",
1116
+ "prepared": "two red apples and a green pear on a plate",
1117
+ "negative": "",
1118
+ "ids": [
1119
+ 50281,
1120
+ 9389,
1121
+ 2502,
1122
+ 28580,
1123
+ 285,
1124
+ 247,
1125
+ 4759,
1126
+ 27887,
1127
+ 327,
1128
+ 247,
1129
+ 5340,
1130
+ 50282
1131
+ ],
1132
+ "neg_ids": [
1133
+ 50281,
1134
+ 50282
1135
+ ],
1136
+ "counts": {
1137
+ "1": 2.0
1138
+ },
1139
+ "find_counts": [
1140
+ [
1141
+ 0,
1142
+ 3,
1143
+ 2
1144
+ ]
1145
+ ]
1146
+ },
1147
+ {
1148
+ "prompt": "a cat without a hat",
1149
+ "prepared": "a cat",
1150
+ "negative": "hat",
1151
+ "ids": [
1152
+ 50281,
1153
+ 66,
1154
+ 5798,
1155
+ 50282
1156
+ ],
1157
+ "neg_ids": [
1158
+ 50281,
1159
+ 700,
1160
+ 50282
1161
+ ],
1162
+ "counts": {},
1163
+ "find_counts": []
1164
+ },
1165
+ {
1166
+ "prompt": "",
1167
+ "prepared": "",
1168
+ "negative": "",
1169
+ "ids": [
1170
+ 50281,
1171
+ 50282
1172
+ ],
1173
+ "neg_ids": [
1174
+ 50281,
1175
+ 50282
1176
+ ],
1177
+ "counts": {},
1178
+ "find_counts": []
1179
+ },
1180
+ {
1181
+ "prompt": "a portrait of a woman, no glasses. soft light",
1182
+ "prepared": "a portrait of a woman. soft light",
1183
+ "negative": "glasses",
1184
+ "ids": [
1185
+ 50281,
1186
+ 66,
1187
+ 22946,
1188
+ 273,
1189
+ 247,
1190
+ 3416,
1191
+ 15,
1192
+ 2602,
1193
+ 1708,
1194
+ 50282
1195
+ ],
1196
+ "neg_ids": [
1197
+ 50281,
1198
+ 25483,
1199
+ 265,
1200
+ 50282
1201
+ ],
1202
+ "counts": {},
1203
+ "find_counts": []
1204
+ },
1205
+ {
1206
+ "prompt": "the word \"Agate\" in red serif letters, 2 lines",
1207
+ "prepared": "the word \"Agate\" in red serif letters, two lines || spell: A g a t e",
1208
+ "negative": "",
1209
+ "ids": [
1210
+ 50281,
1211
+ 783,
1212
+ 3159,
1213
+ 346,
1214
+ 8903,
1215
+ 366,
1216
+ 3,
1217
+ 275,
1218
+ 2502,
1219
+ 1151,
1220
+ 338,
1221
+ 4876,
1222
+ 13,
1223
+ 767,
1224
+ 3104,
1225
+ 2785,
1226
+ 15368,
1227
+ 27,
1228
+ 329,
1229
+ 305,
1230
+ 247,
1231
+ 246,
1232
+ 299,
1233
+ 50282
1234
+ ],
1235
+ "neg_ids": [
1236
+ 50281,
1237
+ 50282
1238
+ ],
1239
+ "counts": {
1240
+ "13": 2.0
1241
+ },
1242
+ "find_counts": [
1243
+ [
1244
+ 39,
1245
+ 42,
1246
+ 2
1247
+ ]
1248
+ ]
1249
+ }
1250
+ ]
1251
+ }