become the paintbrushft(j,40)((d(5RRR3RRR2R,3),23)tipl,100)v5As(l,50)iizZvKKkkkkkkkkkkkllllll1AR51AR52AR5R1At(l,35)t(k,4)At(k,48)(l,35)v(i,7)Adrrfx2At(l,200)fxm(l,5)(i,10)
/* KYS engine — generated by build_bundle.js. Do not edit. */
(function(root){
'use strict';
/* keccak256 — minimal, dependency-free, browser + node.
* Replaces js-sha3 (20.8KB) for the one function we need.
* Must agree byte-for-byte with Solidity's keccak256.
*/
(function (root) {
'use strict';
var RC = [
0x00000001,0x00000000, 0x00008082,0x00000000, 0x0000808a,0x80000000,
0x80008000,0x80000000, 0x0000808b,0x00000000, 0x80000001,0x00000000,
0x80008081,0x80000000, 0x00008009,0x80000000, 0x0000008a,0x00000000,
0x00000088,0x00000000, 0x80008009,0x00000000, 0x8000000a,0x00000000,
0x8000808b,0x00000000, 0x0000008b,0x80000000, 0x00008089,0x80000000,
0x00008003,0x80000000, 0x00008002,0x80000000, 0x00000080,0x80000000,
0x0000800a,0x00000000, 0x8000000a,0x80000000, 0x80008081,0x80000000,
0x00008080,0x80000000, 0x80000001,0x00000000, 0x80008008,0x80000000
];
function keccakf(s) {
var n, i, j, c0,c1,c2,c3,c4,c5,c6,c7,c8,c9, h, l;
var b = new Array(50);
for (n = 0; n < 24; n++) {
c0 = s[0]^s[10]^s[20]^s[30]^s[40]; c1 = s[1]^s[11]^s[21]^s[31]^s[41];
c2 = s[2]^s[12]^s[22]^s[32]^s[42]; c3 = s[3]^s[13]^s[23]^s[33]^s[43];
c4 = s[4]^s[14]^s[24]^s[34]^s[44]; c5 = s[5]^s[15]^s[25]^s[35]^s[45];
c6 = s[6]^s[16]^s[26]^s[36]^s[46]; c7 = s[7]^s[17]^s[27]^s[37]^s[47];
c8 = s[8]^s[18]^s[28]^s[38]^s[48]; c9 = s[9]^s[19]^s[29]^s[39]^s[49];
h = c8 ^ ((c2 << 1) | (c3 >>> 31)); l = c9 ^ ((c3 << 1) | (c2 >>> 31));
for (i = 0; i < 50; i += 10) { s[i] ^= h; s[i+1] ^= l; }
h = c0 ^ ((c4 << 1) | (c5 >>> 31)); l = c1 ^ ((c5 << 1) | (c4 >>> 31));
for (i = 0; i < 50; i += 10) { s[i+2] ^= h; s[i+3] ^= l; }
h = c2 ^ ((c6 << 1) | (c7 >>> 31)); l = c3 ^ ((c7 << 1) | (c6 >>> 31));
for (i = 0; i < 50; i += 10) { s[i+4] ^= h; s[i+5] ^= l; }
h = c4 ^ ((c8 << 1) | (c9 >>> 31)); l = c5 ^ ((c9 << 1) | (c8 >>> 31));
for (i = 0; i < 50; i += 10) { s[i+6] ^= h; s[i+7] ^= l; }
h = c6 ^ ((c0 << 1) | (c1 >>> 31)); l = c7 ^ ((c1 << 1) | (c0 >>> 31));
for (i = 0; i < 50; i += 10) { s[i+8] ^= h; s[i+9] ^= l; }
b[0]=s[0]; b[1]=s[1];
b[32]=(s[11]<<4)|(s[10]>>>28); b[33]=(s[10]<<4)|(s[11]>>>28);
b[14]=(s[20]<<3)|(s[21]>>>29); b[15]=(s[21]<<3)|(s[20]>>>29);
b[46]=(s[31]<<9)|(s[30]>>>23); b[47]=(s[30]<<9)|(s[31]>>>23);
b[28]=(s[40]<<18)|(s[41]>>>14); b[29]=(s[41]<<18)|(s[40]>>>14);
b[20]=(s[2]<<1)|(s[3]>>>31); b[21]=(s[3]<<1)|(s[2]>>>31);
b[2]=(s[13]<<12)|(s[12]>>>20); b[3]=(s[12]<<12)|(s[13]>>>20);
b[34]=(s[22]<<10)|(s[23]>>>22); b[35]=(s[23]<<10)|(s[22]>>>22);
b[16]=(s[33]<<13)|(s[32]>>>19); b[17]=(s[32]<<13)|(s[33]>>>19);
b[48]=(s[42]<<2)|(s[43]>>>30); b[49]=(s[43]<<2)|(s[42]>>>30);
b[40]=(s[5]<<30)|(s[4]>>>2); b[41]=(s[4]<<30)|(s[5]>>>2);
b[22]=(s[14]<<6)|(s[15]>>>26); b[23]=(s[15]<<6)|(s[14]>>>26);
b[4]=(s[25]<<11)|(s[24]>>>21); b[5]=(s[24]<<11)|(s[25]>>>21);
b[36]=(s[34]<<15)|(s[35]>>>17); b[37]=(s[35]<<15)|(s[34]>>>17);
b[18]=(s[45]<<29)|(s[44]>>>3); b[19]=(s[44]<<29)|(s[45]>>>3);
b[10]=(s[6]<<28)|(s[7]>>>4); b[11]=(s[7]<<28)|(s[6]>>>4);
b[42]=(s[17]<<23)|(s[16]>>>9); b[43]=(s[16]<<23)|(s[17]>>>9);
b[24]=(s[26]<<25)|(s[27]>>>7); b[25]=(s[27]<<25)|(s[26]>>>7);
b[6]=(s[36]<<21)|(s[37]>>>11); b[7]=(s[37]<<21)|(s[36]>>>11);
b[38]=(s[47]<<24)|(s[46]>>>8); b[39]=(s[46]<<24)|(s[47]>>>8);
b[30]=(s[8]<<27)|(s[9]>>>5); b[31]=(s[9]<<27)|(s[8]>>>5);
b[12]=(s[18]<<20)|(s[19]>>>12); b[13]=(s[19]<<20)|(s[18]>>>12);
b[44]=(s[29]<<7)|(s[28]>>>25); b[45]=(s[28]<<7)|(s[29]>>>25);
b[26]=(s[38]<<8)|(s[39]>>>24); b[27]=(s[39]<<8)|(s[38]>>>24);
b[8]=(s[48]<<14)|(s[49]>>>18); b[9]=(s[49]<<14)|(s[48]>>>18);
for (i = 0; i < 50; i += 10) {
for (j = 0; j < 10; j++) c0 = 0;
s[i] = b[i] ^ (~b[i+2] & b[i+4]);
s[i+1] = b[i+1] ^ (~b[i+3] & b[i+5]);
s[i+2] = b[i+2] ^ (~b[i+4] & b[i+6]);
s[i+3] = b[i+3] ^ (~b[i+5] & b[i+7]);
s[i+4] = b[i+4] ^ (~b[i+6] & b[i+8]);
s[i+5] = b[i+5] ^ (~b[i+7] & b[i+9]);
s[i+6] = b[i+6] ^ (~b[i+8] & b[i]);
s[i+7] = b[i+7] ^ (~b[i+9] & b[i+1]);
s[i+8] = b[i+8] ^ (~b[i] & b[i+2]);
s[i+9] = b[i+9] ^ (~b[i+1] & b[i+3]);
}
s[0] ^= RC[n*2]; s[1] ^= RC[n*2+1];
}
}
/* bytes (Uint8Array) -> 32-byte Uint8Array */
function keccak256(msg) {
var s = new Int32Array(50), R = 136, i, n = msg.length, off = 0;
while (n - off >= R) { absorb(s, msg, off, R); keccakf(s); off += R; }
var tail = new Uint8Array(R);
tail.set(msg.subarray(off));
tail[n - off] = 0x01;
tail[R - 1] |= 0x80;
absorb(s, tail, 0, R);
keccakf(s);
var out = new Uint8Array(32);
for (i = 0; i < 32; i++) out[i] = (s[i >> 2] >>> (8 * (i & 3))) & 0xff;
return out;
}
function absorb(s, buf, off, R) {
for (var i = 0; i < R; i++) s[i >> 2] ^= buf[off + i] << (8 * (i & 3));
}
function toHex(u8) {
var h = '';
for (var i = 0; i < u8.length; i++) h += (u8[i] < 16 ? '0' : '') + u8[i].toString(16);
return h;
}
function hexToBytes(h) {
/* Accept a number or bigint as well as a hex string: the studio and the
* curation tools identify variants by small integer seeds, and derive()
* must treat those the same way it treats a 32-byte token seed. */
if (typeof h === 'number' || typeof h === 'bigint') {
h = h.toString(16);
if (h.length % 2) h = '0' + h;
}
h = String(h).replace(/^0x/, '');
if (h.length % 2) h = '0' + h;
var u = new Uint8Array(h.length / 2);
for (var i = 0; i < u.length; i++) u[i] = parseInt(h.substr(i * 2, 2), 16);
return u;
}
var api = { keccak256: keccak256, toHex: toHex, hexToBytes: hexToBytes };
root.KECCAK = api;
})(root);
/* --- kys.js --- */
/* KYS on-chain renderer — model + substrate + algorithms.
* Must produce bit-identical output to substrate_chain.py + roster.py.
* No dependencies. Deterministic: no Math.random, no Date, no float drift
* beyond IEEE-754 double which both JS and numpy float64 share.
*/
'use strict';
/* ---------------------------------------------------------------- model */
function loadModel(buf) {
const dv = new DataView(buf);
if (dv.getUint8(0) !== 0x4b || dv.getUint8(1) !== 0x59 ||
dv.getUint8(2) !== 0x53 || dv.getUint8(3) !== 0x4d) throw new Error('bad magic');
const K = dv.getUint8(5);
const nvert = dv.getUint32(6, true), ntri = dv.getUint32(10, true);
const vscale = dv.getFloat32(14, true);
const vmin = [dv.getFloat32(18, true), dv.getFloat32(22, true), dv.getFloat32(26, true)];
let o = 30;
const vq = new Int16Array(buf, o, nvert * 3); o += nvert * 6;
const tris = new Uint16Array(buf, o, ntri * 3); o += ntri * 6;
const basis = f16Array(buf, o, K * nvert * 3);
const V0 = new Float64Array(nvert * 3);
for (let i = 0; i < nvert; i++)
for (let j = 0; j < 3; j++) V0[i * 3 + j] = vq[i * 3 + j] * vscale + vmin[j];
return { K, nvert, ntri, V0, tris, basis };
}
/* float16 decode — JS has no Float16Array in older engines */
function f16Array(buf, off, n) {
const u = new Uint16Array(buf, off, n), out = new Float32Array(n);
for (let i = 0; i < n; i++) out[i] = f16(u[i]);
return out;
}
function f16(h) {
const s = (h & 0x8000) >> 15, e = (h & 0x7c00) >> 10, f = h & 0x03ff;
if (e === 0) return (s ? -1 : 1) * Math.pow(2, -14) * (f / 1024);
if (e === 0x1f) return f ? NaN : (s ? -Infinity : Infinity);
return (s ? -1 : 1) * Math.pow(2, e - 15) * (1 + f / 1024);
}
/* ------------------------------------------------------------ identity */
/* PCA-tapered identity draw — mirrors params.identity() */
function identity(seed, faceShape, dims) {
const rng = gauss(seed);
const sigma = 2.2 * Math.pow(clamp(faceShape, 0, 1), 1.3);
const out = new Float64Array(dims);
for (let k = 0; k < dims; k++) out[k] = rng() * (1 / (1 + k / 40)) * sigma;
return out;
}
/* deterministic PRNG: splitmix64 -> uniform -> Box-Muller.
* NOTE: this does NOT match numpy's PCG64. See determinism note in README. */
function splitmix64(seed) {
let s = BigInt.asUintN(64, BigInt(seed));
return function () {
s = BigInt.asUintN(64, s + 0x9e3779b97f4a7c15n);
let z = s;
z = BigInt.asUintN(64, (z ^ (z >> 30n)) * 0xbf58476d1ce4e5b9n);
z = BigInt.asUintN(64, (z ^ (z >> 27n)) * 0x94d049bb133111ebn);
z = z ^ (z >> 31n);
return Number(z >> 11n) / 9007199254740992; // 53-bit uniform
};
}
function gauss(seed) {
const u = splitmix64(seed); let spare = null;
return function () {
if (spare !== null) { const v = spare; spare = null; return v; }
let a = u(), b = u();
if (a < 1e-300) a = 1e-300;
const r = Math.sqrt(-2 * Math.log(a)), th = 2 * Math.PI * b;
spare = r * Math.sin(th); return r * Math.cos(th);
};
}
const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
/* ---------------------------------------------------------------- verts */
function verts(M, ident) {
/* Accumulate in float32 to match numpy's einsum on a float32 basis.
* Math.fround() forces each step through single precision, which is what
* makes this bit-identical to the Python reference. */
const n = M.nvert, N = n * 3;
const acc = new Float32Array(N);
for (let k = 0; k < M.K; k++) {
const c = Math.fround(ident[k]); if (c === 0) continue;
const off = k * N;
for (let i = 0; i < N; i++) acc[i] = Math.fround(acc[i] + Math.fround(c * M.basis[off + i]));
}
const out = new Float64Array(N);
for (let i = 0; i < N; i++) out[i] = M.V0[i] + acc[i];
return out;
}
function lookAt(V, yawDeg, pitchDeg) {
const n = V.length / 3;
let cx = 0, cy = 0, cz = 0;
for (let i = 0; i < n; i++) { cx += V[i*3]; cy += V[i*3+1]; cz += V[i*3+2]; }
cx /= n; cy /= n; cz /= n;
const ry = yawDeg * Math.PI / 180, rx = pitchDeg * Math.PI / 180;
const cy_ = Math.cos(ry), sy = Math.sin(ry), cp = Math.cos(rx), sp = Math.sin(rx);
const out = new Float64Array(n * 3);
for (let i = 0; i < n; i++) {
const x = V[i*3] - cx, y = V[i*3+1] - cy, z = V[i*3+2] - cz;
// Ry then Rx, matching numpy P @ Ry.T @ Rx.T
const x1 = cy_*x + sy*z, y1 = y, z1 = -sy*x + cy_*z;
out[i*3] = x1;
out[i*3+1] = cp*y1 - sp*z1;
out[i*3+2] = sp*y1 + cp*z1;
}
return out;
}
/* ----------------------------------------------------------- rasterize */
function rasterize(P, tris, size, persp, margin) {
margin = margin === undefined ? 0.90 : margin;
const nt = tris.length / 3, n = P.length / 3;
// face normals
const fn = new Float64Array(nt * 3);
for (let t = 0; t < nt; t++) {
const a = tris[t*3]*3, b = tris[t*3+1]*3, c = tris[t*3+2]*3;
const ux = P[b]-P[a], uy = P[b+1]-P[a+1], uz = P[b+2]-P[a+2];
const vx = P[c]-P[a], vy = P[c+1]-P[a+1], vz = P[c+2]-P[a+2];
let nx = uy*vz-uz*vy, ny = uz*vx-ux*vz, nz = ux*vy-uy*vx;
const L = Math.hypot(nx, ny, nz) || 1;
fn[t*3] = nx/L; fn[t*3+1] = ny/L; fn[t*3+2] = nz/L;
}
// projection
const Q = new Float64Array(P.length); Q.set(P);
if (persp > 0) {
let zmin = Infinity, zmax = -Infinity;
for (let i = 0; i < n; i++) { const z = P[i*3+2]; if (z<zmin) zmin=z; if (z>zmax) zmax=z; }
const span = zmax - zmin, d = span / Math.max(persp, 1e-6) * 3.0;
for (let i = 0; i < n; i++) {
const s = d / (d - P[i*3+2] + span);
Q[i*3] = P[i*3]*s; Q[i*3+1] = P[i*3+1]*s;
}
}
let lox=Infinity, loy=Infinity, hix=-Infinity, hiy=-Infinity;
for (let i=0;i<n;i++){const x=Q[i*3],y=Q[i*3+1];
if(x<lox)lox=x; if(x>hix)hix=x; if(y<loy)loy=y; if(y>hiy)hiy=y;}
const scale = margin * size / Math.max(hix-lox, hiy-loy);
const ctrx = (lox+hix)/2, ctry = (loy+hiy)/2;
const X = new Float64Array(n), Y = new Float64Array(n), Z = new Float64Array(n);
for (let i=0;i<n;i++){
X[i] = (Q[i*3]-ctrx)*scale + size/2;
Y[i] = size - ((Q[i*3+1]-ctry)*scale + size/2);
Z[i] = Q[i*3+2];
}
const zbuf = new Float64Array(size*size).fill(-Infinity);
const nbuf = new Float64Array(size*size*3);
const mask = new Uint8Array(size*size);
// painter order: far -> near, matching the numpy reference
const idx = [], area = new Float64Array(nt);
for (let t=0;t<nt;t++){
const a=tris[t*3],b=tris[t*3+1],c=tris[t*3+2];
const ar=(X[b]-X[a])*(Y[c]-Y[a])-(X[c]-X[a])*(Y[b]-Y[a]);
area[t]=ar; if(ar<0) idx.push(t);
}
idx.sort((p,q)=>{
const zp=Z[tris[p*3]]+Z[tris[p*3+1]]+Z[tris[p*3+2]];
const zq=Z[tris[q*3]]+Z[tris[q*3+1]]+Z[tris[q*3+2]];
return zq-zp; // descending, == numpy argsort(-z)
});
for (const t of idx) {
const a=tris[t*3],b=tris[t*3+1],c=tris[t*3+2];
const x0=X[a],x1=X[b],x2=X[c], y0=Y[a],y1=Y[b],y2=Y[c];
let xmin=Math.max(Math.floor(Math.min(x0,x1,x2)),0);
let xmax=Math.min(Math.ceil(Math.max(x0,x1,x2)),size-1);
let ymin=Math.max(Math.floor(Math.min(y0,y1,y2)),0);
let ymax=Math.min(Math.ceil(Math.max(y0,y1,y2)),size-1);
if(xmax<xmin||ymax<ymin) continue;
const d=area[t];
for(let py=ymin;py<=ymax;py++){
const gy=py+0.5;
for(let px=xmin;px<=xmax;px++){
const gx=px+0.5;
const w0=((x1-x0)*(gy-y0)-(gx-x0)*(y1-y0))/d;
const w1=((x2-x1)*(gy-y1)-(gx-x1)*(y2-y1))/d;
const w2=1-w0-w1;
if(w0<0||w1<0||w2<0) continue;
const zz=w2*Z[a]+w0*Z[b]+w1*Z[c];
const o=py*size+px;
if(zz>zbuf[o]){
zbuf[o]=zz; mask[o]=1;
nbuf[o*3]=fn[t*3]; nbuf[o*3+1]=fn[t*3+1]; nbuf[o*3+2]=fn[t*3+2];
}
}
}
}
// normalise depth over the mask
let zlo=Infinity, zhi=-Infinity;
for(let i=0;i<size*size;i++) if(mask[i]){const z=zbuf[i]; if(z<zlo)zlo=z; if(z>zhi)zhi=z;}
const depth=new Float64Array(size*size);
const rng2=Math.max(zhi-zlo,1e-9);
for(let i=0;i<size*size;i++) if(mask[i]) depth[i]=(zbuf[i]-zlo)/rng2;
return {mask, depth, normal:nbuf, size};
}
var KYS={loadModel:loadModel,identity:identity,verts:verts,lookAt:lookAt,rasterize:rasterize,splitmix64:splitmix64,gauss:gauss,clamp:clamp,f16:f16};
/* --- marks.js --- */
/* Canonical mark-scale mapping. Shared by node tools AND the browser studio.
*
* Rules learned the hard way:
* - cell/grid parameters MUST be integers (cellMean's integral image collapses
* on fractional box sizes -> 0% ink)
* - counts scale by 1/sc^2 (area), spacings by sc (length)
* - every value is clamped so no algorithm degenerates at the extremes
*/
/* Each method's NATIVE pitch, as originally designed. markParams scales these
* rather than handing every method the same number — a shared `spacing` blew
* out methods designed tight (crosshatch wants 6, not 14). */
const BASE = {
crosshatch:6, engraving:6, dashes:5, vertical:7, moire:7, woodcut:9,
domainwarp:10, chevron:11, warpbands:11, diagonal:12, sinebands:12,
hachure:9, quiver:7, rain:3, bars:9, seismo:13, ridgeline:13,
};
function pitchFor(name, sc){
const b = BASE[name];
return b === undefined ? undefined : Math.max(b*sc, 1.5);
}
function markParams(sc){
const I=(v,lo,hi)=>Math.max(lo,Math.min(hi,Math.round(v))); // integer, clamped
const Fl=(v,lo,hi)=>Math.max(lo,Math.min(hi,v)); // float, clamped
return {
// --- integer cell / grid sizes (MUST be whole numbers) ---
cell: I(7*sc, 2, 40),
cw: I(14*sc, 3, 60),
chh: I(7*sc, 2, 30),
mod: I(8*sc, 2, 40),
step: I(7*sc, 2, 30),
minc: I(4*sc, 2, 24),
dot: I(2*sc, 1, 10),
band: I(13*sc, 3, 50),
// --- float spacings (fractional is fine) ---
spacing: Fl(14*sc, 2, 60),
sp: Fl(7*sc, 1.5, 30),
dash: Fl(9*sc, 2, 40),
pitch: Fl(3.1*sc, 0.8, 14),
// --- inverse: fewer/larger as scale rises ---
rings: Fl(30/sc, 4, 90),
turns: Fl(34/sc, 4, 100),
bands: Fl(17/sc, 3, 50),
rows: I(60/sc, 6, 140),
levels: I(26/sc, 4, 60),
count: I(150/sc, 12, 400),
petals: I(20/sc, 3, 60),
sides: I(6, 3, 12),
// --- counts scale by area ---
sites: I(1500/(sc*sc), 40, 4000),
lines: I(2600/(sc*sc), 60, 6000),
blots: I(1500/(sc*sc), 40, 4000),
walkers: I(2600/(sc*sc), 80, 6000),
curves: I(150/(sc*sc), 10, 400),
// --- density-style (need explicit handling) ---
density: Fl(0.42*sc*sc*0.55, 0.06, 1.0),
grain: Fl(0.55, 0.2, 1.6),
cols: I(3*sc, 2, 20),
};
}
/* --- render.js --- */
/* Canonical render entry point.
*
* RESOLUTION AND MARK SCALE ARE COUPLED. A saved composition is defined at a
* reference size (REF). Rendering at any other size multiplies the mark scale
* by size/REF, so marks keep the same PROPORTION of the head — the image looks
* the same at thumbnail size, but has more pixels describing each mark when
* you zoom in.
*
* Without this, doubling the canvas halves the apparent mark size and you get
* a different picture, not a sharper one.
*/
'use strict';
const REF=300; // the size mark scales were tuned against
function paramsFor(v, size, name){
const k = size/REF; // resolution multiplier
/* `mark` and `scale` are the same axis under two names: derive() emits
* `mark`, the studio and canonical.json store `scale`. Accept BOTH.
*
* They were previously not reconciled here, so every seed-derived token —
* batches, simulate, render_onchain and the on-chain entry point — silently
* rendered at mark 1.0 no matter what its seed said, while the studio (which
* stores `scale`) honoured it. That is why studio output and token output
* diverged, and why sweeping `mark` appeared to do nothing. */
const ms = (v.scale !== undefined ? v.scale
: v.mark !== undefined ? v.mark
: 1) * k; // marks grow with the canvas
const p = markParams(ms);
/* markParams divides counts by ms^2 so that BIGGER MARKS mean FEWER of them.
* But when ms grew only because the canvas grew, the count must be restored:
* the canvas has k^2 more pixels, so multiply the area-based counts back by
* k^2. Otherwise a 1080px render has 13x fewer voronoi sites than a 300px
* one and turns into a different (much emptier) picture. */
const a = k*k;
for (const key of ['sites','lines','blots','walkers','curves']) {
if (p[key] !== undefined) p[key] = Math.round(p[key]*a);
}
/* stipple/phyllotaxis/spiraldots use ms directly for dot radius; their
* density must likewise not thin out with resolution. */
if (p.density !== undefined) p.density = Math.min(p.density*a, 1);
/* isoline-family: level count must not fall with resolution, or contours
* spread apart and the image empties out. */
for (const key of ['levels','rows','count','petals']) {
if (p[key] !== undefined) p[key] = Math.max(Math.round(p[key]*k), 3);
}
/* per-method native pitch overrides the shared spacing */
const pf = name ? pitchFor(name, ms) : undefined;
if (pf !== undefined) { p.spacing = pf; p.sp = pf; }
return Object.assign(p, {
ms,
light_dir:v.light_dir, light_elev:v.light_elev, soft:v.soft,
white:v.white, gamma:v.gamma, seed:v.seed,
flowseeds: Math.round(4200*a),
});
}
/* Line-drawing methods hardcode ~1px stroke widths. When the canvas grows,
* those strokes stay 1px and the image thins out. Rather than thread a line
* width through 80+ algorithms, dilate the result by the resolution factor —
* a 1px line at 300px becomes a 2px line at 600px, which is what "same picture,
* more pixels" actually means. Only applies to methods that draw thin strokes;
* area-filling methods (halftone, dither) are already invariant. */
/* Derived EMPIRICALLY: these are the methods whose coverage tracks the
* reference more closely WITH dilation than without. Do not hand-edit —
* re-measure if algorithms change (see _thinaudit.js).
*
* RE-MEASURED after the canvas gained padding. The old 17-method set was
* calibrated when artwork == canvas (full bleed). Once the artwork is inset
* — 556px inside a 1000px canvas for a 50% head — strokeRadius() jumps from
* 0 to 1, so dilation switches on for the first time and FATTENS these
* methods by 1.5x-2.7x. Re-running the same coverage test at 556px artwork
* against the 420px full-bleed reference, 15 of the 17 land CLOSER to the
* reference with dilation OFF. Confirmed by eye, not just by coverage:
* ridgeline's 1px scanlines merged into blobs, seismo lost its structure,
* bubbles' circles closed up, scales went nearly solid.
*
* Only isolines and splatter still earn it — both draw sparse strokes with
* real whitespace between them, which is the case dilation was meant for.
*
* Note strokeRadius() is a STEP function (round(size/REF)-1): it flips 0->1
* at 450px artwork and 1->2 at 750px, so quality changes discontinuously
* with the inset. Re-measure if the default inset moves. */
const THIN = new Set(["isolines", "splatter"]);
function dilate(ink, S, r){
if(r<1) return ink;
const out=new Float64Array(S*S);
for(let y=0;y<S;y++)for(let x=0;x<S;x++){
if(ink[y*S+x]<=0.5) continue;
for(let dy=-r;dy<=r;dy++)for(let dx=-r;dx<=r;dx++){
if(dx*dx+dy*dy>r*r+0.5) continue;
const yy=y+dy,xx=x+dx;
if(yy<0||yy>=S||xx<0||xx>=S) continue;
out[yy*S+xx]=1;
}
}
return out;
}
function strokeRadius(name, size){
if(!THIN.has(name)) return 0;
return Math.max(Math.round(size/REF)-1, 0);
}
var KYSR={paramsFor:paramsFor,REF:REF,dilate:dilate,strokeRadius:strokeRadius,THIN:THIN};
/* --- derive.js --- */
/* Seed -> full parameter set. Pure function, deterministic, matches on-chain.
*
* seed = keccak256(tokenId, burner, blockhash) -- 32 bytes
* Every parameter is drawn from successive slices of an expanded keystream,
* so the whole artwork is a deterministic function of one 32-byte value.
*/
'use strict';
/* keccak256 — the SAME function Solidity uses. In node this resolves the
* bundled implementation; in the browser build the bundler inlines it and this
* require line is stripped, leaving the global KECCAK. */
const KECCAK=root.KECCAK;
/* Keystream: keccak256(seed ++ uint32be(counter)), consumed 4 bytes at a time.
*
* This MUST match the contract. It previously used a sha256 stand-in, which
* agreed with Solidity's keccak256 on only 1.3% of seeds -- i.e. chance -- so
* the Method trait named a different method than the artwork drew. */
function keystream(seedHex, n) {
const seed = KECCAK.hexToBytes(seedHex);
const out = [];
let ctr = 0;
while (out.length < n) {
const msg = new Uint8Array(seed.length + 4);
msg.set(seed, 0);
msg[seed.length] = (ctr >>> 24) & 255;
msg[seed.length + 1] = (ctr >>> 16) & 255;
msg[seed.length + 2] = (ctr >>> 8) & 255;
msg[seed.length + 3] = ctr & 255;
const h = KECCAK.keccak256(msg);
for (let i = 0; i + 4 <= h.length && out.length < n; i += 4) {
const v = ((h[i] << 24) | (h[i+1] << 16) | (h[i+2] << 8) | h[i+3]) >>> 0;
out.push(v / 4294967296);
}
ctr++;
}
return out;
}
/* Parameter ranges — must match the contract exactly. */
const RANGES = {
/* RESTORED to the full [0.50, 2.25]. It was briefly narrowed to
* [0.70, 1.40] because the top end looked "sparse and washed out" — but
* that was measured while render.js's THIN set was dilating 17 methods by
* 1.5x-2.7x at the inset artwork size. Coarse marks were being fattened
* into blobs and then read as degenerate. With THIN corrected to just
* {isolines, splatter}, the full range is clean: swept across all 80
* methods at the token's 556px artwork, NOTHING clogs solid (>97% ink) at
* 0.50 and nothing washes out (<3%) at 2.25 except `maze`, which already
* carries a per-method CLAMPS entry.
*
* The range is the expressive axis of the whole roster — fine texture at
* the low end, coarse abstraction at the high end. Narrowing it to +/-40%
* around 1.0 collapses most of that. Re-validate with a full sweep before
* changing it again, and check what dilation is doing first. */
mark : [0.50, 2.25], // mark scale — fine texture <-> coarse abstraction
face_shape : [0.15, 0.95],
rotate : [-70, 85 ],
pitch : [ -8, 8 ],
perspective: [0.0, 0.90],
light_dir : [-100, 100],
light_elev : [-15, 75 ],
soft : [0.05, 0.70],
/* FLOORS RAISED 0.02 -> 0.12 (white) and 0.60 -> 0.95 (gamma).
*
* The floor governs how DARK a token may get: `tone()` computes
* (t-white)/(1-white) then pow(.,gamma), so a low floor lets the head fill
* in as a featureless white mass. At the old floor, pinning all 80 methods
* to it gave a median of 48.6% ink with 18 methods over 70% — solid
* silhouettes with texture only at the edges.
*
* Raising the FLOOR is a much stronger lever than widening the ceiling:
* ceiling work moved over-filled tokens 7 -> 4, this moves 18 -> 3 at the
* floor itself. Measured over all 80 methods pinned to the floor:
*
* floor median max >70%
* w0.02 g0.60 48.6% 93.0% 18 (old)
* w0.12 g0.95 34.4% 83.3% 3 (this)
* w0.22 g1.30 22.1% 75.0% 2 (breaks up shadow detail)
*
* Chosen for keeping full tonal range — tokens can still be genuinely dark,
* just not featureless. Per-method CLAMPS below may still floor LOWER where
* a method needs it (tally, morse, crossgrid, maze, diamond). */
white : [0.12, 0.30],
gamma : [0.95, 1.60],
};
const ORDER = ['mark','face_shape','rotate','pitch','perspective',
'light_dir','light_elev','soft','white','gamma'];
/* Per-algorithm clamps for the few whose legibility is fragile. */
/* Per-method mark ceilings, as ABSOLUTE values against RANGES.mark.
* These were remapped when the range was briefly narrowed to [0.70,1.40] and
* are now restored alongside it. `maze` is the one method that genuinely
* degenerates at the top of the global range (2.2% ink at 2.25), which is
* what its 1.40 ceiling is for. */
const CLAMPS = {
tally:{white:[0.02,0.14],soft:[0.05,0.40]},
morse:{white:[0.02,0.20]},
crossgrid:{white:[0.02,0.20], mark:[0.50,1.90]},
randomwalk:{mark:[0.50,1.90]},
splatter:{mark:[0.50,1.90]},
/* these six lose legibility above the midpoint of the mark range */
maze:{white:[0.02,0.18], mark:[0.50,1.40]},
truchet:{mark:[0.50,1.40]},
ascii:{mark:[0.50,1.40]},
lowpoly:{mark:[0.50,1.40]},
quadtree:{mark:[0.50,1.40]},
wolfram:{mark:[0.50,1.40]},
/* ---- coverage clamps, measured at the token's own 500px inner canvas
* (1000px @ inset 0.50) over 8 seeds x the axis quartiles.
* These shift the white/gamma WINDOW toward the corrective end while
* keeping it a window — a fixed pair would make every token of the
* method identical. Only methods a clamp actually rescues are listed;
* maze, rain and scales resist white/gamma and need algorithm work. */
diamond:{white:[0.02,0.15], gamma:[0.60,1.05]}, /* 13.2% -> 19.0% */
offsets:{white:[0.06,0.30], gamma:[0.75,1.60]}, /* 55.1% -> 53.2% */
ridgeline:{white:[0.06,0.30], gamma:[0.75,1.60]}, /* 56.8% -> 54.0% */
/* high mark thins these out */
scribble:{mark:[0.50,1.80]},
lissajous:{mark:[0.50,1.80]},
rain:{mark:[0.50,1.90]},
};
/* keccak256(seed ++ uint32(0)) % n -- the contract's method selector. */
function methodIndex(seedHex, n) {
const seed = KECCAK.hexToBytes(seedHex);
const msg = new Uint8Array(seed.length + 4);
msg.set(seed, 0);
const h = KECCAK.keccak256(msg);
let r = 0n;
for (let i = 0; i < 32; i++) r = (r << 8n) | BigInt(h[i]);
return Number(r % BigInt(n));
}
const ASCII_RAMPS = ['classic','code','letters','blocks','dense','numeric','minimal'];
/* ---- ink floor -----------------------------------------------------------
* white and gamma both push the same way: LOW white + LOW gamma = far more ink
* = a mostly-white image (measured 58-80% coverage when both bottom out).
*
* Leave both alone unless their COMBINED position is too low, then lift the
* pair. Most tokens are untouched; only the ones that would wash out get
* nudged.
*
* EXPORTED and shared. This logic was previously inline here AND duplicated in
* studio.html, with nothing testing that the two agreed — see REVIEW.md 4b.
* Mutates `p` in place and returns it. */
const INK_FLOOR = 0.70; // min combined position (of 2.0) -> ~44% ink ceiling
function applyInkFloor(p, rr) {
const [wlo, whi] = rr.white, [glo, ghi] = rr.gamma;
const wPos = (p.white - wlo) / (whi - wlo);
const gPos = (p.gamma - glo) / (ghi - glo);
const sum = wPos + gPos;
if (sum >= INK_FLOOR) return p;
/* Lift GAMMA first. Some methods have a deliberately narrow white range
(tally, maze, morse, crossgrid need low white space to stay legible), so
white often has little room and gamma must carry the shortfall. */
let w = wPos, g = gPos, need = INK_FLOOR - sum;
const gTake = Math.min(need, 1 - g);
g += gTake; need -= gTake;
if (need > 0) w = Math.min(w + need, 1);
p.white = wlo + w * (whi - wlo);
p.gamma = glo + g * (ghi - glo);
return p;
}
/* Ranges for one method, with its clamps applied. */
function rangesFor(algo) {
return Object.assign({}, RANGES, CLAMPS[algo] || {});
}
function derive(seedHex, algoNames) {
const k = keystream(seedHex, 20);
/* Method index is an INTEGER reduction of the first keccak word, matching
* KYSRenderer's `keccak256(seed, uint32(0)) % N` exactly. Do not reintroduce
* float arithmetic here: floor(k[0]*N) does NOT agree with a modulo. */
const algo = algoNames[methodIndex(seedHex, algoNames.length)];
const rr = rangesFor(algo);
const p = { algo, identity_seed: Math.floor(k[1] * 2147483647) };
ORDER.forEach((key, i) => {
const [lo, hi] = rr[key];
p[key] = lo + k[2 + i] * (hi - lo);
});
applyInkFloor(p, rr);
/* per-method sub-parameters */
if (algo === 'ascii') {
p.ramp = ASCII_RAMPS[Math.floor(k[15] * ASCII_RAMPS.length) % ASCII_RAMPS.length];
}
return p;
}
/* How many distinct visually-meaningful outcomes? Quantise each axis to the
* granularity at which a change is actually perceptible. */
const QUANT = {
face_shape:0.08, rotate:6, pitch:4, perspective:0.15,
light_dir:10, light_elev:8, soft:0.08, white:0.04, gamma:0.20,
};
function bucket(p) {
return p.algo + '|' + ORDER.map(k =>
Math.round(p[k] / QUANT[k])).join(',');
}
var KYSD={derive:derive,keystream:keystream,methodIndex:methodIndex,RANGES:RANGES,ORDER:ORDER,CLAMPS:CLAMPS,ASCII_RAMPS:ASCII_RAMPS};
root.KYS=KYS; root.KYSR=KYSR; root.KYSD=KYSD;
})(typeof self!=="undefined"?self:this);
/* --- entry.js --- */
/* KYS token page entry point.
*
* The page arrives with three globals set by KYSRenderer._page():
* window.KYS_SEED 0x-prefixed 32-byte seed
* window.KYS_STORE EthFS FileStore address
* window.KYS_MODEL model filename, e.g. "gnm-k32.bin"
*
* It fetches the model straight from chain -- getFile() for the pointer list,
* then one eth_getCode per chunk. eth_getCode is UNMETERED, so this costs the
* viewer nothing and no node has to serve a large eth_call. (EthFS.read() on
* the whole 2.4MB file measures 31.8 BILLION gas and is unservable.)
*
* Requires kys-engine.js and kys-roster.js to have loaded first.
*/
(function (root) {
'use strict';
var SEL_GETFILE = '0xe0876aa8'; // getFile(string)
function el(id) { return root.document.getElementById(id); }
function status(msg) {
var s = el('s');
if (s) s.textContent = msg;
}
/* ---------------------------------------------------------------- rpc */
/* Provider order: an injected wallet first (no rate limits, no third party),
* then public fallbacks. The artwork must not depend on any single host. */
function providers() {
var out = [];
if (root.ethereum) out.push({ kind: 'injected', p: root.ethereum });
var urls = root.KYS_RPC ? [].concat(root.KYS_RPC) : [];
urls.push('https://ethereum-rpc.publicnode.com');
urls.push('https://eth.llamarpc.com');
urls.push('https://rpc.ankr.com/eth');
for (var i = 0; i < urls.length; i++) out.push({ kind: 'http', url: urls[i] });
return out;
}
function call(prov, method, params) {
if (prov.kind === 'injected') {
return prov.p.request({ method: method, params: params });
}
return root.fetch(prov.url, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: method, params: params })
}).then(function (r) { return r.json(); })
.then(function (j) {
if (j.error) throw new Error(j.error.message);
return j.result;
});
}
/* --------------------------------------------------------------- abi */
function encGetFile(name) {
var hex = '', i;
for (i = 0; i < name.length; i++) {
var c = name.charCodeAt(i);
hex += (c < 16 ? '0' : '') + c.toString(16);
}
var byteLen = name.length;
var pad = (32 - (byteLen % 32)) % 32;
return SEL_GETFILE
+ pad64(32) + pad64(byteLen) + hex + repeat('00', pad);
}
function pad64(n) {
var h = n.toString(16);
return repeat('0', 64 - h.length) + h;
}
function repeat(s, n) { var o = ''; while (n-- > 0) o += s; return o; }
/* File = (uint256 size, Content[] contents); Content = (address,uint32,uint32) */
function decFile(hex) {
var h = hex.slice(2);
function w(i) { return h.slice(i * 64, (i + 1) * 64); }
function n(i) { return parseInt(w(i), 16); }
var structOff = n(0) / 32;
var size = n(structOff);
var arrOff = structOff + n(structOff + 1) / 32;
var len = n(arrOff);
var contents = [];
for (var i = 0; i < len; i++) {
var b = arrOff + 1 + i * 3;
contents.push({ pointer: '0x' + w(b).slice(24), start: n(b + 1), end: n(b + 2) });
}
return { size: size, contents: contents };
}
function hexToBytes(hex, start, end) {
var s = start * 2 + 2, e = end * 2 + 2;
var out = new Uint8Array(end - start);
for (var i = 0; i < out.length; i++) {
out[i] = parseInt(hex.substr(s + i * 2, 2), 16);
}
return out;
}
/* -------------------------------------------------------------- fetch */
function fetchModel(prov, name, onProgress) {
return call(prov, 'eth_call',
[{ to: root.KYS_STORE, data: encGetFile(name) }, 'latest'])
.then(function (raw) {
var file = decFile(raw);
var parts = new Array(file.contents.length);
var total = file.contents.length;
/* Sequential: 101 parallel requests get rate-limited by every public
* node. This is slower but it actually completes. */
var chain = Promise.resolve();
file.contents.forEach(function (c, i) {
chain = chain.then(function () {
return call(prov, 'eth_getCode', [c.pointer, 'latest']);
}).then(function (code) {
parts[i] = hexToBytes(code, c.start, c.end);
if (onProgress) onProgress(i + 1, total);
});
});
return chain.then(function () {
var out = new Uint8Array(file.size), off = 0;
for (var i = 0; i < parts.length; i++) {
out.set(parts[i], off); off += parts[i].length;
}
if (off !== file.size) throw new Error('size mismatch ' + off + '/' + file.size);
return out;
});
});
}
/* Try each provider in turn. A dead RPC must not mean a dead artwork. */
function fetchWithFallback(name, onProgress) {
var provs = providers(), i = 0;
function attempt() {
if (i >= provs.length) throw new Error('all RPC providers failed');
var p = provs[i++];
return fetchModel(p, name, onProgress).catch(function (e) {
if (i < provs.length) { status('retrying via another node...'); return attempt(); }
throw e;
});
}
return attempt();
}
/* ------------------------------------------------------------- render */
/* The payload is "KYSL" + uint32 headerLen + licence text + the KYSM model.
* Skip the header; the licence rides along on chain but is not model data. */
function modelBytes(payload) {
if (payload[0] === 0x4b && payload[1] === 0x59 &&
payload[2] === 0x53 && payload[3] === 0x4c) {
var len = payload[4] | (payload[5] << 8) | (payload[6] << 16) | (payload[7] << 24);
return payload.subarray(8 + len);
}
return payload; // raw KYSM, no licence header
}
function draw(payload) {
var bytes = modelBytes(payload);
var ab = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
var M = root.KYS.loadModel(ab);
var names = Object.keys(root.KYS_A);
var p = root.KYSD.derive(root.KYS_SEED, names);
var canvas = el('c');
var S = canvas.width;
var ident = root.KYS.identity(p.identity_seed, p.face_shape, M.K);
var V = root.KYS.verts(M, ident);
var F = root.KYS.rasterize(root.KYS.lookAt(V, p.rotate, p.pitch), M.tris, S, p.perspective);
var ink = root.KYS_A[p.algo](F, root.KYSR.paramsFor(p, S, p.algo));
var ctx = canvas.getContext('2d');
var img = ctx.createImageData(S, S);
var d = img.data;
for (var i = 0; i < S * S; i++) {
var v = ink[i] > 0.5 ? 0 : 255;
d[i * 4] = d[i * 4 + 1] = d[i * 4 + 2] = v;
d[i * 4 + 3] = 255;
}
ctx.putImageData(img, 0, 0);
root.KYS_METHOD = p.algo;
root.KYS_PARAMS = p;
root.KYS_DONE = true; // render_onchain / headless tests poll this
status('');
}
function main() {
if (!root.KYS_SEED) { status('no seed'); return; }
status('fetching model from chain...');
fetchWithFallback(root.KYS_MODEL || 'gnm-k32.bin', function (i, n) {
if (i % 10 === 0 || i === n) status('chunk ' + i + ' / ' + n);
}).then(function (payload) {
status('rendering...');
/* yield once so the status paints before the synchronous render blocks */
root.setTimeout(function () {
try { draw(payload); }
catch (e) { status('render failed: ' + e.message); root.KYS_ERROR = String(e); }
}, 0);
}).catch(function (e) {
status('failed: ' + e.message);
root.KYS_ERROR = String(e);
});
}
if (root.document && root.document.readyState === 'loading') {
root.document.addEventListener('DOMContentLoaded', main);
} else if (root.document) {
main();
}
root.KYS_MAIN = main;
})(typeof self !== 'undefined' ? self : this);
a clear dividefpk(til,10)(d(1R2R,23),40)a(1R2R,17)v1fpk(til,10)(d(1R2R,23),10)a(1R2R,80)v1fpk(til,10)(d(1R2R,23),23)a(1R2R,26)v1T(i,100)A(d(1R2R,23),23)AqdrrzZxA(d(1R2R,23),23)15AR15R1zZArrrrdrrti(l,84)(i,111)pkkkkzZfx1vKKKK
! # ' ( ) * + ( - . / 0 1 - . 2 3 & ) 3 5 6 7 9 4 7 ; < = < 6 5 > ? > ; : A B C E F G H I F B A K J K M O 9 8 Q O N S Q P D G S U V W Y Z [ \ ] Z W " # _ _ ^ ] a L M c a ` e f g i + f j k j i h b e d m n o l o q v *!+!,!)!,!.!0!*!)!/!)!-!3!0!/!2!/!1!9!6!5!8!5!7!<!9!8!;!8!:!?!@!A!>!A!C!E!?!>!D!>!B!TH!I!J!G!J!L!N!H!G!M!G!K!Q!N!M!S!Q!P!P!M!O!R!P!T!W!OZ![!\!Y!\!^!`!Z!Y!_!Y!]!c!d!e!b!e!g!i!c!b!h!b!f!l!i!h!k!h!j!5 r!o!n!q!n!p!u!r!q!t!q!s!x!u!t!w!t!v!?"@"A"C"?">"@"D"E"D"F"J"M"N"O"L"O"H"L"G"Q"R"S"P"S"U"H"Q"P"I"P"T"W"X"Y"V"Y"R"N"W"V"O"V"Q"["\"]"Z"]"_"R"["Z"S"Z"^"a"b"c"`"c"\"X"a"`"Y"`"["e"f"g"d"g"i"\"e"d"]"d"h"k"l"m"j"m"f"b"k"j"c"j"e"o"p"q"p"r"s"n"q"g"q"s"i"u"v"w"v"o"n"t"w"m"w"n"f"y"z"{"|"}"z"z"p"o"}"r"p"##t"l"%###"#&#u"t"'#&###(#)#k"*#+#)#)#"#l"+#$#"#,#-#"@#A#?#C#@#>#B#>#K#J#J#L#N"K#M#L#L#N#W"M#O#N#N#P#X"O#Q#P#P#R#a"Q#S#R#R#(#b"S#*#(#X#Y#Z#[#\#Z#^#[#Y#]#Y#X#a#^#]#`#]#_#d#e#f#h#d#c#j#h#g#k#l#e#k#m#n#p#m#k#o#k#d#r#p#o#q#o#h#m#s#t#s#u#v#w#x#u#p#w#s#y#z#x#y#w#p#|#}#~#}#$$$-$/$0$1$.$1$3$4$5$0$6$7$5$9$:$;$:$<$=$8$;$?$;$=$@$B$C$D$C$E$F$A$D$:$D$F$<$<$G$H$G$I$J$=$H$K$H$J$L$E$M$N$M$O$P$F$N$G$N$P$I$4#5#R$7#4#Q$:#;#U$5#:#T$`$a$b$a$c$d$f$_$b$e$b$d$j$h$i$a$i$j$c$l$m$n$p$k$n$l$k$r$k$p$s$v$t$n$m$u$o$n$t$d$c$x$w$x$m$d$w$y$w$l$q$u$j$z$x$x$z$u$|$}$~$}$`$_${$~$V%S%R%U%R%T%X%Y%W%T%Y%]%^%_%`%a%^%^%b%c%a%d%b%#g%e%e%h%`%g%i%h%h%j%a%i%k%j%j%l%d%k%m%l%n%v%x%y%x%z%{%z%|%}%y%{%*&)&*&&&,&-&.&+&.&5&1&5&6&3&0&7&8&-&8&9&.&9&:&*&:&;&&&;&<&#&<&=&Q&R&S&l%Q&P&T&U&R&m%T&Q&c%b%W&d%P&W&V&W&Y&P&S&Y&$B%`&N&a&N%M%C%a&`&+"h&f&h&F&
"
"k&j&k&B& "
"j&i&j&D&
"#g k y&k h A&{&|&}&&)'`!*'+','d!c! 'j!f!-'f!g!0'a!]!2'0'/'OA'?'<';'C'E'B'C'!<'B'G' '-'-'.'H'#N'V'W'V'U'T'O'W'$_'X'_'a'Z'R'd'e'd'f'g'S'e'g%e'g'i%]'h'i'\'i'%j'^'f'l'm'l'n'o'g'm'k%m'o'm%q'r's'%q'p'p's'u't'u'w'n'z'{'z'|'}'o'{'T&{'}'U&q(b("(#(q(q(r(c(#($(r(r(s(d($(%(s(s(t(](%(&(t(t(u(Z(&('(u(u(v([('(((v(v(w(\((()(w(w(x(a()(*(x(x(y(^(*(+(y(y(z(_(+(,(z(z({(`(,(-({(|(}(i(.(/(}(}(~(j(/(0(~(~( c#f#\#g#c#[#i#g#^#(I$n&m&J$m&o&O$q&p&P$p&n&9%8%s& 9%r&L$o&t&u&?%>%v&>%Wu&w&A%w&x&C%z&}&K%~&z&J%5'6'$'+')'6'*''')'1'^!X![!5'3'6' C';'Z ;'>'[ D'C']
'I'
'G'A'@'?'F'A'G'F'K'2'1' ()<!;!');!=!d!`!_!e!_!a!+)&"9"+"-";"%"+":"-"/"<"/)"6).#,#8)6$4$7)4$/$:);).$;)9)/$&X U H)I)0&I)J)7&0)X)Y)Z)T)Z)Y)[)[)\))*'`!d!g!a!0'0'2'H'`(f){(g)f)-(c)g)h)(6$8)V [ >'H'2'K'!*),)1 8)7)- 7)9)& 2 ;). 9);)<)' & =)* ' u!x!L)r!u!K)Q)P)~!P)O){!5(I)H)o)p)q)A!()')q)q)r)C!')))r)r)s)%-)>)t)t)u)A#>)6)u)u)v)?#6)5)v)v)w))'*)***+*-*)*(*/*0*1*.*1*3*5***8*9*8*5*4*;*7*6*;*:*0*=*>*?*2*3*?*@*B*C*B*D*E*D*-*,*F*=*<*I*H*I*K*J*K*M*L*M*O*N*O*Q*P*Q*S*R*S*U*T*U*W*Y*Z*[*]*Y*X*X*[*_*\*X*^*b*c*d*f*b*a*a*d*h*e*a*g*(*+*k*,*(*j*j*k*n*l*j*m*p*q***r*p*)*t*u*v*x*t*s*s*v*q*w*s*p*z*{*|*~*z*y*y*|*u*}*y*t*1*0*{*3*1*z**!+#+"+c*m*n*"+$+#+b*o*m*#+%+$+f*A+D+A+@+B+A+D+D+C+G+E+D+F+F+G+J+H+F+I+I+J+M+K+I+L+L+M+P+N+L+O+O+P+S+Q+O+R+R+S+V+T+R+U+|/+X+/+.+Y+.+-+Z+-+,+[+,+++\+++*+]+*+)+_+^+)+a+
++b+
+
+c+`*
+d+\*`*e+]*\*f+l+i+h+k+h+j+l+k+o+k+m+p+r+n+o+q+o+p+u+r+q+t+q+s+u+t+x+t+v+y+w+x+{+x+y+|+z+{+~+{+|+*J*L*h*L*N*g*N*P*i*P*R*1+C+@+2+G+C+3+J+G+4+M+J+5+P+M+6+S+P+7+V+S+9+,7+6+^,9+8+r,=+<+s,>+=+t,?+>+v,$[,\,`,;+:+a,<+;+4
f)`({(g){(-(e(e)h)!7)- 1 9). - ;):)& ;)2 . & :)<)' <)=){!O)L)K)u!K)M)r!~!(f)m(f)g)|(g)c).(c)(n)o)n)I)A!@!p)q)p)()C!A!q)r)q)')%t)u)t)>)?#A#u)v)u)6))+*(*)*(*,*-*1*.*/*3*2*.*6*7*9*+***4*9*8*6*:*;*0*/*;*?*<*=*?*>*2*C*A*@*E*C*B*,*E*D*<*G*F*I*H*K*J*H*M*L*J*O*N*L*Q*P*N*S*R*P*U*T*R*W*V*T*[*X*Y*X*\*]*_*^*X*^*`*\*d*a*b*a*e*f*h*g*a*g*i*e*k*j*(*j*l*,*n*m*j*m*o*l***)*p*)*-*r*v*s*t*s*w*x*q*p*s*p*r*w*|*y*z*y*}*~*u*t*y*t*x*}*{*z*1*z*~*3**c*"+!+"+n*c*b*#+"+#+m*b*f*$+#+$+o*f*@+C+D+D+E+B+G+F+D+F+H+E+J+I+F+I+K+H+M+L+I+L+N+K+P+O+L+O+Q+N+S+R+O+R+T+Q+V+U+R+U+W+T+/+X+|.+Y+X+-+Z+Y+,+[+Z+++\+[+*+]+\+)+^+]+)+(+_++`+a+
+a+b+
+b+c+`*c+d+\*d+e+]*e+f+j+h+h+k+l+j+m+k+o+n+l+p+o+k+o+q+r+p+s+q+q+t+u+s+v+t+x+w+u+y+x+t+{+z+w+|+{+x+~+}+z+0+@+0+1+C+1+2+G+2+3+J+3+4+M+4+5+P+5+6+S+6+7+,8+],^,<+a,r,=+r,s,>+s,t,?+t,v,7,h,t r ~, !!!z,"! !,$ U#G,V#B,G,i,m, " # $ " & % % ' $ ) ( + ( , - / . ( . 0 , 2 1 . 1 3 0 5 4 1 4 6 3 8 7 : 7 ; < > = 7 = ? ; A @ = @ B ? D C @ F E H E I J L K E K M I O N Q N R S U T N T V R X W T W Y V [ Z W Z \ Y ^ ] Z ] _ \ a ` c ` d e g f ` f h d j i f i k h ! k m l " l n # p o l o q n s r o r t q v u r u w t y x u x z w | { x { } z
"
#
'
&
)
*
'
(
-
,
/
0
,
-
.
/
3
&
2
3
5
4
7
9
8
7
;
:
=
<
=
5
?
>
?
:
A
@
C
E
D
G
H
E
F
B
J
K
J
L
M
O
N
8
Q
P
N
S
R
P
D
R
S
U
T
W
Y
X
[
\
Y
Z
W
T
#
_
^
_
\
]
a
`
M
c
b
`
e
d
g
i
h
*
+
f
g
k
j
k
h
b
c
d
m
l
o
l
p
q
v
u
S
R
(
X
W
Z
[
Y
Z
^
]
Y
]
_
X
a
`
]
`
b
_
d
c
f
h
g
c
j
i
g
k
d
e
k
l
n
p
o
k
o
h
d
r
q
o
q
j
h
m
n
t
s
t
v
w
s
u
p
m
s
y
w
x
y
r
p
|
{
~
}
~
* + % + - ' P Q K Q S M
c
[
\
g
^
[
i
a
^
b c ] c e _ X
U
V
)
(
'
(
+
*
/
.
-
-
1
0
3
2
.
3
)
&
7
6
5
7
4
9
=
<
;
5
6
<
?
>
:
;
>
C
B
A
G
F
E
F
I
H
K
A
B
M
K
J
8
9
O
N
O
Q
P
Q
S
S
G
D
W
V
U
[
Z
Y
Z
]
\
#
"
W
_
]
^
_
M
L
a
`
a
c
g
f
e
k
j
f
h
i
j
d
e
b
o
n
m
q
o
l
u
u
v
y
w
u
x
x
y
"-
,
-
/
.
-
1
0
1
?
>
?
A
@
>
@
C
(
R
(
*
S
Z
Y
X
Z
\
[
Y
[
^
X
Y
]
]
^
a
_
]
`
f
e
d
c
d
h
g
h
j
e
l
k
n
m
k
k
m
p
d
k
o
o
p
r
h
o
q
t
s
m
v
u
s
u
x
w
s
w
p
x
z
y
p
w
y
~
}
|
% & * ' % + K L P M K Q \
f
c
[
c
g
^
g
i
0
p
] ^ b _ ] c V
[
X
" ) & ' ( ' * / , - - , 0 3 / . 3 2 & 7 4 5 7 8 9 = : ; 5 = < ? : ? > C @ A G D E F E H K J B M L J 8 N O N P Q P R S S R D W T U [ X Y Z Y \ # T W ^ _ ] \ _ M ` a ` b c g d e k g f h k j d c b o l m q p l w u u x y w z x x %%!#!#!&!'!%!(!&!&!,!)!*!.!-!)!)!/!0!-!1!/!/!2!3!1!4!2!2!7!5!5!8!9!7!:!8!8!;!<!:!=!;!A!>!?!C!B!>!>!D!E!B!F!D!D!R# ! J!G!H!L!K!G!G!M!N!K!O!M!M!P!Q!P!R!S!O!T!P!T!U!R!UX!V!\!Y!Z!^!]!Y!Y!_!`!]!a!_!e!b!c!g!f!b!b!h!i!f!j!h!h!k!l!j!m!k!k!2 p!n!n!q!r!p!s!q!q!t!u!s!v!t!t!w!x!v!y!w!w!;"A">"?">"B"C"E"A"@"E"D"I"F"G"K"J"F"F"S"P"Q"U"T"P"P"I"H"T"K"I"Y"V"W"R"Q"V"V"O"N"Q"H"O"]"Z"["_"^"Z"Z"S"R"^"U"S"c"`"a"\"["`"`"Y"X"["R"Y"g"d"e"i"h"d"d"]"\"h"_"]"m"j"k"f"e"j"j"c"b"e"\"c"q"n"o"s"q"p"g"f"n"i"g"q"w"t"u"n"w"v"m"l"t"f"m"w"{"x"y"z"y"|"o"{"z"p"z"}""l""###"#$#%#t"##&###%#'#k"b"(#)#(#*#l"k")#"#)#+#",#-#,#.#-#/#0#1#0#G#D#E#F#E#~"'#G#F#&#F#N"M"J#L#J#K#W"N"L#N#L#M#X"W"N#P#N#O#a"X"P#R#P#Q#b"a"R#(#R#S#Z#W#X#Z#Y#[#Y#]#^#X#_#]#]#`#a#_#b#`#f#c#d#c#g#h#g#i#j#e#d#k#n#l#k#k#o#p#d#h#o#o#q#r#h#j#q#t#n#m#v#t#s#u#s#w#s#m#p#x#w#y#p#r#y#~#{#|#$1$.$/$3$2$.$0$/$4$5$4$6$;$8$9$=$;$:$?$>$8$@$?$;$D$A$B$F$D$C$:$9$A$<$:$D$H$=$<$J$H$G$K$@$=$L$K$H$N$F$E$P$N$M$G$<$F$I$G$N$R$Q$4#Q$S$7#U$T$:#T$R$5#b$_$`$d$b$a$b$e$f$d$g$e$i$h$a$`$h$c$a$i$n$k$l$n$o$p$r$q$l$s$r$k$u$u$t$n$t$v$o$x$w$d$m$l$w$y$g$d$q$y$w$z$j$x$c$j$u$m$x$~${$|$_$~$}$2%0%0%.%-%2%/%.%:T%R%R%U%V%T%W%U%Y%T%Y%[%W%_%\%]%^%]%`%c%_%^%b%^%a%f%#`%f%e%h%e%g%a%`%h%j%h%i%d%a%j%l%j%k%r%D%/%s%r%2%w%t%u%v%u%y%w%v%{%y%x%}%{%z%& &? C B
&
&G F &&#&$&#&'&(&*&)&3&1&5&8&-&0&9&.&-&;&&&*&<&#&&&=&'&#&%S&P&Q&P&d%l%R&Q&T&Q&l%m%W&V&c%W&b%d%Y&X&V&Y&W&P&%X&Z&[&Z&\&]&V&[&"F&G&f&k&j&
"B&D&j&j&i& "D&E&i&l&k&
"C&B&k&&g A&y&k }&z&{&z&~&I!H!N!K!L!
'O!K!','f!-' 'g!.'-']!/'0'/'1'2'3'P!7':'!'"'>';'<'='<'C'B'<'C'D'E'B'E'!-'F'G'H'F'-'G'
'$Z'X'_'e'S'R'g'e'd'g%%m'g'f'o'm'l'k%i%g'm%k%m's'p'q'p'{'o'n'}'{'z'T&m%o'U&T&{'M'~'"(c(b(q(r(q(#(d(c(r(s(r($(](d(s(t(s(%(Z(](t(u(t(&([(Z(u(v(u('(\([(v(w(v(((a(\(w(x(w()(^(a(x(y(x(*(_(^(y(z(y(+(`(_(z({(z(,(i(m(|(}(|(.(j(i(}(~(}(/(k(j(~( J!I!" L!J!V!\![!X!^!\!!\#[#c#[#^#g#^#a#i#0 m&J$I$o&L$J$p&P$O$n&I$P$s&r&9%r&v&A%?%u&C%A%w&K%J%z&J%N%~&N%a&3'$'3'5'6'5'+')'+'*'X!4'1'3'W![!%'$'6'B$8'7''Z ] C'[ Z ;'] ^ D'D'^ A'I'
'F'H'@'F'?'A'1'4'K' &)$);!')()=!))')_!e!d!a!g!e!;":"+":"7"%"<";"-""2)5),#5)6)4$7)8)/$9)7).$2$:)/$.$;):)2$)`)_)d!,'*'0'.'g!H'.'0'
HCV{.8H
VY.fH\VN.#%Z-Mp$(ZBM+% Z+M[<NW$[[VNb#W[ANp#Z,NT%P[NNG&2[4N=&H\V1#{\sN:"d\:NP!=\ ! & $ % + * ) - + ( ( ) / , ( . . / 2 0 . 1 1 2 5 3 1 4 : 9 8 < : 7 7 8 > ; 7 = = > A ? = @ @ A D H G F J H E E F L I E K Q P O S Q N N O U R N T T U X V T W W X [ Y W Z Z [ ^ \ Z ] c b a e c ` ` a g d ` f f g j h f i i j ! k i " $ m # " l l m p n l o o p s q o r r s v t r u u v y w u x x y | z x { { |
===== LICENSE (Apache-2.0, Google GNM) =====
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================================================
Third-Party Component Licenses
================================================================================
A portion of this repository, specifically the tongue animation meshes for the
articulation of the tongue inside of the mouth was adapted from work by Salvador
Medina, which is licensed under the MIT License:
Copyright (c) 2022 Salvador Medina
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
===== THIRD-PARTY NOTICES =====
# Third-Party Notices
This project incorporates material from the third-party works listed below.
---
## GNM (Generative aNthropometric Model) — GNM Head
- **Source:** https://github.com/google/gnm
- **Copyright:** Copyright 2026 Google LLC
- **License:** Apache License, Version 2.0
- **License text:** http://www.apache.org/licenses/LICENSE-2.0
- **Technical report:** https://arxiv.org/abs/2607.23687
### What is used
This project uses the GNM Head statistical model data file
`gnm/shape/data/versions/v3_0/gnm_head.npz`. No GNM source code is
incorporated; the model data is read directly with NumPy.
### Modifications
As required by Section 4(b) of the Apache License 2.0, the following
modifications to the GNM model data have been made in this project. The
distributed binary `proto/chain/gnm_k32.bin` is a **modified subset** of the
original GNM Head model data, NOT the original file:
1. **Vertex subset.** Only vertices belonging to the `skin_exterior` vertex
group are retained (17,821 vertices reduced to 11,460); triangles are
filtered to those fully inside that group and re-indexed.
2. **Basis truncation.** Only the first K = 32 principal identity components
of the 253-component `vertex_identity_basis` are retained. The expression
basis, joint/skinning data, UVs, and all other arrays are omitted.
3. **Quantization.** Template vertex positions are quantized from float64 to
int16 with a scale/offset; the identity basis is reduced from float32 to
float16.
These modifications are performed by `proto/chain/pack_model.py`.
### Citation
```bibtex
@article{ploumpis2026gnmhead,
title={GNM Head: A Generative aNthropometric Model of the human head},
author={Ploumpis, S. and Bednarik, J. and Zoss, G. and Guseinov, R. and
Prasso, L. and Chandran, P. and Boyne, O. and Choutas, V. and
Bolkart, T. and Wang, D. and Chai, M. and Qiu, D. and Winberg, S.
and Rainer, G. and Bridgeman, L. and Vicini, D. and Riviere, J. and
Boetzel, Y. and Koumis, A. and Busch, J. and Herrera, C. and
Still, J. and Ysebert, S. and Lincoln, P. and Escolano, S. O. and
Rhemann, C. and Wood, E. and Beeler, T. and Zafeiriou, S.},
year={2026},
eprint={2607.23687},
archivePrefix={arXiv},
url={https://arxiv.org/abs/2607.23687},
}
```
### On-chain redistribution — UNRESOLVED
`gnm_k32.bin` is intended to be written to Ethereum mainnet via EthFS, where
it becomes **permanent and unmodifiable**. Two consequences that a lawyer, not
an engineer, should sign off on before deploy:
1. Apache 2.0 §4 requires that recipients receive a copy of the licence and
this NOTICE. A file on EthFS carries no such metadata by default — the
licence and notice must be written on-chain alongside the model, or the
distribution arguably does not comply.
2. Compliance defects are **not fixable after the fact**. Nothing on-chain can
be amended or withdrawn.
Neither point has been actioned. See `REVIEW.md`.
### Note on trademarks
Apache License 2.0 Section 6 does not grant trademark rights. "Google" and
"GNM" are used here for attribution and accurate description of origin only.
This project is not endorsed by or affiliated with Google LLC.
## On-chain distribution (resolved)
`gnm_k32.bin` is published to Ethereum via EthFS with the **full Apache-2.0
licence and these notices embedded in the file's own bytes**, ahead of the model
payload. Any reader of the on-chain file receives the licence before the model.
EthFS's own licence field (`extraData` on `createFileFromPointers`) is also set
to `{"license":"Apache-2.0"}`, but it is **not** relied on for compliance: that
value is emitted in an event and is not returned by `getFile()`, so it is
indexer metadata rather than content.
The licence bytes are read from `GNM-main/LICENSE` verbatim at build time.
Verified on a mainnet fork: the licence text and these notices are readable back
out of the registered file. See `contracts/DEPLOY.md`.
===== END NOTICES — MODEL PAYLOAD FOLLOWS =====
KYSMZ:Nq;U[TNf<P[ANW<4rPzQ451Q\P 7SOuO/b7L@Q1D:#?U,%5"?Lr>I?wMM?d<8L><{C+O
the joy of the presentfpk(til,10)(d(1R2R,23),40)a(1R2R,17)v1fpk(til,10)(d(1R2R,23),10)a(1R2R,80)v1fpk(til,10)(d(1R2R,23),23)a(1R2R,26)v1A(5R2R,10)(d(1R2R,17)pl,10)AsKKKt(il.40)(d(1R2R,17)T(l,50)tlpl,10)Azv1deEAqdrqqqqdrzZdrrrAdSKq zdedEdeAdSIASKKK(l,50)AdRfxvJJJJJJJJJJKkkkkmliii
a single orbfpk(til,10)(d(1R2R,23),40)a(1R2R,17)v1fpk(til,10)(d(1R2R,23),10)a(1R2R,80)v1fpk(til,10)(d(1R2R,23),23)a(1R2R,26)v1A(5R2R,10)(d(1R2R,17),10)A(5R2R,10)(d(1R2R,17),10)AsKKKKt(k,80)fxvkj0(i,130)
hot potatopepenT(ij,65)5f(dt(l,4),7)vKKKkk1(AS(l,40)slfx)A(dtkkkk,7)(dtkkkk,2)#Jdtkkkkdtkkkk#Jdtkkkk#Jdtkkkk#Jdtkkkk#JJdtkkkk#JJdtkkkk#(j,47)qdetlldEtiiqdetllqqqqdetlldEtkkdetllqqqdEtkkqqqqTklrrzdetjjjjjjdCEt(k,64)kkkkkkdCet((l,32),3)jjjjjj#[0:64]dt((k,32),2)(dt(l,32),3)(dt(k,32)(j,32)dt(j,32))(dt(k,64)dt(l,32))zdEzZxA(#(J,1)p(l,0),1094)A2(dtllllp(j,5),7)(Adtkk)fxmllli
last of many1f((dtlllpl,7)(dtlllpll,7)(dtlllplll,7),1)A((dtkkkpjjj,7)(dtkkkpjj,7)(dtkkkpj,7),1)AqqdpletjAqqqdplEtkAS(l,10)A2((dt(l,17)p(l,1))(dt(l,17)p(l,3))(dt(l,17)p(l,5)),2)1AqdpllledEdeAqdpkedEdeAqdedEdeA5qqqqdrrre(ASK(l,4)sKKll)(fxzZv1KKKKKkkkk7mlllii)
three of many1f((dtlllpl,7)(dtlllpll,7)(dtlllplll,7),1)A((dtkkkpjjj,7)(dtkkkpjj,7)(dtkkkpj,7),1)AqqdpletjAqqqdplEtkAS(l,10)A2((dt(l,17)p(l,1))(dt(l,17)p(l,3))(dt(l,17)p(l,5)),2)1AqdpllledEdeAqdpkedEdeAqdedEde(ASK(l,4)sKKll)(fxzZv1KKKkkkkkomlllii)
two of many1f((dtlllpl,7)(dtlllpll,7)(dtlllplll,7),1)A((dtkkkpjjj,7)(dtkkkpjj,7)(dtkkkpj,7),1)AqqdpletjAqqqdplEtkAS(l,10)A2((dt(l,17)p(l,1))(dt(l,17)p(l,3))(dt(l,17)p(l,5)),2)1AqdpllledEdeAqdpkedEdeAqdedEde(ASK(l,4)sKKll)(fxzZv1Kkkkkkomlllii)
one of many1f((dtlllpl,7)(dtlllpll,7)(dtlllplll,7),1)A((dtkkkpjjj,7)(dtkkkpjj,7)(dtkkkpj,7),1)AqqdpletjAqqqdplEtkAS(l,10)A2((dt(l,17)p(l,1))(dt(l,17)p(l,3))(dt(l,17)p(l,5)),2)1AqdpllledEdeAqdpkedEdeAqdedEde(ASK(l,4)sKKll)(fxzZv1kkkkkomlllii)
collapsing the tesseract1fsIIIIIII2((dtl,20)plll,10)ASKKKKzZv1Adpl5r1t(i,10)l(i,2)2t(l,2)1AdplllE5r12rt(l,3)(k,14)1t(j,3)2Adplkrrt(l,29)(i,3)j1t(j,10)5AdplllrdplllrdplllrAdplksIdplksIdsIdsIdsIIfxmlvKKK0li
the limits1v1ftl(i,2)lili(l,2)idlt(k,2)(l,2)(k,2)ldkltlklkl(k,2)ldtlklkl(k,2)ldtl(k,2)(l,2)kdt(kl,3)dt(kl,3)dtlkl(k,2)ldtl(k,2)ldlti(l,2)il(k,2)lklkkdtlkl(k,2)lk(l,4)idltlk(l,8)dt(l,7)kldt(l,9)kldt(l,14)kAdtl(i,2)(l,2)ilil(i,2)lililidtl(i,2)(l,2)i(l,2)(i,2)lilililildtlilil(i,2)lililili(l,2)idtl(i,2)lililililililili(l,2)dtililililili(l,2)ililidlitlil(i,2)lilililAqrrrrrrrrdtijijiji(j,2)(k,4)(j,2)(k,2)(j,2)(k,3)jkjkj(k,3)jkjkjijijlklkp(k,3)Adt(ij,5)kjkjjijijijAdtkjkjk(j,13)(k,17)(j,45)(k,20)(l,4)kl(k,2)(l,2)k(l,4)kjpkfxA21fxAdrrrrzrrfxzZ2A(dtlllpl,20)1vKKkkkkml