Every registered string now lives in one table (codecs/registry.rs) with its kind, status, media class, registering SPEC section, edition and registration date. `wai registry export` writes registry/capabilities.json, its JSON Schema, the generated table in REGISTRY.md and vocab v2; --check fails on drift. vocab v1 stays stable. A candidate capability (a registered string whose payload format no revision has pinned) must not be emitted. The C packers, the browser writers and `wai wrap` refuse one, and `codecs::is_dispatchable` is the single dispatch and selection predicate (SPEC §4): candidates, companion-only capabilities, records and derivations are not dispatchable, and an unregistered string is decoded only where the sink advertises it. The codec glossary is linked to the registry through a reviewed table and held to its schema by a library test; `org` becomes `body`, from a closed allowlist. The SPEC gains §5 sections on kinds and candidates, a §8 edition rule, corrected citations and rewritten appendices. The quantum-ops vectors are regenerated with neutral device ids. CI declares the feature-gated test targets with required-features, checks the registry export, builds wai-web for wasm32, and runs two public-text checks (comparative prose, and private name patterns) with a self-test. The specification, extensions, research notes, READMEs, glossary and source comments are rewritten to name no company or product and to make no comparisons. Co-Authored-By: Claude Opus 5.5 <[email protected]>
323 lines
15 KiB
Rust
323 lines
15 KiB
Rust
//! Deterministic, integer, **byte-exact generative synthesis** — the
|
||
//! `intent = create` analog of the integer-exact codecs.
|
||
//!
|
||
//! Every other generative path (`wai.semantic.*`) regenerates with a float model
|
||
//! and conforms by *similarity*: the output legitimately varies by generator and
|
||
//! by hardware, so it can never go in a receipt. This module is instead a
|
||
//! generator whose ENTIRE pipeline is integer/fixed-point, pinned to
|
||
//! `(model, prompt, seed, steps)`, so the generated image is **byte-identical on
|
||
//! every machine**. There is no original to reconstruct — the content is created
|
||
//! — yet its hash is portable, so a publisher can pin "this prompt+seed under
|
||
//! this model produces exactly these bytes" and anyone can verify it client-side.
|
||
//! That is *generation-equivalence*: the create-side of "conformance is a hash".
|
||
//!
|
||
//! The synthesis is fixed-point spectral composition — a sum of integer-LUT
|
||
//! sinusoids whose frequencies, amplitudes and phases are drawn by a pure-integer
|
||
//! PRNG seeded from the prompt and seed (the byte-exact analog of FFT-based image
|
||
//! synthesis). The prompt's *meaning* shapes the image through a pinned integer
|
||
//! **lexicon** ([`SpectralModel::intent`]): recognized words set a per-channel
|
||
//! colour (`ocean`→blue, `fire`→red), a frequency tendency (`calm`→smooth,
|
||
//! `chaos`→busy) and an orientation (`horizon`→horizontal, `tall`→vertical),
|
||
//! while the prompt+seed still fix the exact draw — so the conditioning is
|
||
//! semantic yet fully deterministic. The model ([`SpectralModel`], from
|
||
//! `tools/wai_generate_model.py`) carries the integer sine LUT, the lexicon and
|
||
//! the draw configuration; the float `sin()` that bakes the LUT runs once at
|
||
//! model-build time, never here. Pure i64, no `ort`, no float — like
|
||
//! `int_synth`/`int_hyper_synth`, it compiles to wasm32 and is bit-checkable with
|
||
//! no ML runtime.
|
||
|
||
use std::collections::HashMap;
|
||
|
||
use serde_json::Value;
|
||
|
||
/// FNV-1a 64-bit — the dependency-free checksum shared across the integer paths,
|
||
/// also used here to fold the prompt into the PRNG seed.
|
||
pub fn fnv1a(bytes: &[u8]) -> u64 {
|
||
let mut hash: u64 = 14695981039346656037;
|
||
for &b in bytes {
|
||
hash = (hash ^ b as u64).wrapping_mul(1099511628211);
|
||
}
|
||
hash
|
||
}
|
||
|
||
/// `splitmix64` — a pure-integer PRNG. Deterministic on every machine (no float,
|
||
/// no platform RNG); the fixed-point sampler the generative-replicate contract
|
||
/// requires.
|
||
fn splitmix64(s: &mut u64) -> u64 {
|
||
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||
let mut z = *s;
|
||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||
z ^ (z >> 31)
|
||
}
|
||
|
||
/// Integer `atan2(dy, dx)` as a LUT index in `[0, t)` (`t` = a full turn), via
|
||
/// **CORDIC** (vectoring) — shifts and adds only, no float, so the angle is
|
||
/// deterministic on every machine. The accuracy (≈±1 step) doesn't matter; the
|
||
/// *determinism* does. The step table is `round(atan(2^-i)/2π · 1024)`; `t` is
|
||
/// assumed to be the LUT size the table was tabulated for (1024). This is the
|
||
/// integer angle the angular / spiral component types need.
|
||
fn iatan2(dy: i64, dx: i64, t: i64) -> i64 {
|
||
if dx == 0 {
|
||
return if dy == 0 { 0 } else if dy > 0 { t / 4 } else { 3 * t / 4 };
|
||
}
|
||
if dy == 0 {
|
||
return if dx > 0 { 0 } else { t / 2 };
|
||
}
|
||
const ATAN: [i64; 14] = [128, 76, 40, 20, 10, 5, 3, 1, 1, 0, 0, 0, 0, 0];
|
||
// first-quadrant magnitudes, scaled up for precision
|
||
let (mut x, mut y, mut ang) = (dx.abs() << 12, dy.abs() << 12, 0i64);
|
||
for (i, &a) in ATAN.iter().enumerate() {
|
||
if y > 0 {
|
||
let (nx, ny) = (x + (y >> i), y - (x >> i));
|
||
x = nx;
|
||
y = ny;
|
||
ang += a;
|
||
} else if y < 0 {
|
||
let (nx, ny) = (x - (y >> i), y + (x >> i));
|
||
x = nx;
|
||
y = ny;
|
||
ang -= a;
|
||
}
|
||
}
|
||
// ang ≈ atan(|dy|/|dx|) ∈ [0, t/4]; place it in the right quadrant.
|
||
let q = match (dx >= 0, dy >= 0) {
|
||
(true, true) => ang,
|
||
(false, true) => t / 2 - ang,
|
||
(false, false) => t / 2 + ang,
|
||
(true, false) => t - ang,
|
||
};
|
||
((q % t) + t) % t
|
||
}
|
||
|
||
/// The pinned generator model: an integer sine LUT plus the spectral-draw config.
|
||
/// Pure data, no float; the same bundle the export tool commits.
|
||
pub struct SpectralModel {
|
||
pub version: String,
|
||
pub lut: Vec<i64>,
|
||
pub lut_size: i64,
|
||
pub lut_bits: u32,
|
||
pub freq_max: i64,
|
||
pub amp_bits: u32,
|
||
pub color_offsets: [i64; 3],
|
||
pub base_components: usize,
|
||
pub step_components: usize,
|
||
/// Concept word → `[r, g, b, freq_q8, orient, radial, mirror, gradient,
|
||
/// angular, spiral]` — the semantic lexicon that makes the prompt's *meaning*
|
||
/// shape the image, deterministically (colour + texture + orientation + shape
|
||
/// grammar). Shorter entries are zero-padded; empty for a v1 model.
|
||
pub lexicon: HashMap<String, [i64; 10]>,
|
||
}
|
||
|
||
/// The spectral *intent* a prompt folds into — colour, texture, orientation and
|
||
/// the shape-grammar weights. All integer; a neutral (unmatched) prompt is all
|
||
/// zero shape with `hue=[256;3], freq=256`.
|
||
struct Intent {
|
||
hue: [i64; 3], // per-channel weight, max channel = 256 (the tint)
|
||
freq: i64, // frequency multiplier, Q8
|
||
orient: i64, // -256 horizontal … +256 vertical
|
||
radial: i64, // 0..256 weight toward concentric-ring components
|
||
mirror: i64, // 0..256 weight toward bilaterally-folded components
|
||
gradient: i64, // -256..256 vertical brightness ramp (+ = bright top)
|
||
angular: i64, // 0..256 weight toward angular-ray components (star/sunburst)
|
||
spiral: i64, // 0..256 weight toward spiral components (swirl/vortex)
|
||
}
|
||
|
||
impl SpectralModel {
|
||
/// Parse the committed model bundle (`generate_spectral_model.json`).
|
||
pub fn from_value(v: &Value) -> Self {
|
||
let co = v["color_offsets"].as_array().unwrap();
|
||
let lexicon = v.get("lexicon").and_then(|l| l.as_object()).map(|o| {
|
||
o.iter().filter_map(|(k, val)| {
|
||
let a = val.as_array()?;
|
||
let mut e = [0i64; 10];
|
||
for (i, slot) in e.iter_mut().enumerate() {
|
||
*slot = a.get(i).and_then(|n| n.as_i64()).unwrap_or(0);
|
||
}
|
||
Some((k.clone(), e))
|
||
}).collect()
|
||
}).unwrap_or_default();
|
||
Self {
|
||
version: v["version"].as_str().unwrap_or("").to_string(),
|
||
lut: v["sine_lut"].as_array().unwrap().iter().map(|n| n.as_i64().unwrap()).collect(),
|
||
lut_size: v["lut_size"].as_i64().unwrap(),
|
||
lut_bits: v["lut_bits"].as_u64().unwrap_or(14) as u32,
|
||
freq_max: v["freq_max"].as_i64().unwrap(),
|
||
amp_bits: v["amp_bits"].as_u64().unwrap() as u32,
|
||
color_offsets: [co[0].as_i64().unwrap(), co[1].as_i64().unwrap(), co[2].as_i64().unwrap()],
|
||
base_components: v["base_components"].as_u64().unwrap() as usize,
|
||
step_components: v["step_components"].as_u64().unwrap() as usize,
|
||
lexicon,
|
||
}
|
||
}
|
||
|
||
/// Fold the prompt's recognized words (averaged) into an [`Intent`]. Unmatched
|
||
/// prompts return the neutral intent, so an unknown prompt still generates.
|
||
fn intent(&self, prompt: &str) -> Intent {
|
||
let (mut h, mut f, mut o, mut r, mut m, mut gr, mut an, mut sp, mut n) =
|
||
([0i64; 3], 0, 0, 0, 0, 0, 0, 0, 0i64);
|
||
let lower = prompt.to_lowercase();
|
||
for tok in lower.split(|c: char| !c.is_ascii_alphanumeric()).filter(|t| !t.is_empty()) {
|
||
if let Some(e) = self.lexicon.get(tok) {
|
||
h[0] += e[0];
|
||
h[1] += e[1];
|
||
h[2] += e[2];
|
||
f += e[3];
|
||
o += e[4];
|
||
r += e[5];
|
||
m += e[6];
|
||
gr += e[7];
|
||
an += e[8];
|
||
sp += e[9];
|
||
n += 1;
|
||
}
|
||
}
|
||
if n == 0 {
|
||
return Intent {
|
||
hue: [256, 256, 256], freq: 256, orient: 0,
|
||
radial: 0, mirror: 0, gradient: 0, angular: 0, spiral: 0,
|
||
};
|
||
}
|
||
let mx = *h.iter().max().unwrap().max(&1);
|
||
Intent {
|
||
hue: [h[0] * 256 / mx, h[1] * 256 / mx, h[2] * 256 / mx],
|
||
freq: f / n,
|
||
orient: (o / n).clamp(-256, 256),
|
||
radial: (r / n).clamp(0, 256),
|
||
mirror: (m / n).clamp(0, 256),
|
||
gradient: (gr / n).clamp(-256, 256),
|
||
angular: (an / n).clamp(0, 256),
|
||
spiral: (sp / n).clamp(0, 256),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Generate an `h`×`w` RGB image (row-major **HWC** `u8`) deterministically from
|
||
/// `(model, prompt, seed, steps)`. Pure i64 — identical bytes on every machine.
|
||
///
|
||
/// `steps` scales the spectral richness: `K = base + steps·step` components are
|
||
/// drawn by [`splitmix64`] seeded from `fnv1a(prompt) ^ seed`. The prompt's
|
||
/// recognized words ([`SpectralModel::intent`]) shape the draw: a per-channel
|
||
/// **hue** tint, a **frequency** multiplier (calm↔busy), an **orientation** skew
|
||
/// (horizontal↔vertical), and a **shape grammar** — each component is a *plane*
|
||
/// wave, a *radial* ring (`sin(fr·r + φ)`, integer distance via `isqrt`), a
|
||
/// *mirror* fold (`sin(fx·|x−cx| + fy·|y−cy| + φ)`), an *angular* ray
|
||
/// (`sin(fa·θ + φ)`, integer angle θ via the CORDIC [`iatan2`]) or a *spiral*
|
||
/// (`sin(fa·θ + fr·r + φ)`), drawn from the `radial`/`mirror`/`angular`/`spiral`
|
||
/// weights — plus a vertical brightness `gradient`. The prompt+seed still fix the
|
||
/// specific draw. The field is normalized to `[0,255]` by its integer min/max,
|
||
/// then hue-tinted. Same inputs → exact same bytes; different prompts → different,
|
||
/// *thematically shaped & structured*, images.
|
||
pub fn generate(m: &SpectralModel, prompt: &str, seed: u64, steps: usize, h: usize, w: usize) -> Vec<u8> {
|
||
let k = m.base_components + steps * m.step_components;
|
||
let it = m.intent(prompt);
|
||
// orientation skews how wide the horizontal vs vertical frequency draw is:
|
||
// negative → suppress fx (horizontal bands), positive → suppress fy (vertical).
|
||
let (fx_scale, fy_scale) = if it.orient < 0 {
|
||
((256 + it.orient).max(0), 256)
|
||
} else {
|
||
(256, (256 - it.orient).max(0))
|
||
};
|
||
let fmax = (m.freq_max * it.freq / 256).max(1);
|
||
// deterministic seed: prompt folded into the explicit seed (a fixed mix const
|
||
// keeps an empty prompt or zero seed from degenerating the stream).
|
||
let mut rng = fnv1a(prompt.as_bytes()) ^ seed ^ 0xD1B5_4A32_D192_ED03;
|
||
let amp_max = 1i64 << m.amp_bits;
|
||
let span = 2 * fmax as u64 + 1;
|
||
// component-type mix: a base of plane waves plus the shape-grammar weights;
|
||
// each component draws its type from this distribution. (typ 0 plane, 1 radial,
|
||
// 2 mirror, 3 angular, 4 spiral; stored as `(typ, fx, fy, amp, phase)`.) The
|
||
// draw order — pick, fxr, fyr, amp, phase — is the SAME for every type (the
|
||
// angular/spiral frequencies reuse fxr/fyr), so a prompt with no shape weights
|
||
// produces the exact same stream as before (the pin is unchanged).
|
||
let thr = [it.radial, it.mirror, it.angular, it.spiral];
|
||
let plane_w = (256 - thr.iter().sum::<i64>()).max(48);
|
||
let total = (plane_w + thr.iter().sum::<i64>()) as u64;
|
||
let comps: Vec<(u8, i64, i64, i64, i64)> = (0..k)
|
||
.map(|_| {
|
||
let pick = (splitmix64(&mut rng) % total) as i64; // u64 modulo (as before), in [0, total)
|
||
let mut typ = 0u8;
|
||
if pick >= plane_w {
|
||
let mut bound = plane_w;
|
||
for (i, &wgt) in thr.iter().enumerate() {
|
||
bound += wgt;
|
||
if pick < bound {
|
||
typ = i as u8 + 1;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
let fxr = (splitmix64(&mut rng) % span) as i64 - fmax;
|
||
let fyr = (splitmix64(&mut rng) % span) as i64 - fmax;
|
||
let amp = (splitmix64(&mut rng) % amp_max as u64) as i64 + 1;
|
||
let phase = (splitmix64(&mut rng) % m.lut_size as u64) as i64;
|
||
match typ {
|
||
1 => (1, fxr.abs().max(2), 0, amp, phase), // radial: fr = |fxr|
|
||
3 => (3, fxr.abs() % 11 + 2, 0, amp, phase), // angular: fa rays
|
||
4 => (4, fxr.abs() % 9 + 2, fyr.abs().max(2), amp, phase), // spiral: fa angular, fr radial
|
||
_ => (typ, fxr * fx_scale / 256, fyr * fy_scale / 256, amp, phase), // plane/mirror
|
||
}
|
||
})
|
||
.collect();
|
||
|
||
let n = h * w;
|
||
let t = m.lut_size;
|
||
let (cx, cy) = (w as i64 / 2, h as i64 / 2);
|
||
// radius (radial/spiral) and angle (angular/spiral) depend only on the pixel —
|
||
// compute each at most once per pixel, and only if some component needs it.
|
||
let need_r = comps.iter().any(|&(typ, ..)| typ == 1 || typ == 4);
|
||
let need_th = comps.iter().any(|&(typ, ..)| typ == 3 || typ == 4);
|
||
// vertical brightness ramp, scaled to sit alongside the sinusoid sum (not swamp it)
|
||
let gscale = ((1i64 << m.lut_bits) * amp_max * k as i64) / (2048 * cy.max(1));
|
||
let mut acc = vec![0i64; 3 * n];
|
||
for (c, &coff) in m.color_offsets.iter().enumerate() {
|
||
let plane = c * n;
|
||
for y in 0..h {
|
||
let yi = y as i64;
|
||
let grad = it.gradient * (cy - yi) * gscale; // same per channel — a brightness gradient
|
||
for x in 0..w {
|
||
let xi = x as i64;
|
||
let (dx, dy) = (xi - cx, yi - cy);
|
||
let r = if need_r { ((dx * dx + dy * dy) as u64).isqrt() as i64 } else { 0 };
|
||
let th = if need_th { iatan2(dy, dx, t) } else { 0 };
|
||
let mut a = grad;
|
||
for &(typ, fx, fy, amp, phase) in &comps {
|
||
let arg = match typ {
|
||
1 => fx * r + phase + coff, // radial: fr·r
|
||
2 => fx * dx.abs() + fy * dy.abs() + phase + coff, // mirror: fold
|
||
3 => fx * th + phase + coff, // angular: fa·θ (rays)
|
||
4 => fx * th + fy * r + phase + coff, // spiral: fa·θ + fr·r
|
||
_ => fx * xi + fy * yi + phase + coff, // plane
|
||
};
|
||
let idx = ((arg % t) + t) % t;
|
||
a += amp * m.lut[idx as usize];
|
||
}
|
||
acc[plane + y * w + x] = a;
|
||
}
|
||
}
|
||
}
|
||
|
||
// integer min/max normalization → [0,255], round-half-up, then a per-channel
|
||
// hue TINT: the prompt's dominant colour keeps full range, the others are
|
||
// scaled toward a floor (so the colour reads without going muddy-black). A
|
||
// neutral prompt has hue `[256;3]` → no tint (the colour-offset rainbow). Pure
|
||
// integer → byte-exact.
|
||
const FLOOR: i64 = 64; // non-dominant channels never drop below ~floor/256
|
||
let eh = [
|
||
FLOOR + it.hue[0] * (256 - FLOOR) / 256,
|
||
FLOOR + it.hue[1] * (256 - FLOOR) / 256,
|
||
FLOOR + it.hue[2] * (256 - FLOOR) / 256,
|
||
];
|
||
let gmin = *acc.iter().min().unwrap();
|
||
let gmax = *acc.iter().max().unwrap();
|
||
let range = (gmax - gmin).max(1);
|
||
let mut rgb = vec![0u8; n * 3];
|
||
for c in 0..3 {
|
||
for p in 0..n {
|
||
let v = acc[c * n + p];
|
||
let q = ((v - gmin) * 255 + range / 2) / range;
|
||
rgb[p * 3 + c] = (q * eh[c] / 256).clamp(0, 255) as u8;
|
||
}
|
||
}
|
||
rgb
|
||
}
|