Tsmith2024 commited on
Commit
399fbe5
·
verified ·
1 Parent(s): 36df6fb

Copy of Logolabs/agate-webgpu with an optional full SD-VAE decoder (?vae=full)

Browse files
README.md CHANGED
@@ -1,10 +1,113 @@
1
- ---
2
- title: Agate Webgpu Fullvae
3
- emoji: 🐢
4
- colorFrom: pink
5
- colorTo: indigo
6
- sdk: static
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Agate WebGPU
3
+ colorFrom: red
4
+ colorTo: gray
5
+ sdk: static
6
+ app_file: index.html
7
+ pinned: false
8
+ license: mit
9
+ short_description: Agate text-to-image, 100% in your browser on WebGPU
10
+ custom_headers:
11
+ cross-origin-embedder-policy: require-corp
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
+ **Why we built Agate.** Agate is LogoLabs' search for the best architecture for small-scale image generation, as
29
+ groundwork for glyph and symbol generation: we want to generate vector-native fonts to go with the wordmarks we make at
30
+ LogoLabs. We trained a general text-to-image model because general image generation is a good proxy for how an
31
+ architecture performs on those tasks, and it is very benchmark-friendly (GenEval, Qwen-Image-Bench, FID), unlike tasks
32
+ such as SDF generation, which have almost no established benchmarks. Next, we plan to build on it to generate SVGs with
33
+ our vectoriser, Inkvec, and to improve Inkvec's prior with flow matching to help trace the fonts. We decided to release
34
+ Agate to share these architectural efforts.
35
+
36
+ ## Models
37
+
38
+ Pick a model at the top of the page. A first visit gets Preview 003; the page remembers your last choice in
39
+ this browser.
40
+
41
+ | model | resolution | notes | download |
42
+ |---|---|---|---|
43
+ | [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) |
44
+ | [Preview 002](https://huggingface.co/Logolabs/agate-preview-002) | 256 px | same network as 001, trained longer | 530 MB |
45
+ | [Preview 001](https://huggingface.co/Logolabs/agate-preview-001) | 256 px | the first preview | 530 MB |
46
+
47
+ Each model is downloaded once and kept in the browser's Cache Storage. The files live in each release's model repo
48
+ under `webgpu/v2/` (e.g. [Logolabs/agate-preview-003/webgpu/v2](https://huggingface.co/Logolabs/agate-preview-003/tree/main/webgpu/v2)),
49
+ with a `manifest.json` holding sizes and sha256s (the cache keys). `models/` in this Space is the original 001 build
50
+ (without the plan output), kept for `?models=./models/`.
51
+
52
+ ## Show thinker
53
+
54
+ Agate's thinker lays the picture out on a 16 × 16 grid (the plan) that steers the renderer. With **Show thinker**
55
+ (on by default; it does not slow sampling: previews are read back asynchronously and coloured in a Web Worker) the page shows, at every step, the plan of the conditional branch as
56
+ colours and the image the model currently expects (x₁ = z + (1 − t)·v, with the SD latent→RGB approximation) —
57
+ the same live preview as the [ComfyUI nodes](https://github.com/logolabs/agate-comfyui): the plan's cells are
58
+ projected on its top three principal components, fitted at the first step with the 2% / 98% quantiles fixed then,
59
+ so a colour keeps its meaning while the plan evolves.
60
+
61
+ ## Requirements
62
+
63
+ A browser with WebGPU: a recent Chrome or Edge (113+). Safari and Firefox may lack WebGPU or need it enabled in
64
+ their settings; without it the page offers a WebAssembly (CPU) path that works but takes minutes per image.
65
+
66
+ ## Speed and accuracy (RTX 4060 laptop, Edge, WebGPU, warm, 50 steps)
67
+
68
+ | model | per step (CFG batch of 2) | image | before (2026-09-29 morning) |
69
+ |---|---|---|---|
70
+ | 003 at 512 px | ~161 ms | **~8.1 s** (Fast, 25 steps: ~4.1 s) | 275 ms, 14 s |
71
+ | 003 at 256 px | ~70 ms | ~3.5 s | 120 ms, 6.1 s |
72
+ | 002 / 001 | ~65 ms | ~3.3 s | 105 ms, 5.3 s |
73
+
74
+ How: each step is one graph run that also does the CFG mix and the Euler update, so the latent never leaves the
75
+ GPU between steps (the only readback is the final latent); the network computes in **fp16** (WebGPU `shader-f16`;
76
+ WGSL has no bf16), with fp32 kept for the graph I/O, the CFG/Euler arithmetic, the timestep / resolution / count
77
+ embeddings (sin(1000·t·f) needs fp32) and `Range`; the transposed-conv upsamplers run as 1x1 conv + depth-to-space.
78
+ The thinker preview never blocks sampling: its tensors are downloaded asynchronously, coloured in a Web Worker,
79
+ and frames are dropped while one is pending (measured cost: none).
80
+
81
+ fp16 compute moves the result slightly: against the fp32 PyTorch reference with the same initial noise, 001 is
82
+ within 16/255 (PSNR 48 dB), 002 36 dB, 003 at 256 px 42 dB and 003 at 512 px 31 dB (mean 0.6-1.8/255). For scale,
83
+ the Python package's own CUDA path (bf16) is 14 dB from that fp32 reference for the same noise. Run `?parity=1`.
84
+ Seeds use a JavaScript PRNG, so a seed gives the same image in every browser, but not the same image as the
85
+ PyTorch pipeline with that seed. The Python packages decode with the full SD-VAE by default; this page uses TAESD.
86
+
87
+ ## AI-generated content marking
88
+
89
+ Every image the page makes is marked as AI-generated (EU AI Act Art. 50(2)), with the same marks as the Python
90
+ packages:
91
+
92
+ - an invisible watermark in the pixels: [invisible-watermark](https://github.com/ShieldMnt/invisible-watermark)'s
93
+ `dwtDctSvd` method, ported to JavaScript bit for bit, with the 64-bit payload `AGATE` + release
94
+ (`AGATE003`, ...). `agate.detect_watermark(img)` from the model packages reads it;
95
+ - in the saved PNG: text fields `ai_generated`, `generator`, `model` and `watermark`. The prompt is not written;
96
+ - a visible "AI-generated" note next to each result.
97
+
98
+ Neither mark is tamper-proof: screenshots and re-encodes drop the metadata, and heavy edits can remove the
99
+ watermark. If you publish images made here, label them as AI-generated.
100
+
101
+ Query parameters: `?v=001|002|003` picks a model, `?res=256` picks 003's resolution, `?autoload=1` loads the
102
+ model immediately, `?ep=wasm` forces the CPU backend, `?parity=1` runs the parity self-test, `?golden=1` checks
103
+ 003's prompt pipeline against the Python package's output (38 prompts).
104
+
105
+ ## Links
106
+
107
+ - [logolabs.org](https://logolabs.org)
108
+ - [huggingface.co/Logolabs](https://huggingface.co/Logolabs)
109
+
110
+ Licence: MIT.
111
+
112
+ We acknowledge EuroHPC JU for awarding the project ID EHPC-AIF-2026PG01-907 access to resources on
113
+ Arrhenius GPU at NAISS, Sweden.
assets/agate-a.svg ADDED
assets/agate-lockup.svg ADDED
assets/agate-wordmark.svg ADDED
assets/logolabs-mark.svg ADDED
css/style.css ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --paper: #FCFCF7;
3
+ --ink: #0B0A08;
4
+ --red: #F4291F;
5
+ --mute: #6B6862;
6
+ --hair: 1.5px solid var(--ink);
7
+ --mono: "IBM Plex Mono", ui-monospace, Menlo, Consolas, monospace;
8
+ --sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
9
+ --head: "Inter Tight", "Inter", system-ui, sans-serif;
10
+ }
11
+ * { box-sizing: border-box; }
12
+ [hidden] { display: none !important; }
13
+ html, body { margin: 0; background: var(--paper); color: var(--ink); }
14
+ body { font: 16px/1.5 var(--sans); -webkit-font-smoothing: antialiased; }
15
+ a { color: inherit; text-decoration-thickness: 1.5px; text-underline-offset: 3px; }
16
+ a:hover { color: var(--red); }
17
+
18
+ .page { max-width: 1080px; margin: 0 auto; padding: 28px 24px 40px; }
19
+ .rule { border: 0; border-top: var(--hair); margin: 22px 0; }
20
+
21
+ .label { font: 500 11.5px/1.3 var(--mono); letter-spacing: .14em; text-transform: uppercase; display: inline-flex; align-items: center; gap: 8px; }
22
+ .label.dim { color: var(--mute); }
23
+ .label b { font-weight: 600; color: var(--red); letter-spacing: .06em; }
24
+ .sq { display: inline-block; width: 8px; height: 8px; background: var(--ink); flex: none; }
25
+ .sq.red { background: var(--red); }
26
+
27
+ .mast { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px; }
28
+ .mast-left { display: flex; align-items: flex-end; gap: 22px; }
29
+ .lockup { height: 96px; width: auto; display: block; }
30
+ .mast-meta { display: flex; flex-direction: column; gap: 6px; padding-bottom: 6px; }
31
+ .imprint { display: flex; align-items: center; gap: 10px; text-decoration: none; padding-bottom: 6px; }
32
+
33
+ .intro h1 { font: 800 clamp(34px, 5.4vw, 58px)/1.02 var(--head); letter-spacing: -.02em; margin: 6px 0 12px; }
34
+ .intro p { max-width: 62ch; margin: 0; color: #2a2824; }
35
+
36
+ .notice { border: var(--hair); padding: 16px 18px; margin: 22px 0 0; background: #fff; }
37
+ .notice p { margin: 8px 0; }
38
+ .notice.ok { border-color: var(--ink); }
39
+
40
+ .grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 28px; margin-top: 26px; }
41
+ @media (max-width: 820px) { .grid { grid-template-columns: 1fr; } .lockup { height: 72px; } }
42
+
43
+ .panel { border-top: var(--hair); padding-top: 14px; }
44
+ textarea, input[type=number] {
45
+ width: 100%; font: 16px/1.45 var(--sans); color: var(--ink); background: #fff;
46
+ border: var(--hair); border-radius: 0; padding: 10px 12px; margin-top: 8px; resize: vertical;
47
+ }
48
+ textarea:focus, input:focus { outline: 2px solid var(--red); outline-offset: 1px; }
49
+
50
+ .examples { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 10px 0 18px; }
51
+ .chip { font: 13px/1.2 var(--sans); background: transparent; border: 1px solid var(--ink); padding: 5px 9px; cursor: pointer; color: var(--ink); text-align: left; }
52
+ .chip:hover { background: var(--ink); color: var(--paper); }
53
+
54
+ .row { display: grid; grid-template-columns: 1.1fr 1fr 1fr; gap: 16px; margin-bottom: 20px; }
55
+ @media (max-width: 480px) { .row { grid-template-columns: 1fr; } }
56
+ .field { display: flex; flex-direction: column; }
57
+ .seed-row { display: flex; gap: 6px; align-items: stretch; }
58
+ .seed-row input { margin-top: 8px; }
59
+ input[type=range] { width: 100%; margin-top: 16px; accent-color: var(--red); }
60
+
61
+ .btn { font: 600 13px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase; border: var(--hair); background: var(--paper); color: var(--ink); padding: 14px 18px; cursor: pointer; border-radius: 0; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }
62
+ .btn.primary { width: 100%; background: var(--ink); color: var(--paper); }
63
+ .btn.primary:hover:not(:disabled) { background: var(--red); border-color: var(--red); }
64
+ .btn:disabled { opacity: .45; cursor: default; }
65
+ .btn.icon { margin-top: 8px; padding: 0 12px; font-size: 18px; letter-spacing: 0; }
66
+ .btn.ghost:hover { background: var(--ink); color: var(--paper); }
67
+ .btn.small { padding: 9px 12px; font-size: 11px; }
68
+
69
+ .progress { margin-top: 16px; }
70
+ .bar { height: 8px; border: var(--hair); background: #fff; }
71
+ #bar-fill { height: 100%; width: 0; background: var(--red); transition: width .12s linear; }
72
+ .progress-text { display: flex; justify-content: space-between; margin-top: 8px; gap: 12px; }
73
+
74
+ .out-head, .out-foot { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
75
+ .out-foot { align-items: flex-start; margin-top: 12px; }
76
+ .toggle { cursor: pointer; }
77
+ .toggle input { accent-color: var(--red); margin: 0; }
78
+ .frame { position: relative; margin-top: 10px; border: var(--hair); background: #fff; aspect-ratio: 1 / 1; width: 100%; }
79
+ #canvas { width: 100%; height: 100%; display: block; image-rendering: auto; }
80
+ .frame.crisp #canvas { image-rendering: pixelated; }
81
+ .placeholder { position: absolute; inset: 0; display: grid; place-items: center; background: var(--paper); }
82
+ .placeholder img { width: 28%; opacity: .14; }
83
+ .frame.busy .placeholder img { animation: pulse 1.2s ease-in-out infinite; }
84
+ @keyframes pulse { 50% { opacity: .32; } }
85
+
86
+ .timings { display: grid; grid-template-columns: auto auto; gap: 2px 14px; margin: 0; font: 12px/1.5 var(--mono); }
87
+ .timings dt { color: var(--mute); text-transform: uppercase; letter-spacing: .1em; font-size: 11px; }
88
+ .timings dd { margin: 0; }
89
+
90
+ .foot p { margin: 0 0 8px; }
91
+ .foot .strong { font: 700 18px/1.3 var(--head); }
92
+ .foot .label { display: flex; flex-wrap: wrap; gap: 4px 8px; }
93
+ @media (max-width: 520px) {
94
+ .page { padding: 20px 16px 32px; }
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); }
133
+ .label .toggle.fast { margin-left: 10px; text-transform: uppercase; letter-spacing: .1em; color: var(--ink); }
134
+ .label .toggle.fast input { accent-color: var(--red); margin: 0 4px 0 0; vertical-align: -1px; }
index.html CHANGED
@@ -1,19 +1,147 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
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>
11
+ <link crossorigin="anonymous" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&family=Inter+Tight:wght@700;800&display=swap" rel="stylesheet">
12
+ <link rel="stylesheet" href="css/style.css">
13
+ </head>
14
+ <body>
15
+ <div class="page">
16
+ <header class="mast">
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">
25
+ <img src="assets/logolabs-mark.svg" alt="" width="22" height="30">
26
+ <span class="label">LogoLabs</span>
27
+ </a>
28
+ </header>
29
+ <hr class="rule">
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>
44
+ <span class="label"><i class="sq red"></i>WebGPU unavailable</span>
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
+
52
+ <main class="grid">
53
+ <section class="panel controls">
54
+ <label class="label" for="prompt">Prompt</label>
55
+ <textarea id="prompt" rows="3" spellcheck="false">a green teapot and a red cup on a table</textarea>
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">
64
+ <label class="label" for="seed">Seed</label>
65
+ <div class="seed-row">
66
+ <input id="seed" type="number" min="0" max="4294967295" step="1" value="0">
67
+ <button id="dice" class="btn icon" title="Random seed" aria-label="Random seed">⟳</button>
68
+ </div>
69
+ </div>
70
+ <div class="field">
71
+ <label class="label" for="steps">Steps <b id="steps-v">50</b>
72
+ <span class="toggle fast" title="25 steps: about twice as fast; detail holds, the layout can change"><input type="checkbox" id="fast"> Fast</span></label>
73
+ <input id="steps" type="range" min="4" max="50" step="1" value="50">
74
+ </div>
75
+ <div class="field">
76
+ <label class="label" for="cfg">CFG <b id="cfg-v">3.0</b></label>
77
+ <input id="cfg" type="range" min="1" max="8" step="0.5" value="3">
78
+ </div>
79
+ </div>
80
+ <div class="row res-row" id="res-row" hidden>
81
+ <div class="field">
82
+ <span class="label">Resolution</span>
83
+ <div class="seg small" role="radiogroup" id="res"></div>
84
+ </div>
85
+ </div>
86
+
87
+ <button id="go" class="btn primary" disabled>Load model</button>
88
+
89
+ <div class="progress" aria-live="polite">
90
+ <div class="bar"><div id="bar-fill"></div></div>
91
+ <div class="progress-text"><span id="status" class="label">Idle</span><span id="status-r" class="label dim"></span></div>
92
+ </div>
93
+ <p class="prepared" id="prepared" hidden></p>
94
+ </section>
95
+
96
+ <section class="panel output">
97
+ <div class="out-head">
98
+ <span class="label"><i class="sq"></i><span id="out-label">Output · 512 × 512</span></span>
99
+ <span class="toggles">
100
+ <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>
101
+ <label class="label toggle"><input type="checkbox" id="crisp"> Crisp pixels</label>
102
+ </span>
103
+ </div>
104
+ <div class="frame" id="frame">
105
+ <canvas id="canvas" width="512" height="512"></canvas>
106
+ <div class="placeholder" id="placeholder">
107
+ <img src="assets/agate-a.svg" alt="">
108
+ </div>
109
+ </div>
110
+ <div class="thinker" id="thinker" hidden>
111
+ <figure><canvas id="plan-canvas" width="16" height="16"></canvas>
112
+ <figcaption class="label dim">Thinker plan · 16 × 16</figcaption></figure>
113
+ <figure><canvas id="pred-canvas" width="32" height="32"></canvas>
114
+ <figcaption class="label dim">Expected · <span id="thinker-step">0 / 0</span></figcaption></figure>
115
+ <p class="thinker-note">The thinker lays the picture out on a 16 × 16 grid before the renderer paints it.
116
+ Colours are the plan's three main directions (fixed at the first step, as in the ComfyUI nodes); the
117
+ expected image is x₁ = z + (1 − t)·v, shown with the SD latent→RGB approximation.</p>
118
+ </div>
119
+ <p class="ai-note" id="ai-note" hidden><i class="sq red"></i><span><b>AI-generated image.</b>
120
+ 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>
121
+ <div class="out-foot">
122
+ <dl class="timings" id="timings"></dl>
123
+ <a id="save" class="btn ghost small" download="agate.png" hidden>Save PNG</a>
124
+ </div>
125
+ </section>
126
+ </main>
127
+
128
+ <div id="parity" class="notice" hidden></div>
129
+
130
+ <hr class="rule">
131
+ <footer class="foot">
132
+ <p class="strong">Runs entirely in your browser on WebGPU — nothing leaves your machine.</p>
133
+ <p class="small-print">Every image made here is marked as AI-generated (EU AI Act Art. 50): an invisible watermark in the
134
+ pixels (invisible-watermark “dwtDctSvd”, payload AGATE + release) and, in the saved PNG, text fields
135
+ <code>ai_generated</code>, <code>generator</code>, <code>model</code>. Neither survives every edit; please label AI images you publish.</p>
136
+ <p class="small-print">Why Agate: LogoLabs' search for the best small-scale image architecture, as groundwork for glyph and symbol generation and vector-native fonts to go with our wordmarks. General image generation served as a benchmark-friendly proxy for those tasks (unlike, e.g., SDF generation); next it feeds SVG generation with Inkvec. Released to share these architectural efforts.</p>
137
+ <p class="label dim">
138
+ <a href="https://logolabs.org" target="_blank" rel="noopener">logolabs.org</a> ·
139
+ <a href="https://huggingface.co/Logolabs" target="_blank" rel="noopener">huggingface.co/Logolabs</a> ·
140
+ MIT licence · <span id="backend">backend: —</span>
141
+ </p>
142
+ </footer>
143
+ </div>
144
+ <script src="js/config.js"></script>
145
+ <script type="module" src="js/app.js"></script>
146
+ </body>
147
+ </html>
js/agate.js ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/v2/`; // v2: fast fp16 step graphs
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
+ // ---- optional full SD-VAE decoder (?vae=full) ----------------------------------------------------
28
+ const USE_FULL_VAE = new URLSearchParams(globalThis.location?.search || "").get("vae") === "full";
29
+ const FULL_VAE_URL = "https://huggingface.co/axodoxian/stable_diffusion_onnx/resolve/main/vae_decoder/model.onnx";
30
+ const VAE_SCALE = 0.18215; // config.json: vae_scale. The full VAE wants latents divided by this.
31
+
32
+ const _f32 = new Float32Array(1), _u32 = new Uint32Array(_f32.buffer);
33
+ function f32ToHalfBits(a) {
34
+ const out = new Uint16Array(a.length);
35
+ for (let i = 0; i < a.length; i++) {
36
+ _f32[0] = a[i]; const x = _u32[0];
37
+ const sign = (x >>> 16) & 0x8000, ex = (x >>> 23) & 0xff, m = x & 0x7fffff;
38
+ const e = ex - 127 + 15;
39
+ if (e <= 0) out[i] = e < -10 ? sign : sign | ((m | 0x800000) >> (14 - e));
40
+ else if (e >= 31) out[i] = sign | 0x7c00 | (ex === 0xff && m ? 0x200 : 0);
41
+ else out[i] = sign | (e << 10) | (m >> 13);
42
+ }
43
+ return out;
44
+ }
45
+ function halfBitsToF32(u) {
46
+ const out = new Float32Array(u.length);
47
+ for (let i = 0; i < u.length; i++) {
48
+ const h = u[i], s = (h & 0x8000) ? -1 : 1, e = (h >> 10) & 0x1f, m = h & 0x3ff;
49
+ out[i] = e === 0 ? s * Math.pow(2, -14) * (m / 1024) : e === 31 ? (m ? NaN : s * Infinity) : s * Math.pow(2, e - 15) * (1 + m / 1024);
50
+ }
51
+ return out;
52
+ }
53
+ const toHalf = (f32) => (typeof Float16Array !== "undefined" ? Float16Array.from(f32) : f32ToHalfBits(f32));
54
+
55
+ export async function webgpuStatus() {
56
+ if (!("gpu" in navigator)) return { ok: false, why: "This browser does not expose WebGPU (navigator.gpu is missing)." };
57
+ try {
58
+ const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
59
+ if (!adapter) return { ok: false, why: "WebGPU is present but no GPU adapter is available." };
60
+ let name = "";
61
+ try { const info = adapter.info || (await adapter.requestAdapterInfo?.()); name = [info?.vendor, info?.architecture, info?.description].filter(Boolean).join(" "); } catch { /* optional */ }
62
+ return { ok: true, adapter: name, maxBuffer: adapter.limits?.maxBufferSize };
63
+ } catch (e) {
64
+ return { ok: false, why: `WebGPU adapter request failed: ${e.message || e}` };
65
+ }
66
+ }
67
+
68
+ // ---- downloads with progress + Cache Storage ---------------------------------------------------
69
+ async function openCache(name) {
70
+ try { return await caches.open(name); } catch { return null; } // file://, private mode, ...
71
+ }
72
+
73
+ async function fetchCached(cacheName, url, key, expectBytes, onBytes) {
74
+ const cache = await openCache(cacheName);
75
+ if (cache) {
76
+ const hit = await cache.match(key);
77
+ if (hit) { const buf = new Uint8Array(await hit.arrayBuffer()); onBytes(buf.byteLength, true); return { buf, cached: true }; }
78
+ }
79
+ const res = await fetch(url);
80
+ if (!res.ok) throw new Error(`${url}: HTTP ${res.status}`);
81
+ // manifest size first: a compressed response would report the encoded length
82
+ const total = expectBytes || Number(res.headers.get("content-length")) || 0;
83
+ const reader = res.body.getReader();
84
+ let buf = new Uint8Array(total || 1 << 20), n = 0;
85
+ for (;;) {
86
+ const { done, value } = await reader.read();
87
+ if (done) break;
88
+ if (n + value.byteLength > buf.byteLength) { const b2 = new Uint8Array(Math.max(buf.byteLength * 2, n + value.byteLength)); b2.set(buf.subarray(0, n)); buf = b2; }
89
+ buf.set(value, n); n += value.byteLength; onBytes(value.byteLength, false);
90
+ }
91
+ buf = n === buf.byteLength ? buf : buf.slice(0, n);
92
+ if (cache) {
93
+ try { await cache.put(key, new Response(buf, { headers: { "content-type": "application/octet-stream", "content-length": String(n) } })); }
94
+ catch (e) { console.warn("cache put failed (quota?)", e); }
95
+ }
96
+ return { buf, cached: false };
97
+ }
98
+
99
+ export async function hasCachedModel(version = "001") {
100
+ try { const c = await caches.open(VERSIONS[version].cache); return (await c.keys()).length >= 5; } catch { return false; }
101
+ }
102
+
103
+ export async function clearCache() {
104
+ let ok = true;
105
+ for (const v of Object.values(VERSIONS)) { try { ok = (await caches.delete(v.cache)) && ok; } catch { ok = false; } }
106
+ return ok;
107
+ }
108
+
109
+ // The Hub counts a model download only on a request for the model repo's root config.json; the ONNX files
110
+ // come from this Space, so every model load reads the loaded release's config.json once -- the same per-load
111
+ // count a Python from_pretrained() produces, cached weights or not.
112
+ function countLoad(version) { fetch(countUrl(version), { cache: "no-store" }).catch(() => {}); }
113
+
114
+ // ---- the model ---------------------------------------------------------------------------------
115
+ export class Agate {
116
+ // base: the page's models folder (001); override: an explicit models base for every version (local layout)
117
+ constructor({ base = "./models/", override = null, version = "003", ep = "webgpu", variant = null, optLevel = "all" } = {}) {
118
+ this.optLevel = optLevel;
119
+ const slash = (u) => (u.endsWith("/") ? u : u + "/");
120
+ this.version = version;
121
+ this.spec = VERSIONS[version];
122
+ this.base = override ? slash(override) + this.spec.dir : (this.spec.remote || slash(base) + this.spec.dir);
123
+ this.ep = ep;
124
+ this.variant = variant; // e.g. "full16": fp16-compute generator (experimental, 001 only)
125
+ this.marks = marks(version);
126
+ }
127
+
128
+ get mr() { return this.manifest?.arch === "fcdm_t2mr"; }
129
+
130
+ // the fast build (2026-09-29): one Euler step per run, fp16 compute, everything stays on the GPU between steps
131
+ get fast() { return !!this.manifest?.step; }
132
+
133
+ fileKey(f) { return new Request(`${this.base}${f}?sha256=${this.manifest.files[f].sha256}`); }
134
+
135
+ async getFile(f, onBytes = () => {}) {
136
+ const meta = this.manifest.files[f];
137
+ return (await fetchCached(this.spec.cache, this.base + f, this.fileKey(f), meta.bytes, onBytes)).buf;
138
+ }
139
+
140
+ // onProgress({loaded, total, file, phase}); resolution: 003 only (512 default, or 256)
141
+ async load(onProgress = () => {}, resolution = null) {
142
+ const manifest = await (await fetch(this.base + "manifest.json", { cache: "no-cache" })).json();
143
+ this.manifest = manifest;
144
+ const swap = (this.variant && manifest.variants?.[this.variant]) || {};
145
+ const real = (f) => swap[f] || f;
146
+ this.resolution = this.mr ? Number(resolution || manifest.resolution) : 256;
147
+ // 003: download everything (the two graphs are ~1 MB each and share one weights file), so a resolution
148
+ // switch needs no network
149
+ const need = this.mr || manifest.step ? Object.keys(manifest.files) : FILES_T2;
150
+ const total = need.reduce((s, f) => s + manifest.files[real(f)].bytes, 0);
151
+ let loaded = 0, fromCache = 0;
152
+ const bufs = {};
153
+ const t0 = performance.now();
154
+ for (const f of need) {
155
+ const meta = manifest.files[real(f)];
156
+ const { buf } = await fetchCached(this.spec.cache, this.base + real(f), this.fileKey(real(f)), meta.bytes, (b, c) => {
157
+ loaded += b; if (c) fromCache += b; onProgress({ phase: "download", file: f, loaded, total });
158
+ });
159
+ bufs[f] = buf;
160
+ }
161
+ this.stats = { downloadMB: total / 1e6, fromCacheMB: fromCache / 1e6, downloadMs: performance.now() - t0 };
162
+ countLoad(this.version);
163
+ // drop cached files of older builds of this release (keys carry the sha256)
164
+ try {
165
+ const cache = await openCache(this.spec.cache), keep = new Set(need.map((f) => new URL(this.fileKey(real(f)).url, location.href).href));
166
+ if (cache) for (const req of await cache.keys()) if (!keep.has(req.url)) await cache.delete(req);
167
+ } catch (e) { console.warn("cache cleanup", e); }
168
+ const dec = new TextDecoder();
169
+ this.tok = new Tokenizer(JSON.parse(dec.decode(bufs["tokenizer.json"])), JSON.parse(dec.decode(bufs["tokenizer_config.json"])));
170
+
171
+ if (this.ep === "wasm") {
172
+ ort.env.wasm.numThreads = self.crossOriginIsolated ? Math.min(8, navigator.hardwareConcurrency || 4) : 1;
173
+ }
174
+ const t1 = performance.now();
175
+ this.sessions = {};
176
+ for (const [name, f] of [["text", "text_encoder.onnx"], ["vae", "taesd_decoder.onnx"]]) {
177
+ onProgress({ phase: "init", file: f, loaded: total, total });
178
+ const ts = performance.now();
179
+ this.sessions[name] = await ort.InferenceSession.create(bufs[f], this.sessionOptions());
180
+ this.stats[`${name}SessionMs`] = performance.now() - ts;
181
+ bufs[f] = null;
182
+ }
183
+ onProgress({ phase: "init", file: "generator", loaded: total, total });
184
+ const ts = performance.now();
185
+ if (this.fast) {
186
+ this.weights = bufs[manifest.step.external_data]; // kept for a resolution switch (no re-read)
187
+ await this.createGenerator(this.resolution, bufs);
188
+ } else if (this.mr) {
189
+ this.weights = bufs["generator.weights"]; // kept for a resolution switch (no re-read)
190
+ await this.createGenerator(this.resolution, bufs);
191
+ } else {
192
+ this.sessions.gen = await ort.InferenceSession.create(bufs["generator.onnx"], this.sessionOptions());
193
+ }
194
+ this.stats.genSessionMs = performance.now() - ts;
195
+ this.stats.sessionMs = performance.now() - t1;
196
+ if (USE_FULL_VAE) await this.loadFullVae(onProgress);
197
+ return this.stats;
198
+ }
199
+
200
+ // Optional: the full SD-VAE decoder (?vae=full). Falls back to TAESD if anything goes wrong.
201
+ async loadFullVae(onProgress = () => {}) {
202
+ try {
203
+ let got = 0;
204
+ const { buf } = await fetchCached("agate-full-vae", FULL_VAE_URL, new Request(FULL_VAE_URL), 0, (b) => {
205
+ got += b; onProgress({ phase: "download", file: "full VAE", loaded: got, total: 99.1e6 });
206
+ });
207
+ onProgress({ phase: "init", file: "full VAE", loaded: 1, total: 1 });
208
+ this.fullVae = await ort.InferenceSession.create(buf, this.sessionOptions());
209
+ console.info("full VAE ready", this.fullVae.inputNames, this.fullVae.outputNames);
210
+ } catch (e) {
211
+ console.warn("full VAE failed to load, using TAESD", e);
212
+ this.fullVae = null; this.vaeError = String(e.message || e);
213
+ }
214
+ }
215
+
216
+ // -> { x: Float32Array (1 x 3 x H x W, range about -1..1), H, W }
217
+ async decode(z, C, hw) {
218
+ if (this.fullVae) {
219
+ try { return await this.decodeFull(z, C, hw); }
220
+ catch (e) { console.warn("full VAE decode failed, falling back to TAESD", e); this.vaeError = String(e.message || e); this.fullVae = null; }
221
+ }
222
+ const img = await this.sessions.vae.run({ latent: new ort.Tensor("float32", z, [1, C, hw, hw]) });
223
+ return { x: img.image.data, H: img.image.dims[2], W: img.image.dims[3] };
224
+ }
225
+
226
+ async decodeFull(z, C, hw) {
227
+ const s = this.fullVae, inName = s.inputNames[0], outName = s.outputNames[0];
228
+ const zs = new Float32Array(z.length);
229
+ for (let i = 0; i < z.length; i++) zs[i] = z[i] / VAE_SCALE;
230
+ const dims = [1, C, hw, hw];
231
+ let out;
232
+ try { out = await s.run({ [inName]: new ort.Tensor("float32", zs, dims) }); }
233
+ catch (e) { out = await s.run({ [inName]: new ort.Tensor("float16", toHalf(zs), dims) }); } // fp16 graph input
234
+ const t = out[outName], raw = await t.getData(true);
235
+ const x = t.type === "float16"
236
+ ? (typeof Float16Array !== "undefined" && raw instanceof Float16Array ? Float32Array.from(raw) : halfBitsToF32(raw))
237
+ : Float32Array.from(raw);
238
+ for (let i = 0; i < x.length; i += 997) if (!Number.isFinite(x[i])) throw new Error("full VAE gave NaN/Inf (fp16 overflow?)");
239
+ return { x, H: t.dims[2], W: t.dims[3] };
240
+ }
241
+
242
+ sessionOptions(extra = {}) { return { executionProviders: [this.ep], graphOptimizationLevel: this.optLevel, ...extra }; }
243
+
244
+ // 003: the generator graph for one resolution; the weights file is shared by both graphs.
245
+ async createGenerator(res, bufs = {}) {
246
+ let graphName, ext, extra = {};
247
+ if (this.fast) {
248
+ graphName = this.manifest.step.graphs[String(res)];
249
+ ext = this.manifest.step.external_data;
250
+ extra = { preferredOutputLocation: "gpu-buffer" }; // z_next / x1 / plan stay on the GPU
251
+ } else {
252
+ const r = this.manifest.resolutions[String(res)];
253
+ graphName = r?.graph; ext = r?.external_data;
254
+ }
255
+ if (!graphName) throw new Error(`resolution ${res} not available`);
256
+ const graph = bufs[graphName] || await this.getFile(graphName);
257
+ const weights = this.weights || await this.getFile(ext);
258
+ if (this.sessions.gen) { try { await this.sessions.gen.release(); } catch { /* */ } this.sessions.gen = null; }
259
+ this.sessions.gen = await ort.InferenceSession.create(graph, this.sessionOptions({ externalData: [{ path: ext, data: weights }], ...extra }));
260
+ this.resolution = Number(res);
261
+ }
262
+
263
+ async setResolution(res) {
264
+ if (!this.mr || Number(res) === this.resolution) return 0;
265
+ const t0 = performance.now();
266
+ await this.createGenerator(res);
267
+ return performance.now() - t0;
268
+ }
269
+
270
+ async release() {
271
+ for (const s of Object.values(this.sessions || {})) { try { await s?.release(); } catch { /* */ } }
272
+ try { await this.fullVae?.release(); } catch { /* */ }
273
+ this.fullVae = null;
274
+ this.sessions = null; this.weights = null;
275
+ }
276
+
277
+ tokenize(text) {
278
+ const max = this.manifest.text_max_len;
279
+ let ids = this.tok.encode(text).ids; // [CLS] ... [SEP], as the HF tokenizer
280
+ if (ids.length > max) ids = ids.slice(0, max - 1).concat(ids[ids.length - 1]); // truncation=True
281
+ return ids;
282
+ }
283
+
284
+ async encode(text) {
285
+ const ids = this.tokenize(text);
286
+ const n = ids.length;
287
+ const feeds = {
288
+ input_ids: new ort.Tensor("int64", BigInt64Array.from(ids, BigInt), [1, n]),
289
+ attention_mask: new ort.Tensor("int64", new BigInt64Array(n).fill(1n), [1, n]),
290
+ };
291
+ const out = await this.sessions.text.run(feeds);
292
+ const h = out.last_hidden_state;
293
+ const data = h.data.slice(); // (1, n, 512) fp32
294
+ h.dispose?.();
295
+ return { data, n, ids };
296
+ }
297
+
298
+ // The prompt as the model sees it: 003 runs its prompt pipeline (prompt.js), 001/002 use the prompt as typed.
299
+ prepare(prompt, negative = "") {
300
+ const pp = this.manifest.prompt_pipeline;
301
+ if (!pp) return { text: prompt, negative };
302
+ const [text, neg] = prepare(prompt, negative, pp.normalize, pp.spell);
303
+ return { text, negative: neg };
304
+ }
305
+
306
+ static gaussianNoise(seed, count) {
307
+ // mulberry32 + Box-Muller. Seeds do NOT reproduce PyTorch's torch.randn stream.
308
+ let a = seed >>> 0;
309
+ const rnd = () => { a |= 0; a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
310
+ const out = new Float32Array(count);
311
+ for (let i = 0; i < count; i += 2) {
312
+ const u1 = Math.max(rnd(), 1e-12), u2 = rnd();
313
+ const r = Math.sqrt(-2 * Math.log(u1));
314
+ out[i] = r * Math.cos(2 * Math.PI * u2);
315
+ if (i + 1 < count) out[i + 1] = r * Math.sin(2 * Math.PI * u2);
316
+ }
317
+ return out;
318
+ }
319
+
320
+ // SD3's resolution shift in Agate's convention (t = 0 noise), as agate/pipeline.py shift_t
321
+ static shiftT(t, shift) {
322
+ if (shift === 1) return t;
323
+ const s = 1 - t;
324
+ return 1 - shift * s / (1 + (shift - 1) * s);
325
+ }
326
+
327
+ // -> { rgba (watermarked unless watermark=false), width, height, latent, timings, prepared }
328
+ get hasPlan() { return !!this.sessions?.gen?.outputNames?.includes("plan"); }
329
+
330
+ // GPU-resident sampler for the step graphs: z_next of one run is the z input of the next, ctx / mask / counts
331
+ // are uploaded once, and nothing is read back until the end. The preview (plan + x1) is requested only when
332
+ // onPreview is set, previewReady() says the viewer is free and no earlier readback is still in flight; its
333
+ // download is started but never awaited by the loop (frames are dropped instead).
334
+ async sampleFast({ z, grid, cfg, hw, C, L, D, ctx, mask, cnt, onStep, onPreview, previewReady, shouldStop, stepMs }) {
335
+ const dev = ort.env.webgpu?.device;
336
+ const bufs = [];
337
+ const gpuT = (data, dims) => {
338
+ if (!dev) return new ort.Tensor("float32", data, dims);
339
+ const buf = dev.createBuffer({ size: Math.ceil(data.byteLength / 16) * 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST });
340
+ dev.queue.writeBuffer(buf, 0, data);
341
+ bufs.push(buf);
342
+ return ort.Tensor.fromGpuBuffer(buf, { dataType: "float32", dims });
343
+ };
344
+ const fixed = { ctx: gpuT(ctx, [2, L, D]), mask: gpuT(mask, [2, L]) };
345
+ if (this.sessions.gen.inputNames.includes("counts")) fixed.counts = gpuT(cnt || new Float32Array(2 * L), [2, L]);
346
+ const steps = grid.length - 1;
347
+ let zT = new ort.Tensor("float32", z, [1, C, hw, hw]), inflight = false;
348
+ const one = (v) => new ort.Tensor("float32", new Float32Array([v]), [1]);
349
+ const cfgT = one(cfg);
350
+ try {
351
+ for (let i = 0; i < steps; i++) {
352
+ if (shouldStop()) throw new Error("stopped");
353
+ const ts = performance.now();
354
+ const want = !!onPreview && this.hasPlan && !inflight && previewReady();
355
+ const out = await this.sessions.gen.run({ z: zT, t: one(grid[i]), dt: one(grid[i + 1] - grid[i]), cfg: cfgT, ...fixed },
356
+ want ? ["z_next", "x1", "plan"] : ["z_next"]);
357
+ if (i > 0) zT.dispose?.();
358
+ zT = out.z_next;
359
+ if (want) {
360
+ inflight = true;
361
+ const step = i + 1;
362
+ Promise.all([out.plan.getData(true), out.x1.getData(true)])
363
+ .then(([plan, x1]) => onPreview({ step, steps, plan, x1, hw }))
364
+ .catch((e) => console.warn("preview", e))
365
+ .finally(() => { inflight = false; });
366
+ }
367
+ stepMs.push(performance.now() - ts);
368
+ onStep(i + 1, steps, null);
369
+ if ((i & 3) === 3) await new Promise((r) => setTimeout(r, 0)); // let the page paint now and then
370
+ }
371
+ const zf = await zT.getData(true); // the only synchronising readback
372
+ return Float32Array.from(zf);
373
+ } finally {
374
+ for (const b of bufs) { try { b.destroy(); } catch { /* */ } }
375
+ }
376
+ }
377
+
378
+ // onPreview({ step, steps, plan, x1, hw }) after every step, when given and the graph outputs the plan: plan =
379
+ // the thinker output for the conditional branch (Float32Array 640 x 16 x 16), x1 = z + (1 - t) v (guided).
380
+ async generate({ prompt, negative = "", seed = 0, steps = 50, cfg = 3.0, noise = null, watermark = true, onStep = () => {}, onPreview = null, previewReady = () => true, shouldStop = () => false }) {
381
+ const C = this.manifest.latent_ch, D = this.manifest.ctx_dim;
382
+ const rc = this.mr ? this.manifest.resolutions[String(this.resolution)] : { latent_hw: this.manifest.latent_hw, shift: 1 };
383
+ const hw = rc.latent_hw, shift = Number(rc.shift || 1);
384
+ const per = C * hw * hw;
385
+ const T = { };
386
+ let t0 = performance.now();
387
+ const prep = this.prepare(prompt, negative);
388
+ const c = await this.encode(prep.text), u = await this.encode(prep.negative);
389
+ T.textMs = performance.now() - t0;
390
+ const BUCKETS = this.manifest.buckets;
391
+ const need = Math.max(c.n, u.n);
392
+ const L = BUCKETS.find((b) => b >= need) ?? BUCKETS[BUCKETS.length - 1];
393
+ const ctx = new Float32Array(2 * L * D), mask = new Float32Array(2 * L);
394
+ ctx.set(c.data, 0); ctx.set(u.data, L * D); // zero padding, as F.pad in the pipeline
395
+ mask.fill(1, 0, c.n); mask.fill(1, L, L + u.n);
396
+ const feeds = { ctx: new ort.Tensor("float32", ctx, [2, L, D]), mask: new ort.Tensor("float32", mask, [2, L]) };
397
+ let countsNonzero = 0;
398
+ if (this.mr) { // count code: conditional half only
399
+ const cnt = new Float32Array(2 * L);
400
+ if (this.manifest.prompt_pipeline?.count_code) cnt.set(countVector(this.tok, prep.text, c.ids, Math.min(L, c.n)), 0);
401
+ countsNonzero = cnt.reduce((s, v) => s + (v > 0), 0);
402
+ feeds.counts = new ort.Tensor("float32", cnt, [2, L]);
403
+ }
404
+
405
+ let z = noise ? Float32Array.from(noise) : Agate.gaussianNoise(seed, per);
406
+ const grid = Array.from({ length: steps + 1 }, (_, i) => Agate.shiftT(i / steps, shift));
407
+ t0 = performance.now();
408
+ const stepMs = [];
409
+ if (this.fast) {
410
+ z = await this.sampleFast({ z, grid, cfg, hw, C, L, D, ctx, mask, cnt: feeds.counts?.data, onStep, onPreview, previewReady, shouldStop, stepMs });
411
+ } else {
412
+ const zz = new Float32Array(2 * per), tt = new Float32Array(2);
413
+ for (let i = 0; i < steps; i++) {
414
+ if (shouldStop()) throw new Error("stopped");
415
+ const ts = performance.now();
416
+ zz.set(z, 0); zz.set(z, per);
417
+ tt[0] = tt[1] = grid[i];
418
+ const want = onPreview && this.hasPlan ? ["v", "plan"] : ["v"];
419
+ 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);
420
+ const v = out.v.data;
421
+ const dt = grid[i + 1] - grid[i];
422
+ const zn = new Float32Array(per);
423
+ let x1 = null;
424
+ if (out.plan) x1 = new Float32Array(per);
425
+ for (let k = 0; k < per; k++) {
426
+ const vc = v[k], vu = v[per + k], g = vu + cfg * (vc - vu);
427
+ zn[k] = z[k] + dt * g;
428
+ if (x1) x1[k] = z[k] + (1 - grid[i]) * g;
429
+ }
430
+ out.v.dispose?.();
431
+ if (out.plan) { const plan = out.plan.data.slice(); out.plan.dispose?.(); onPreview({ step: i + 1, steps, plan, x1, hw }); }
432
+ z = zn;
433
+ stepMs.push(performance.now() - ts);
434
+ onStep(i + 1, steps, z);
435
+ await new Promise((r) => setTimeout(r, 0)); // let the progress bar paint
436
+ }
437
+ }
438
+ T.samplerMs = performance.now() - t0;
439
+ T.firstStepMs = stepMs[0];
440
+ T.stepMs = stepMs.length > 1 ? (T.samplerMs - stepMs[0]) / (stepMs.length - 1) : stepMs[0];
441
+
442
+ t0 = performance.now();
443
+ const { x, H, W } = await this.decode(z, C, hw);
444
+ const rgba = new Uint8ClampedArray(H * W * 4);
445
+ for (let p = 0; p < H * W; p++) {
446
+ for (let ch = 0; ch < 3; ch++) {
447
+ const val = Math.min(1, Math.max(-1, x[ch * H * W + p]));
448
+ rgba[p * 4 + ch] = Math.round((val + 1) * 127.5);
449
+ }
450
+ rgba[p * 4 + 3] = 255;
451
+ }
452
+ T.decodeMs = performance.now() - t0;
453
+ t0 = performance.now();
454
+ if (watermark) embedWatermark(rgba, W, H, this.marks.payload);
455
+ T.markMs = performance.now() - t0;
456
+ T.totalMs = T.textMs + T.samplerMs + T.decodeMs + T.markMs;
457
+ T.bucket = L;
458
+ return { rgba, width: W, height: H, latent: z, timings: T, prepared: { ...prep, countsNonzero }, watermarked: watermark };
459
+ }
460
+ }
js/app.js ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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; $("fast").checked = Number(ui.steps.value) === 25; };
120
+ $("fast").onchange = () => { ui.steps.value = $("fast").checked ? 25 : 50; $("steps-v").textContent = ui.steps.value; };
121
+ ui.cfg.oninput = () => { $("cfg-v").textContent = Number(ui.cfg.value).toFixed(1); };
122
+ ui.dice.onclick = () => { ui.seed.value = Math.floor(Math.random() * 2 ** 31); };
123
+ ui.crisp.checked = store.get("agate-crisp") === "1";
124
+ const applyCrisp = () => { ui.frame.classList.toggle("crisp", ui.crisp.checked); store.set("agate-crisp", ui.crisp.checked ? "1" : "0"); };
125
+ ui.crisp.onchange = applyCrisp; applyCrisp();
126
+
127
+ const mb = (b) => (b / 1e6).toFixed(0);
128
+ function setProgress(frac, left, right = "") {
129
+ ui.fill.style.width = `${Math.max(0, Math.min(1, frac)) * 100}%`;
130
+ ui.status.textContent = left; ui.statusR.textContent = right;
131
+ }
132
+ function showTimings(rows) {
133
+ ui.timings.innerHTML = rows.map(([k, v]) => `<dt>${k}</dt><dd>${v}</dd>`).join("");
134
+ }
135
+ function clearOutput() {
136
+ ui.placeholder.hidden = false; ui.aiNote.hidden = true; ui.thinker.hidden = true; ui.save.hidden = true; ui.prepared.hidden = true;
137
+ showTimings([]); last = null;
138
+ if (ui.save.href?.startsWith("blob:")) URL.revokeObjectURL(ui.save.href);
139
+ }
140
+
141
+ async function load() {
142
+ busy = true; ui.go.disabled = true;
143
+ window.__agate.state = "loading";
144
+ agate = new Agate({ base: BASE, override: MODELS_OVERRIDE, version, ep, variant: VARIANT, optLevel: params.get("opt") || "all" });
145
+ try {
146
+ const t0 = performance.now();
147
+ const st = await agate.load(({ phase, file, loaded, total }) => {
148
+ if (phase === "download") setProgress(loaded / total, `Downloading ${file}`, `${mb(loaded)} / ${mb(total)} MB`);
149
+ else setProgress(1, `Preparing ${file} on ${ep === "webgpu" ? "WebGPU" : "CPU"}`, `${mb(total)} MB`);
150
+ }, res());
151
+ const loadMs = performance.now() - t0;
152
+ $("dl-size").textContent = `${st.downloadMB.toFixed(0)} MB`;
153
+ ui.backend.textContent = `backend: ${ep === "webgpu" ? "WebGPU" : "WebAssembly (CPU)"}`;
154
+ const src = st.fromCacheMB > st.downloadMB * 0.99 ? "from browser cache" : "downloaded";
155
+ setProgress(0, "Ready", `Preview ${version} · ${st.downloadMB.toFixed(0)} MB ${src} · ${(loadMs / 1000).toFixed(1)} s`);
156
+ window.__agate = { state: "ready", version, load: { ...st, loadMs, ep } };
157
+ ui.go.textContent = "Generate"; ui.go.disabled = false;
158
+ } catch (e) {
159
+ console.error(e);
160
+ setProgress(0, "Load failed", String(e.message || e));
161
+ window.__agate = { state: "error", version, error: String(e.message || e) };
162
+ ui.go.textContent = "Retry load"; ui.go.disabled = false; agate = null;
163
+ }
164
+ busy = false;
165
+ }
166
+
167
+ async function pngWithMetadata() {
168
+ const blob = await new Promise((r) => ui.canvas.toBlob(r, "image/png"));
169
+ const bytes = addPngText(new Uint8Array(await blob.arrayBuffer()), agate.marks.info);
170
+ return new Blob([bytes], { type: "image/png" });
171
+ }
172
+
173
+ // The previews: frames go to thinkerView, which colours them in a Web Worker and drops frames while busy. The
174
+ // sampler never waits for any of it (agate.js sampleFast); with Show thinker off nothing is requested at all.
175
+ function previewFn() {
176
+ thinkerView.reset();
177
+ let n = 0, t0 = performance.now();
178
+ window.__agate.thinker = { submitted: 0 };
179
+ thinkerView.onDrawn = ({ step, steps, workerMs, dropped, drawn }) => {
180
+ ui.thinker.hidden = false;
181
+ ui.thinkerStep.textContent = `${step} / ${steps}`;
182
+ window.__agate.thinker = { submitted: n, drawn, dropped, lastWorkerMs: workerMs, lastStep: step };
183
+ };
184
+ return ({ step, steps, plan, x1, hw }) => {
185
+ if (thinkerView.submit(plan, x1, hw, { step, steps })) n++;
186
+ };
187
+ }
188
+
189
+ async function generate(opts = {}) {
190
+ busy = true; stop = false;
191
+ ui.go.textContent = "Stop"; ui.frame.classList.add("busy");
192
+ window.__agate.state = "generating";
193
+ const steps = opts.steps ?? Number(ui.steps.value);
194
+ try {
195
+ setProgress(0, "Encoding prompt", "");
196
+ const r = await agate.generate({
197
+ prompt: opts.prompt ?? ui.prompt.value.trim(), negative: opts.negative ?? ui.negative.value.trim(),
198
+ seed: Number(ui.seed.value) >>> 0, steps, cfg: opts.cfg ?? Number(ui.cfg.value), noise: opts.noise ?? null,
199
+ watermark: opts.watermark ?? true,
200
+ onStep: (i, n) => setProgress(i / n, `Step ${i} / ${n}`, ""),
201
+ onPreview: (opts.thinker ?? ui.showThinker.checked) ? previewFn() : null,
202
+ previewReady: () => !thinkerView.pending,
203
+ shouldStop: () => stop,
204
+ });
205
+ const ctx = ui.canvas.getContext("2d");
206
+ ui.canvas.width = r.width; ui.canvas.height = r.height;
207
+ ctx.putImageData(new ImageData(r.rgba, r.width, r.height), 0, 0);
208
+ ui.placeholder.hidden = true;
209
+ ui.aiNote.hidden = false;
210
+ const T = r.timings;
211
+ setProgress(1, "Done", `${(T.totalMs / 1000).toFixed(1)} s`);
212
+ showTimings([
213
+ ["text encode", `${T.textMs.toFixed(0)} ms (bucket ${T.bucket})`],
214
+ ["per step", `${T.stepMs.toFixed(0)} ms × ${steps} (first ${T.firstStepMs.toFixed(0)} ms)`],
215
+ ["decode + mark", `${T.decodeMs.toFixed(0)} + ${T.markMs.toFixed(0)} ms`],
216
+ ["total", `${(T.totalMs / 1000).toFixed(2)} s`],
217
+ ]);
218
+ const p = r.prepared;
219
+ const changed = agate.mr && (p.text !== (opts.prompt ?? ui.prompt.value.trim()) || p.negative || p.countsNonzero);
220
+ ui.prepared.hidden = !changed;
221
+ if (changed) ui.prepared.textContent = `Model sees: ${p.text}${p.negative ? ` · avoid: ${p.negative}` : ""}${p.countsNonzero ? ` · count code on ${p.countsNonzero} token(s)` : ""}`;
222
+ if (ui.save.href?.startsWith("blob:")) URL.revokeObjectURL(ui.save.href);
223
+ ui.save.href = URL.createObjectURL(await pngWithMetadata());
224
+ ui.save.download = `agate-${version}-${r.width}px-seed${Number(ui.seed.value) >>> 0}.png`;
225
+ ui.save.hidden = false;
226
+ last = r;
227
+ window.__agate = { ...window.__agate, state: "done", timings: T, prepared: p, size: [r.width, r.height], watermarked: r.watermarked,
228
+ thinkerShown: !ui.thinker.hidden };
229
+ return r;
230
+ } catch (e) {
231
+ if (String(e.message) === "stopped") setProgress(0, "Stopped", "");
232
+ else { console.error(e); setProgress(0, "Error", String(e.message || e)); window.__agate.error = String(e.message || e); }
233
+ window.__agate.state = "done";
234
+ } finally {
235
+ busy = false; ui.go.textContent = "Generate"; ui.frame.classList.remove("busy");
236
+ }
237
+ }
238
+
239
+ ui.go.onclick = async () => {
240
+ if (busy && agate?.sessions) { stop = true; return; }
241
+ if (busy) return;
242
+ if (!agate) return load();
243
+ return generate();
244
+ };
245
+ ui.prompt.addEventListener("keydown", (e) => { if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && agate && !busy) generate(); });
246
+
247
+ // Parity self-test: ?parity=1 runs the chosen version's fixture (export/parity_full*.py): same initial noise as the
248
+ // PyTorch package, watermark off (the fixture holds unmarked pixels); then checks the mark on the marked image.
249
+ async function parity() {
250
+ const dir = version === "001" ? "test/" : version === "002" ? "test/002/" : `test/003_${res()}/`;
251
+ const fx = await (await fetch(`${dir}parity.json`)).json();
252
+ const bin = async (f) => new Uint8Array(await (await fetch(`${dir}${f}`)).arrayBuffer());
253
+ const noise = new Float32Array((await bin("parity_noise.bin")).buffer);
254
+ const refZ = new Float32Array((await bin("parity_latent.bin")).buffer);
255
+ const refImg = await bin("parity_image.bin"); // HWC uint8
256
+ ui.prompt.value = fx.prompt; ui.steps.value = fx.steps; ui.cfg.value = fx.cfg; ui.negative.value = "";
257
+ const r = await generate({ prompt: fx.prompt, negative: "", steps: fx.steps, cfg: fx.cfg, noise, watermark: false });
258
+ let dz = 0, dzs = 0, di = 0, dis = 0, se = 0;
259
+ for (let k = 0; k < refZ.length; k++) { const d = Math.abs(r.latent[k] - refZ[k]); dz = Math.max(dz, d); dzs += d; }
260
+ for (let p = 0; p < refImg.length / 3; p++) for (let c = 0; c < 3; c++) {
261
+ const d = Math.abs(r.rgba[p * 4 + c] - refImg[p * 3 + c]); di = Math.max(di, d); dis += d; se += d * d;
262
+ }
263
+ const psnr = 10 * Math.log10(255 * 255 / Math.max(1e-9, se / refImg.length));
264
+ const prepOk = fx.prepared === undefined || (r.prepared.text === fx.prepared && r.prepared.negative === fx.negative);
265
+ // the same image marked, read back in the page
266
+ const { embedWatermark } = await import("./marking.js");
267
+ const marked = Uint8ClampedArray.from(r.rgba);
268
+ embedWatermark(marked, r.width, r.height, agate.marks.payload);
269
+ const rb = readWatermark(marked, r.width, r.height, agate.marks.payload);
270
+ const res_ = { version, resolution: res(), latentMaxAbs: dz, latentMeanAbs: dzs / refZ.length, imageMaxAbs: di, imageMeanAbs: dis / refImg.length,
271
+ psnr, preparedMatchesPython: prepOk, watermarkReadback: rb.text, watermarkBitAcc: rb.bitAccuracy, ep, timings: r.timings };
272
+ const el = $("parity"); el.hidden = false;
273
+ el.innerHTML = `<span class="label"><i class="sq red"></i>Parity vs PyTorch package · Preview ${version} · ${res()} px (${ep})</span>
274
+ <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)
275
+ · prompt pipeline ${prepOk ? "identical" : "DIFFERENT"} · watermark read back: ${rb.text} (${(rb.bitAccuracy * 100).toFixed(0)}% bits)</p>`;
276
+ window.__agate = { ...window.__agate, parity: res_, state: "parity-done" };
277
+ }
278
+
279
+ // Golden test of the prompt pipeline in this browser (tokenizers.js + prompt.js) against the Python package.
280
+ async function golden() {
281
+ const G = await (await fetch("test/prompt_golden.json")).json();
282
+ const tok = agate.tok, fails = [];
283
+ const enc = (s) => agate.tokenize(s).map(Number);
284
+ for (const c of G.cases) {
285
+ const [text, neg] = preparePrompt(c.prompt);
286
+ const ids = enc(text);
287
+ const cv = countVector(tok, text, ids, ids.length), counts = {};
288
+ cv.forEach((v, j) => { if (v) counts[String(j)] = v; });
289
+ const ok = text === c.prepared && neg === c.negative && JSON.stringify(ids) === JSON.stringify(c.ids)
290
+ && JSON.stringify(enc(neg)) === JSON.stringify(c.neg_ids) && JSON.stringify(counts) === JSON.stringify(c.counts);
291
+ if (!ok) fails.push(c.prompt);
292
+ }
293
+ const res_ = { cases: G.cases.length, identical: G.cases.length - fails.length, fails };
294
+ const el = $("parity"); el.hidden = false;
295
+ 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>`;
296
+ window.__agate = { ...window.__agate, golden: res_ };
297
+ }
298
+
299
+ // For the automated test: the PNG exactly as "Save PNG" gives it, base64
300
+ window.__agateSavedPng = async () => {
301
+ const b = new Uint8Array(await (await pngWithMetadata()).arrayBuffer());
302
+ let s = ""; for (let i = 0; i < b.length; i += 0x8000) s += String.fromCharCode(...b.subarray(i, i + 0x8000));
303
+ return btoa(s);
304
+ };
305
+ window.agateClearCache = clearCache;
306
+
307
+ (async () => {
308
+ renderVersion();
309
+ if (!FORCE_EP) {
310
+ const s = await webgpuStatus();
311
+ if (!s.ok) {
312
+ gpuOk = false; ep = "wasm";
313
+ $("nogpu").hidden = false; $("nogpu-why").textContent = s.why;
314
+ ui.go.disabled = true; ui.go.textContent = "WebGPU required";
315
+ window.__agate = { state: "no-webgpu", why: s.why, version };
316
+ $("use-wasm").onclick = () => { $("nogpu").hidden = true; gpuOk = true; ui.go.textContent = "Load model (CPU)"; ui.go.disabled = false; };
317
+ return;
318
+ }
319
+ ui.backend.textContent = `backend: WebGPU${s.adapter ? " · " + s.adapter : ""}`;
320
+ }
321
+ ui.go.disabled = false;
322
+ ui.go.textContent = "Load model";
323
+ if (params.has("autoload") || PARITY || GOLDEN || await hasCachedModel(version)) { // second visit: load straight from cache
324
+ await load();
325
+ if (GOLDEN && agate && version === "003") await golden();
326
+ if (PARITY && agate) await parity();
327
+ if (GOLDEN && !PARITY) window.__agate.state = "golden-done";
328
+ }
329
+ })();
js/config.js ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ // Where the model files live. The Hugging Face Space hosts them next to the page in models/.
2
+ // A ?models=<url> query parameter overrides this.
3
+ window.AGATE_MODEL_BASE = "./models/";
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-worker.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Web Worker: colours the thinker previews off the main thread (see thinker.js).
2
+ import { planPixels, predPixels } from "./thinker.js";
3
+
4
+ let gen = -1, state = {};
5
+ self.onmessage = (e) => {
6
+ const { gen: g, plan, x1, hw, meta } = e.data;
7
+ if (g !== gen) { gen = g; state = {}; } // a new generation: fit a new basis at its first frame
8
+ const t0 = performance.now();
9
+ const p = planPixels(plan, state), q = predPixels(x1, hw);
10
+ self.postMessage({ gen: g, plan: p, pred: q, hw, meta, ms: performance.now() - t0 }, [p.buffer, q.buffer]);
11
+ };
js/thinker.js ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ // state: { basis, lo, hi } (filled at the first call of a generation). -> RGBA Uint8ClampedArray (g * g * 4)
79
+ export function planPixels(plan, state, C = 640, g = 16) {
80
+ const n = g * g, X = new Float64Array(n * C);
81
+ for (let c = 0; c < C; c++) for (let p = 0; p < n; p++) X[p * C + c] = plan[c * n + p];
82
+ 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; }
83
+ if (!state.basis) state.basis = topComponents(X, n, C);
84
+ const Y = state.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; });
85
+ if (!state.lo) {
86
+ state.lo = Y.map((y) => quantile(Float64Array.from(y).sort(), 0.02));
87
+ state.hi = Y.map((y) => quantile(Float64Array.from(y).sort(), 0.98));
88
+ }
89
+ const px = new Uint8ClampedArray(n * 4);
90
+ for (let p = 0; p < n; p++) {
91
+ for (let k = 0; k < 3; k++) px[p * 4 + k] = Math.round(Math.min(1, Math.max(0, (Y[k][p] - state.lo[k]) / (state.hi[k] - state.lo[k] + 1e-8))) * 255);
92
+ px[p * 4 + 3] = 255;
93
+ }
94
+ return px;
95
+ }
96
+
97
+ // x1: Float32Array (4 * hw * hw) in the model's (scaled SD-VAE) latent space -> RGBA (hw * hw * 4)
98
+ export function predPixels(x1, hw) {
99
+ const n = hw * hw, px = new Uint8ClampedArray(n * 4);
100
+ for (let p = 0; p < n; p++) {
101
+ for (let k = 0; k < 3; k++) {
102
+ let s = 0; for (let c = 0; c < 4; c++) s += x1[c * n + p] * RGB_FACTORS[c][k];
103
+ px[p * 4 + k] = Math.floor(Math.min(1, Math.max(0, (s + 1) / 2)) * 255);
104
+ }
105
+ px[p * 4 + 3] = 255;
106
+ }
107
+ return px;
108
+ }
109
+
110
+ // Draws previews; the colouring runs in a Web Worker (thinker-worker.js) when available, so the page and the
111
+ // sampler never wait for it. A frame arriving while the previous one is still being coloured is dropped.
112
+ export class ThinkerView {
113
+ constructor(planCanvas, predCanvas, onDrawn = () => {}) {
114
+ this.planCanvas = planCanvas; this.predCanvas = predCanvas; this.onDrawn = onDrawn;
115
+ this.gen = 0; this.busy = false; this.dropped = 0; this.drawn = 0;
116
+ try {
117
+ this.worker = new Worker(new URL("./thinker-worker.js", import.meta.url), { type: "module" });
118
+ this.worker.onmessage = (e) => this._paint(e.data);
119
+ } catch { this.worker = null; }
120
+ this.reset();
121
+ }
122
+
123
+ reset() { this.gen++; this.state = {}; this.busy = false; this.dropped = 0; this.drawn = 0; }
124
+
125
+ get pending() { return this.busy; }
126
+
127
+ // plan: Float32Array (640 * 16 * 16), x1: Float32Array (4 * hw * hw); both are transferred to the worker
128
+ submit(plan, x1, hw, meta = {}) {
129
+ if (this.busy) { this.dropped++; return false; }
130
+ this.busy = true;
131
+ const job = { gen: this.gen, plan, x1, hw, meta };
132
+ if (this.worker) this.worker.postMessage(job, [plan.buffer, x1.buffer]);
133
+ else setTimeout(() => this._paint({ gen: job.gen, hw, meta, plan: planPixels(plan, this.state), pred: predPixels(x1, hw) }), 0);
134
+ return true;
135
+ }
136
+
137
+ _paint({ gen, plan, pred, hw, meta, ms }) {
138
+ this.busy = false;
139
+ if (gen !== this.gen) return;
140
+ const put = (cv, px, s) => { cv.width = s; cv.height = s; cv.getContext("2d").putImageData(new ImageData(px, s, s), 0, 0); };
141
+ put(this.planCanvas, plan, 16);
142
+ put(this.predCanvas, pred, hw);
143
+ this.drawn++;
144
+ this.onDrawn({ ...meta, workerMs: ms, dropped: this.dropped, drawn: this.drawn });
145
+ }
146
+
147
+ // synchronous path (tests): colour on this thread
148
+ drawPlan(plan) { const px = planPixels(plan, this.state); const cv = this.planCanvas; cv.width = 16; cv.height = 16; cv.getContext("2d").putImageData(new ImageData(px, 16, 16), 0, 0); }
149
+ }
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/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.0348610058426857,
8
+ "onnx_cpu_image_max_abs": 5
9
+ }
test/parity_image.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f02c5c097a4f5ba6bfc0738ef8b707c77b7ccffda50ebb349f408b16ee77d5e3
3
+ size 196608
test/parity_latent.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f33683243a20402962a1cb9a4f901bcb862aef2509a7a184e6eda637a234b91a
3
+ size 16384
test/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/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
+ }