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]>
291 lines
12 KiB
Rust
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 })
|
|
}
|
|
}
|