open-standards/wai/wai-rs/src/semantic.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

291 lines
12 KiB
Rust

//! The perceptual / generative-semantic path — the Rust port of the moved-out
//! `wai-neural` prototype, on-charter this time.
//!
//! Where the bit-exact path ([`crate::neural`]) pins *which model* by hash and
//! reproduces bytes, this path does not reproduce bytes, and says so: it
//! transmits a compact **understanding** of the source and lets the sink
//! **regenerate** something semantically equivalent with whatever conforming
//! generator it has. Conformance is **behavioral** — CLIP cosine similarity
//! against the transmitted understanding ≥ the file's declared threshold
//! ([`ModelRequirement::min_sim`](crate::container::ModelRequirement)) — never
//! byte identity, never a weights hash. Any conforming generator may serve the
//! file; the carrier names *how close is close enough*, not *which model*.
//!
//! The heavy models live at the sink, never in this crate: the generator and
//! the CLIP embedder are [`SemanticGenerator`] / [`SemanticEmbedder`] traits the
//! sink supplies (mirroring how [`crate::neural`] takes decoder ONNX from a
//! registry rather than bundling weights). This module owns the wire format,
//! the conformance metric, and the verify loop — the parts that must be exact.
use std::path::Path;
use crate::container::Wai;
/// RGB image, row-major, 3 bytes/pixel — what a [`SemanticGenerator`] produces
/// and a [`SemanticEmbedder`] consumes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Frame {
pub width: u32,
pub height: u32,
pub rgb: Vec<u8>,
}
#[derive(Debug)]
pub enum SemanticError {
/// The semantic-package payload is malformed / truncated.
Truncated(String),
/// A frame's `rgb` length does not match `width * height * 3`.
InvalidImage(String),
/// The sink generator or embedder failed.
Generator(String),
/// ONNX runtime error from the bundled [`OnnxClip`] embedder.
Ort(String),
}
impl std::fmt::Display for SemanticError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Truncated(e) => write!(f, "truncated semantic package: {e}"),
Self::InvalidImage(e) => write!(f, "invalid image: {e}"),
Self::Generator(e) => write!(f, "generator: {e}"),
Self::Ort(e) => write!(f, "onnxruntime error: {e}"),
}
}
}
impl std::error::Error for SemanticError {}
/// The wire payload of a `wai.semantic.*` envelope: a compact, model-agnostic
/// "understanding" of the source. Byte-faithful to the prototype's
/// `semantic_package_v1` (four length-prefixed parts).
#[derive(Debug, Clone, PartialEq)]
pub struct SemanticPackage {
/// Dequantization scale for the CLIP vector.
pub scale: f32,
/// CLIP image embedding, int8-quantized (projection dim, typically 768).
pub clip_q: Vec<i8>,
/// Tiny JPEG thumbnail — global colour / composition.
pub thumb_jpeg: Vec<u8>,
/// 1-bit Canny edge map, PNG — geometry / layout.
pub edges_png: Vec<u8>,
/// Optional short text tag.
pub tag: String,
}
impl SemanticPackage {
/// Build a package from a (unit-norm) CLIP embedding plus the side maps.
/// Quantizes the embedding to int8 exactly as the prototype does.
pub fn from_embedding(
embedding: &[f32],
thumb_jpeg: Vec<u8>,
edges_png: Vec<u8>,
tag: impl Into<String>,
) -> Self {
let peak = embedding.iter().fold(0f32, |m, &x| m.max(x.abs()));
let scale = if peak == 0.0 { 1.0 } else { peak };
let clip_q = embedding
.iter()
.map(|&x| (x / scale * 127.0).round().clamp(-127.0, 127.0) as i8)
.collect();
Self { scale, clip_q, thumb_jpeg, edges_png, tag: tag.into() }
}
/// Dequantized embedding vector (`q * scale / 127`). Not re-normalized
/// here; [`cosine_sim`] normalizes both operands.
pub fn clip(&self) -> Vec<f32> {
self.clip_q.iter().map(|&q| q as f32 * self.scale / 127.0).collect()
}
/// Serialize to the `semantic_package_v1` wire bytes.
pub fn pack(&self) -> Vec<u8> {
let mut clip_part = Vec::with_capacity(4 + self.clip_q.len());
clip_part.extend_from_slice(&self.scale.to_le_bytes());
clip_part.extend(self.clip_q.iter().map(|&b| b as u8));
let parts: [&[u8]; 4] =
[&clip_part, &self.thumb_jpeg, &self.edges_png, self.tag.as_bytes()];
let mut out = Vec::new();
for p in parts {
out.extend_from_slice(&(p.len() as u32).to_le_bytes());
out.extend_from_slice(p);
}
out
}
/// Parse `semantic_package_v1` bytes. Bounds-checked: malformed input
/// returns `Err`, never panics.
pub fn unpack(blob: &[u8]) -> Result<Self, SemanticError> {
fn take<'a>(blob: &'a [u8], o: &mut usize) -> Result<&'a [u8], SemanticError> {
if *o + 4 > blob.len() {
return Err(SemanticError::Truncated(format!("length prefix at {o}")));
}
let n = u32::from_le_bytes(blob[*o..*o + 4].try_into().unwrap()) as usize;
*o += 4;
if *o + n > blob.len() {
return Err(SemanticError::Truncated(format!("part of {n} bytes at {o}")));
}
let s = &blob[*o..*o + n];
*o += n;
Ok(s)
}
let mut o = 0usize;
let clip_part = take(blob, &mut o)?;
let thumb = take(blob, &mut o)?;
let edges = take(blob, &mut o)?;
let tag = take(blob, &mut o)?;
if clip_part.len() < 4 {
return Err(SemanticError::Truncated("clip part < 4 bytes".into()));
}
let scale = f32::from_le_bytes(clip_part[0..4].try_into().unwrap());
let clip_q = clip_part[4..].iter().map(|&b| b as i8).collect();
Ok(Self {
scale,
clip_q,
thumb_jpeg: thumb.to_vec(),
edges_png: edges.to_vec(),
tag: String::from_utf8_lossy(tag).into_owned(),
})
}
}
/// CLIP cosine similarity — the conformance metric. `1.0` identical, `0.0`
/// orthogonal. Both operands are normalized here, so callers may pass raw
/// (unnormalized) embeddings.
pub fn cosine_sim(a: &[f32], b: &[f32]) -> f32 {
let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
let na = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let nb = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if na == 0.0 || nb == 0.0 { 0.0 } else { dot / (na * nb) }
}
/// A sink-supplied CLIP image embedder. The protocol needs embeddings to score
/// conformance; the model that produces them is the sink's, not this crate's.
pub trait SemanticEmbedder {
/// Unit-norm CLIP image embedding of `frame`.
fn embed(&self, frame: &Frame) -> Result<Vec<f32>, SemanticError>;
}
/// A sink-supplied generative model that regenerates media from a semantic
/// package. Any conforming generator may serve — SD-XL-Turbo, a tiny on-device
/// model, whatever is real-time on *this* sink's hardware.
pub trait SemanticGenerator {
fn regenerate(&self, pkg: &SemanticPackage, w: u32, h: u32) -> Result<Frame, SemanticError>;
}
/// Outcome of a verified regeneration.
#[derive(Debug, Clone)]
pub struct SemanticResult {
pub frame: Frame,
/// Best similarity (cosine in the manifest's `sim_metric` space) achieved
/// against the transmitted understanding.
pub similarity: f32,
/// Whether `similarity` met the file's declared `min_sim` threshold.
pub conformed: bool,
/// The embedding space the score was computed in (the envelope's
/// `sim_metric`, e.g. `"clip"`, `"dreamsim"`). The sink MUST have scored
/// with an embedder matching this space for the result to mean anything.
pub metric: String,
}
/// Regenerate semantically-equivalent media from a `wai.semantic.*` envelope,
/// best-of-`k`, and gate on **behavioral conformance**: the regenerated
/// frame's CLIP embedding must reach the file's declared
/// [`min_sim`](crate::container::ModelRequirement::min_sim) against the
/// transmitted understanding. Byte identity is never required; semantic
/// equivalence is. Returns the best candidate with its score and whether it
/// conformed — the caller decides what to do with a non-conforming result.
pub fn regenerate_verified<G: SemanticGenerator, E: SemanticEmbedder>(
env: &Wai,
generator: &G,
embedder: &E,
w: u32,
h: u32,
k: usize,
) -> Result<SemanticResult, SemanticError> {
let pkg = SemanticPackage::unpack(&env.payload)?;
let target = pkg.clip();
let threshold = env.manifest.model_requirement.min_sim.unwrap_or(0.0);
let mut best: Option<(Frame, f32)> = None;
for _ in 0..k.max(1) {
let frame = generator.regenerate(&pkg, w, h)?;
let sim = cosine_sim(&embedder.embed(&frame)?, &target);
let better = match &best {
None => true,
Some((_, b)) => sim > *b,
};
if better {
best = Some((frame, sim));
}
}
let (frame, sim) = best.expect("k.max(1) >= 1 guarantees a candidate");
Ok(SemanticResult {
frame,
similarity: sim,
conformed: sim >= threshold,
metric: env.sim_metric().to_string(),
})
}
// ---- ORT-backed CLIP embedder (sink reference implementation) -------------
/// CLIP image embedder backed by a sink-installed ONNX model, run through the
/// shared deterministic runtime. This is a *reference* `SemanticEmbedder` — the
/// model file is the sink's; this crate bundles no weights.
pub struct OnnxClip {
session: std::sync::Mutex<ort::session::Session>,
}
impl OnnxClip {
/// Load a CLIP image-encoder ONNX (output = image embedding).
pub fn load(model: &Path) -> Result<Self, SemanticError> {
let s = crate::ort_session::load_deterministic(model).map_err(SemanticError::Ort)?;
Ok(Self { session: std::sync::Mutex::new(s) })
}
}
impl SemanticEmbedder for OnnxClip {
fn embed(&self, frame: &Frame) -> Result<Vec<f32>, SemanticError> {
const SIZE: u32 = 224;
// CLIP ViT-L/14 preprocessing.
const MEAN: [f32; 3] = [0.481_454_66, 0.457_827_5, 0.408_210_73];
const STD: [f32; 3] = [0.268_629_54, 0.261_302_58, 0.275_777_11];
if frame.rgb.len() != (frame.width * frame.height * 3) as usize {
return Err(SemanticError::InvalidImage("rgb len != w*h*3".into()));
}
let img = image::RgbImage::from_raw(frame.width, frame.height, frame.rgb.clone())
.ok_or_else(|| SemanticError::InvalidImage("from_raw failed".into()))?;
let resized =
image::imageops::resize(&img, SIZE, SIZE, image::imageops::FilterType::Triangle);
let mut input = ndarray::Array4::<f32>::zeros((1, 3, SIZE as usize, SIZE as usize));
for y in 0..SIZE {
for x in 0..SIZE {
let px = resized.get_pixel(x, y);
for c in 0..3 {
input[[0, c, y as usize, x as usize]] =
(px[c] as f32 / 255.0 - MEAN[c]) / STD[c];
}
}
}
let mut sess = self.session.lock().unwrap();
let input_name = sess.inputs().first().map(|i| i.name().to_string())
.unwrap_or_else(|| "pixel_values".into());
let output_name = sess.outputs().first().map(|o| o.name().to_string())
.unwrap_or_else(|| "image_embeds".into());
let value = ort::value::Value::from_array(input)
.map_err(|e| SemanticError::Ort(format!("input tensor: {e}")))?;
let outputs = sess.run(ort::inputs![input_name => value])
.map_err(|e| SemanticError::Ort(format!("clip run: {e}")))?;
let out = outputs.get(output_name.as_str())
.ok_or_else(|| SemanticError::Ort(format!("output {output_name} missing")))?;
let view = out.try_extract_array::<f32>()
.map_err(|e| SemanticError::Ort(format!("extract embedding: {e}")))?;
let emb: Vec<f32> = view.view().iter().copied().collect();
let norm = emb.iter().map(|x| x * x).sum::<f32>().sqrt();
Ok(if norm > 0.0 { emb.iter().map(|x| x / norm).collect() } else { emb })
}
}