open-standards/wai/wai-rs/src/generate.rs
Transaction Science fb103ac5ae wai: one registry table, candidate capabilities, public-text checks
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]>
2026-09-30 22:38:09 -04:00

323 lines
15 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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
}