Spaces:
Running
Running
File size: 8,647 Bytes
399fbe5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | // "Show thinker": the live previews of the ComfyUI nodes (agate-comfyui nodes.py, _progress, live_preview
// "side_by_side"), computed the same way so the two frontends show the same thing:
// * the plan: the thinker's output for the conditional branch, 640 channels on a 16 x 16 grid. Its cells are
// projected on the top 3 principal components (fitted once, at the first step, centred over the cells; each
// component's sign fixed so its largest-magnitude loading is positive) and mapped to RGB with that step's 2% /
// 98% quantiles -- a fixed basis, so a colour keeps its meaning while the plan evolves;
// * the image the model expects at the end, x1 = z + (1 - t) v (guided velocity), shown with ComfyUI's SD 1.5
// latent -> RGB factors (its "latent2rgb" preview).
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]];
// 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
// aside), via the smaller (n x n) X X^T: block (subspace) iteration with a 12-vector block + Rayleigh-Ritz, which
// separates near-equal eigenvalues that plain power iteration mixes; then v = X^T u / |X^T u|.
function jacobiEig(A, m) { // symmetric m x m (Float64Array) -> {vals, vecs (column-major rows)}
const a = Float64Array.from(A), V = new Float64Array(m * m);
for (let i = 0; i < m; i++) V[i * m + i] = 1;
for (let sweep = 0; sweep < 60; sweep++) {
let off = 0;
for (let p = 0; p < m; p++) for (let q = p + 1; q < m; q++) off += a[p * m + q] ** 2;
if (off < 1e-24) break;
for (let p = 0; p < m; p++) for (let q = p + 1; q < m; q++) {
const apq = a[p * m + q];
if (Math.abs(apq) < 1e-300) continue;
const th = (a[q * m + q] - a[p * m + p]) / (2 * apq);
const t = Math.sign(th || 1) / (Math.abs(th) + Math.sqrt(th * th + 1)), c = 1 / Math.sqrt(t * t + 1), s = t * c;
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; }
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; }
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; }
}
}
return { vals: Array.from({ length: m }, (_, i) => a[i * m + i]), V };
}
function topComponents(X, n, d, k = 3, iters = 60, block = 12) {
const G = new Float64Array(n * n);
for (let i = 0; i < n; i++) for (let j = i; j < n; j++) {
let s = 0; const ri = i * d, rj = j * d;
for (let a = 0; a < d; a++) s += X[ri + a] * X[rj + a];
G[i * n + j] = G[j * n + i] = s;
}
const m = Math.min(block, n);
let Q = Array.from({ length: m }, (_, c) => Float64Array.from({ length: n }, (_, i) => Math.sin(1.7 * i + 0.9 * c + 1) + 0.01 * c));
const orth = (B) => { // modified Gram-Schmidt, in place
for (let c = 0; c < B.length; c++) {
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]; }
let nrm = 0; for (let i = 0; i < n; i++) nrm += B[c][i] ** 2; nrm = Math.sqrt(nrm) || 1;
for (let i = 0; i < n; i++) B[c][i] /= nrm;
}
return B;
};
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; };
orth(Q);
for (let it = 0; it < iters; it++) Q = orth(Q.map(mul));
const GQ = Q.map(mul), T = new Float64Array(m * m); // Rayleigh-Ritz on the block
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; }
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;
const { vals, V } = jacobiEig(T, m);
const order = vals.map((v, i) => [v, i]).sort((x, y) => y[0] - x[0]).slice(0, k).map((x) => x[1]);
return order.map((col) => {
const u = new Float64Array(n);
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]; }
const v = new Float64Array(d);
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]; }
let nrm = 0; for (let a = 0; a < d; a++) nrm += v[a] * v[a]; nrm = Math.sqrt(nrm) || 1;
let piv = 0;
for (let a = 0; a < d; a++) { v[a] /= nrm; if (Math.abs(v[a]) > Math.abs(v[piv])) piv = a; }
if (v[piv] < 0) for (let a = 0; a < d; a++) v[a] = -v[a];
return v;
});
}
function quantile(sorted, q) { // torch.quantile, linear interpolation
const pos = (sorted.length - 1) * q, lo = Math.floor(pos), hi = Math.ceil(pos);
return sorted[lo] + (sorted[hi] - sorted[lo]) * (pos - lo);
}
// state: { basis, lo, hi } (filled at the first call of a generation). -> RGBA Uint8ClampedArray (g * g * 4)
export function planPixels(plan, state, C = 640, g = 16) {
const n = g * g, X = new Float64Array(n * C);
for (let c = 0; c < C; c++) for (let p = 0; p < n; p++) X[p * C + c] = plan[c * n + p];
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; }
if (!state.basis) state.basis = topComponents(X, n, C);
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; });
if (!state.lo) {
state.lo = Y.map((y) => quantile(Float64Array.from(y).sort(), 0.02));
state.hi = Y.map((y) => quantile(Float64Array.from(y).sort(), 0.98));
}
const px = new Uint8ClampedArray(n * 4);
for (let p = 0; p < n; p++) {
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);
px[p * 4 + 3] = 255;
}
return px;
}
// x1: Float32Array (4 * hw * hw) in the model's (scaled SD-VAE) latent space -> RGBA (hw * hw * 4)
export function predPixels(x1, hw) {
const n = hw * hw, px = new Uint8ClampedArray(n * 4);
for (let p = 0; p < n; p++) {
for (let k = 0; k < 3; k++) {
let s = 0; for (let c = 0; c < 4; c++) s += x1[c * n + p] * RGB_FACTORS[c][k];
px[p * 4 + k] = Math.floor(Math.min(1, Math.max(0, (s + 1) / 2)) * 255);
}
px[p * 4 + 3] = 255;
}
return px;
}
// Draws previews; the colouring runs in a Web Worker (thinker-worker.js) when available, so the page and the
// sampler never wait for it. A frame arriving while the previous one is still being coloured is dropped.
export class ThinkerView {
constructor(planCanvas, predCanvas, onDrawn = () => {}) {
this.planCanvas = planCanvas; this.predCanvas = predCanvas; this.onDrawn = onDrawn;
this.gen = 0; this.busy = false; this.dropped = 0; this.drawn = 0;
try {
this.worker = new Worker(new URL("./thinker-worker.js", import.meta.url), { type: "module" });
this.worker.onmessage = (e) => this._paint(e.data);
} catch { this.worker = null; }
this.reset();
}
reset() { this.gen++; this.state = {}; this.busy = false; this.dropped = 0; this.drawn = 0; }
get pending() { return this.busy; }
// plan: Float32Array (640 * 16 * 16), x1: Float32Array (4 * hw * hw); both are transferred to the worker
submit(plan, x1, hw, meta = {}) {
if (this.busy) { this.dropped++; return false; }
this.busy = true;
const job = { gen: this.gen, plan, x1, hw, meta };
if (this.worker) this.worker.postMessage(job, [plan.buffer, x1.buffer]);
else setTimeout(() => this._paint({ gen: job.gen, hw, meta, plan: planPixels(plan, this.state), pred: predPixels(x1, hw) }), 0);
return true;
}
_paint({ gen, plan, pred, hw, meta, ms }) {
this.busy = false;
if (gen !== this.gen) return;
const put = (cv, px, s) => { cv.width = s; cv.height = s; cv.getContext("2d").putImageData(new ImageData(px, s, s), 0, 0); };
put(this.planCanvas, plan, 16);
put(this.predCanvas, pred, hw);
this.drawn++;
this.onDrawn({ ...meta, workerMs: ms, dropped: this.dropped, drawn: this.drawn });
}
// synchronous path (tests): colour on this thread
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); }
}
|