Some checks failed
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 17s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Failing after 54s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Failing after 2m33s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 3m8s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (push) Has been cancelled
wai-audit collector and player integration / does this change touch it (push) Has been cancelled
wai-audit collector and player integration / collector · linux (push) Has been cancelled
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Has been cancelled
WAI operation catalogue / does this change touch it (push) Has been cancelled
WAI operation catalogue / the catalogue and every door · linux (push) Has been cancelled
wai-quantum for JavaScript / does this change touch it (push) Has been cancelled
wai-quantum for JavaScript / javascript · linux (push) Has been cancelled
wai-quantum studio and desktop / does this change touch it (push) Has been cancelled
wai-quantum studio and desktop / studio · browser (push) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (push) Has been cancelled
wai-quantum studio and desktop / desktop app · linux (push) Has been cancelled
wai-quantum studio and desktop / desktop app · windows (push) Has been cancelled
wai-sink-android native sink library / does this change touch it (push) Has been cancelled
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Has been cancelled
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Has been cancelled
wai-sink-swift native sink package / does this change touch it (push) Has been cancelled
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Has been cancelled
WAI Studio / does this change touch it (push) Has been cancelled
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Has been cancelled
WAI Studio / Studio in Chromium and Firefox · macos (push) Has been cancelled
WAI Studio / Studio in WebKit · linux (push) Has been cancelled
wai-ledger service / does this change touch it (push) Has been cancelled
wai-ledger service / service · linux (push) Has been cancelled
wai-ledger service / service · macos (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Has been cancelled
wai-app and Create / wai-app · does this change touch it (push) Has been cancelled
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Has been cancelled
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Has been cancelled
wai-fleet service / does this change touch it (push) Has been cancelled
wai-fleet service / service · linux (push) Has been cancelled
wai-fleet service / service · macos (push) Has been cancelled
The reference synthesiser gains what a short channel sting needs, all in the wai.det.fixed64 floor and trig-free, every committed score rendering byte for byte as before (the three first-revision vectors keep their mixdown hash, PCM SHA-256 and score hash): - score §2.2, §3.1: waveforms 4 to 6, the saw, square and 25 %-pulse with each step corrected by a polynomial (PolyBLEP); 7, a sine by a refined parabola; 8, noise held from a 32-bit LCG between wraps of the phase. - score §2.1, §3.4: a contract of two channels mixes down to interleaved frames; a reader refuses any count but 1 and 2; a score with no voices centres each note. - score §2.4, §3.3: an optional third section, one 73-byte voice per note: a constant-power pan by the profile's integer square root, curved attack, decay and release (the release from the level at note-off), up to four one-pole low-pass sections that follow the envelope, a linear glide, the noise seed, and up to eight echoes at alternating pans. The 65-byte note record is unchanged; the score hash binds the voices when present. - wai score build|render: channels 2, waveforms to 8, each note's optional voice; render prints frames and channels. Receipt blocks are 4096 frames. - score-conformance: five vectors at 48 kHz (band-limited, two channels, voiced in one, a two-second sting, a voiced scale); multi_verify.py's independent synth agrees with all eight. - packaging §5.8: a score of one or two channels packs beside the lead with its voices, its floor the FLAC of the mixdown in its channels; verify holds the floor to the base's channels and rate. pack-conformance/multi/multi-sting-01 is a voiced two-channel score at 48 kHz. - The composing sink synthesises and plays a score in its contract's channels; its reference package's score is now two channels at 48 kHz, voiced. Co-Authored-By: Claude Opus 5.5 <[email protected]>
568 lines
30 KiB
Rust
568 lines
30 KiB
Rust
// wai-conformance: int-kernels
|
||
//! `wai generate` and `wai score build|render`, run as a user runs them: the native
|
||
//! pins a page checks its own output against. Each output is checked against a
|
||
//! committed vector — `tests/vectors/generate/` for pictures, `score-conformance/`
|
||
//! for scores — and every refusal is checked to write nothing.
|
||
//!
|
||
//! Cargo.toml declares this target with required-features = ["generate", "score"], so a
|
||
//! run without them stops with an error instead of reporting an empty binary as passed.
|
||
//! The two larger listed sizes of `instr-v0.json` take seconds in a debug build and run
|
||
//! in the release step.
|
||
|
||
use std::path::{Path, PathBuf};
|
||
use std::process::{Command, Output};
|
||
|
||
use serde_json::Value;
|
||
use wai::container::Wai;
|
||
|
||
fn root() -> PathBuf {
|
||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||
}
|
||
fn gen_dir() -> PathBuf {
|
||
root().join("tests/vectors/generate")
|
||
}
|
||
fn score_dir() -> PathBuf {
|
||
root().join("../score-conformance")
|
||
}
|
||
fn model() -> PathBuf {
|
||
gen_dir().join("generate_spectral_model.json")
|
||
}
|
||
fn jload(p: &Path) -> Value {
|
||
serde_json::from_slice(&std::fs::read(p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()))).unwrap()
|
||
}
|
||
fn sha256_hex(b: &[u8]) -> String {
|
||
wai::pin::sha256(b).iter().map(|x| format!("{x:02x}")).collect()
|
||
}
|
||
|
||
fn scratch(name: &str) -> PathBuf {
|
||
let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("cli_generate_score");
|
||
std::fs::create_dir_all(&dir).unwrap();
|
||
let p = dir.join(name);
|
||
let _ = std::fs::remove_file(&p);
|
||
p
|
||
}
|
||
|
||
fn wai(args: &[&str]) -> Output {
|
||
Command::new(env!("CARGO_BIN_EXE_wai")).args(args).output().expect("run wai")
|
||
}
|
||
|
||
fn ok(run: &Output) -> String {
|
||
assert!(run.status.success(), "wai failed: {}", String::from_utf8_lossy(&run.stderr));
|
||
String::from_utf8(run.stdout.clone()).unwrap()
|
||
}
|
||
|
||
fn refused(run: &Output, out: &Path, what: &str) -> String {
|
||
let stderr = String::from_utf8_lossy(&run.stderr).into_owned();
|
||
assert!(!run.status.success(), "{what}: must be refused; stdout: {}", String::from_utf8_lossy(&run.stdout));
|
||
assert!(!out.exists(), "{what}: a refusal writes nothing");
|
||
stderr
|
||
}
|
||
|
||
/// The `data` of the JSON envelope (envelope/1) that `wai ... --json` prints.
|
||
fn data(stdout: &str) -> Value {
|
||
let env: Value = serde_json::from_str(stdout.trim()).expect("one JSON envelope");
|
||
assert_eq!((env["wai"].as_str(), env["ok"].as_bool()), (Some("envelope/1"), Some(true)), "{env}");
|
||
env["data"].clone()
|
||
}
|
||
|
||
/// The `--json` data `wai generate` prints, checked against the file it wrote.
|
||
fn made(stdout: &str, out: &Path, w: u64, h: u64) -> String {
|
||
let v: Value = data(stdout);
|
||
let rgb = std::fs::read(out).unwrap();
|
||
assert_eq!((v["w"].as_u64(), v["h"].as_u64()), (Some(w), Some(h)));
|
||
assert_eq!(v["bytes"].as_u64(), Some(w * h * 3));
|
||
assert_eq!(rgb.len() as u64, w * h * 3, "RGB8, w·h·3 bytes");
|
||
let sha = v["sha256"].as_str().unwrap().to_string();
|
||
assert_eq!(sha256_hex(&rgb), sha, "the printed SHA-256 is the file's");
|
||
sha
|
||
}
|
||
|
||
fn m() -> String {
|
||
model().to_string_lossy().into_owned()
|
||
}
|
||
fn p(path: &Path) -> &str {
|
||
path.to_str().unwrap()
|
||
}
|
||
|
||
#[test]
|
||
fn generate_reproduces_the_committed_vectors() {
|
||
// 96×96: the committed vector and its SHA-256 sidecar.
|
||
let pin = jload(&gen_dir().join("generate.json"));
|
||
let side = jload(&gen_dir().join("sha256.json"));
|
||
let out = scratch("p96.rgb");
|
||
let seed = pin["seed"].to_string();
|
||
let steps = pin["steps"].to_string();
|
||
let run = wai(&["generate", "--model", &m(), "--prompt", pin["prompt"].as_str().unwrap(), "--seed", &seed,
|
||
"--steps", &steps, "--size", "96x96", "-o", p(&out), "--json"]);
|
||
assert_eq!(made(&ok(&run), &out, 96, 96), side["generate"]["sha256"].as_str().unwrap());
|
||
// sizes.json: the same prompt, seed and steps at its non-square sizes.
|
||
let sizes = jload(&gen_dir().join("sizes.json"));
|
||
for s in sizes["sizes"].as_array().unwrap() {
|
||
let (w, h) = (s["w"].as_u64().unwrap(), s["h"].as_u64().unwrap());
|
||
let out = scratch(&format!("p{w}x{h}.rgb"));
|
||
let run = wai(&["generate", "--model", &m(), "--prompt", sizes["prompt"].as_str().unwrap(), "--seed", &seed,
|
||
"--steps", &steps, "--size", &format!("{w}x{h}"), "-o", p(&out), "--json"]);
|
||
assert_eq!(made(&ok(&run), &out, w, h), s["sha256"].as_str().unwrap(), "{w}x{h}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn generate_refuses_flags_outside_form_v0() {
|
||
let base = ["generate", "--model", &m(), "--prompt", "calm ocean", "--seed", "3", "--steps", "4"];
|
||
for (extra, what) in [
|
||
(&["--size", "0x128"][..], "a side of 0"),
|
||
(&["--size", "4097x1"][..], "a side past 4096"),
|
||
(&["--size", "4096x4096"][..], "more than 2^23 pixels"),
|
||
(&["--size", "256"][..], "not WxH"),
|
||
] {
|
||
let out = scratch("flags.rgb");
|
||
let mut args: Vec<&str> = base.to_vec();
|
||
args.extend_from_slice(extra);
|
||
args.extend_from_slice(&["-o", p(&out)]);
|
||
refused(&wai(&args), &out, what);
|
||
}
|
||
for (flag, value, what) in [
|
||
("--seed", "4294967296", "a seed past 2^32 - 1"),
|
||
("--steps", "0", "steps below 1"),
|
||
("--steps", "65", "steps past 64"),
|
||
("--prompt", "a \"quoted\" prompt", "a quotation mark in the prompt"),
|
||
] {
|
||
let out = scratch("flags.rgb");
|
||
let mut args: Vec<String> = base.iter().map(|s| s.to_string()).collect();
|
||
let i = args.iter().position(|a| a == flag).unwrap();
|
||
args[i + 1] = value.into();
|
||
args.extend(["--size", "64x32", "-o", p(&out)].map(String::from));
|
||
let args: Vec<&str> = args.iter().map(String::as_str).collect();
|
||
refused(&wai(&args), &out, what);
|
||
}
|
||
}
|
||
|
||
/// The vector's object, written as `--write-instr` writes it from the same flags.
|
||
#[test]
|
||
fn write_instr_writes_the_vectors_object() {
|
||
let v = jload(&gen_dir().join("instr-v0.json"));
|
||
let object = v["object"].as_str().unwrap();
|
||
let o: Value = serde_json::from_str(object).unwrap();
|
||
let sizes: Vec<String> = o["sizes"].as_array().unwrap().iter().map(|s| format!("{}x{}", s[0], s[1])).collect();
|
||
let out = scratch("written.instr");
|
||
ok(&wai(&["generate", "--model", &m(), "--prompt", o["prompt"].as_str().unwrap(), "--seed", &o["seed"].to_string(),
|
||
"--steps", &o["steps"].to_string(), "--sizes", &sizes.join(","), "--write-instr", p(&out)]));
|
||
assert_eq!(std::fs::read(&out).unwrap(), object.as_bytes());
|
||
}
|
||
|
||
fn instr_sizes(min_pixels: u64, max_pixels: u64) -> usize {
|
||
let v = jload(&gen_dir().join("instr-v0.json"));
|
||
// A file of its own per caller: the two tests that call this run at once, and each
|
||
// removes and rewrites its object before its `wai` children read it.
|
||
let object = scratch(&format!("object-{min_pixels}.instr"));
|
||
std::fs::write(&object, v["object"].as_str().unwrap()).unwrap();
|
||
let mut n = 0;
|
||
for e in v["outputs"].as_array().unwrap() {
|
||
let (w, h) = (e["w"].as_u64().unwrap(), e["h"].as_u64().unwrap());
|
||
if !(min_pixels..=max_pixels).contains(&(w * h)) {
|
||
continue;
|
||
}
|
||
let out = scratch(&format!("instr{w}x{h}.rgb"));
|
||
let run = wai(&["generate", "--model", &m(), "--instr", p(&object), "--size", &format!("{w}x{h}"),
|
||
"-o", p(&out), "--json"]);
|
||
assert_eq!(made(&ok(&run), &out, w, h), e["sha256"].as_str().unwrap(), "{w}x{h}");
|
||
n += 1;
|
||
}
|
||
n
|
||
}
|
||
|
||
#[test]
|
||
fn instr_makes_the_listed_small_sizes() {
|
||
assert_eq!(instr_sizes(0, 640 * 320), 2);
|
||
}
|
||
|
||
#[test]
|
||
#[cfg_attr(debug_assertions, ignore = "1280x720 and 1920x1080 take seconds in a debug build; the release step runs them")]
|
||
fn instr_makes_the_listed_large_sizes() {
|
||
assert_eq!(instr_sizes(640 * 320 + 1, u64::MAX), 2);
|
||
}
|
||
|
||
/// Every object the vector lists as refused is refused by `wai generate --instr` and
|
||
/// by `wai wrap`, each for the reason the vector states (the first rule the reader
|
||
/// finds broken), and neither writes anything. Holding the reason, not only the
|
||
/// refusal, notices a rule that stops refusing while another rule still refuses the
|
||
/// same object: a quotation mark in the prompt, say, which the canonical-bytes check
|
||
/// would refuse if the prompt rule let it through. The vector's own object is accepted
|
||
/// by both, and the envelope carries it byte for byte with intent `create`.
|
||
#[test]
|
||
fn the_vectors_refused_objects_are_refused() {
|
||
let v = jload(&gen_dir().join("instr-v0.json"));
|
||
let cases = v["refused"].as_array().unwrap();
|
||
assert!(cases.len() >= 30, "the vector lists its refusals");
|
||
let object = scratch("refused.instr");
|
||
for c in cases {
|
||
let why = c["why"].as_str().unwrap();
|
||
let reason = c["reason"].as_str().unwrap_or_else(|| panic!("{why}: the vector states no reason"));
|
||
std::fs::write(&object, c["object"].as_str().unwrap()).unwrap();
|
||
let out = scratch("refused.rgb");
|
||
let stderr = refused(&wai(&["generate", "--model", &m(), "--instr", p(&object), "--size", "256x128",
|
||
"-o", p(&out)]), &out, why);
|
||
assert!(stderr.contains(&format!("refused: {reason}: ")), "{why}: refused, but not as {reason}: {stderr}");
|
||
let env = scratch("refused.wai");
|
||
let stderr = refused(&wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", "create", "-o", p(&env)]),
|
||
&env, why);
|
||
assert!(stderr.contains("instruction object of form v0") && stderr.contains(&format!(": {reason}: ")),
|
||
"{why}: refused, but not as {reason}: {stderr}");
|
||
}
|
||
std::fs::write(&object, v["object"].as_str().unwrap()).unwrap();
|
||
let env = scratch("accepted.wai");
|
||
ok(&wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", "create", "-o", p(&env)]));
|
||
let e = Wai::read(&env).unwrap();
|
||
assert_eq!((e.capability(), e.intent()), ("wai.generate.spectral", "create"));
|
||
assert_eq!(e.payload, v["object"].as_str().unwrap().as_bytes());
|
||
}
|
||
|
||
/// Every object the vector lists as accepted — one at each inclusive bound of form v0 —
|
||
/// is accepted by `wai generate --instr`, which makes its first listed size with the
|
||
/// SHA-256 the vector states, and by `wai wrap`, whose envelope carries it byte for
|
||
/// byte. A reader stricter than form v0 by one refuses one of them.
|
||
#[test]
|
||
fn the_vectors_accepted_objects_are_accepted() {
|
||
let v = jload(&gen_dir().join("instr-v0.json"));
|
||
let cases = v["accepted"].as_array().unwrap();
|
||
assert!(cases.len() >= 12, "the vector lists an object at each bound");
|
||
let object = scratch("accepted.instr");
|
||
for c in cases {
|
||
let why = c["why"].as_str().unwrap();
|
||
std::fs::write(&object, c["object"].as_str().unwrap()).unwrap();
|
||
for e in c["outputs"].as_array().unwrap() {
|
||
let (w, h) = (e["w"].as_u64().unwrap(), e["h"].as_u64().unwrap());
|
||
let out = scratch("accepted.rgb");
|
||
let run = wai(&["generate", "--model", &m(), "--instr", p(&object), "--size", &format!("{w}x{h}"),
|
||
"-o", p(&out), "--json"]);
|
||
assert!(run.status.success(), "{why}: refused: {}", String::from_utf8_lossy(&run.stderr));
|
||
assert_eq!(made(&ok(&run), &out, w, h), e["sha256"].as_str().unwrap(), "{why}: {w}x{h}");
|
||
}
|
||
let env = scratch("accepted-bound.wai");
|
||
let run = wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", "create", "-o", p(&env)]);
|
||
assert!(run.status.success(), "{why}: wrap refused it: {}", String::from_utf8_lossy(&run.stderr));
|
||
assert_eq!(Wai::read(&env).unwrap().payload, c["object"].as_str().unwrap().as_bytes(), "{why}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn instr_refuses_another_model_and_an_unlisted_size() {
|
||
let v = jload(&gen_dir().join("instr-v0.json"));
|
||
let object = scratch("model.instr");
|
||
std::fs::write(&object, v["object"].as_str().unwrap()).unwrap();
|
||
// The same model with one more byte (a line feed): the same parameters, another digest.
|
||
let other = scratch("other-model.json");
|
||
let mut bytes = std::fs::read(model()).unwrap();
|
||
bytes.push(b'\n');
|
||
std::fs::write(&other, bytes).unwrap();
|
||
let out = scratch("model.rgb");
|
||
let stderr = refused(&wai(&["generate", "--model", p(&other), "--instr", p(&object), "--size", "256x128",
|
||
"-o", p(&out)]), &out, "another model file");
|
||
assert!(stderr.contains("the object's model is sha256:"), "{stderr}");
|
||
let stderr = refused(&wai(&["generate", "--model", &m(), "--instr", p(&object), "--size", "96x96", "-o", p(&out)]),
|
||
&out, "an unlisted size");
|
||
assert!(stderr.contains("not a listed size"), "{stderr}");
|
||
}
|
||
|
||
#[test]
|
||
fn wrap_refuses_replicate_over_the_generator() {
|
||
let v = jload(&gen_dir().join("instr-v0.json"));
|
||
let object = scratch("replicate.instr");
|
||
std::fs::write(&object, v["object"].as_str().unwrap()).unwrap();
|
||
for extra in [&[][..], &["--intent", "replicate"][..]] {
|
||
let env = scratch("replicate.wai");
|
||
let mut args = vec!["wrap", "wai.generate.spectral", p(&object), "-o", p(&env)];
|
||
args.extend_from_slice(extra);
|
||
let stderr = refused(&wai(&args), &env, "replicate over a generator");
|
||
assert!(stderr.contains("GenerativeDeterministic"), "{stderr}");
|
||
}
|
||
}
|
||
|
||
/// SPEC §5: an envelope that names the generator carries the intent `create`. `wai wrap`
|
||
/// writes no other: not `improve`, not a string the manifest schema leaves open, and not
|
||
/// with the generator as the fallback of another capability.
|
||
#[test]
|
||
fn wrap_writes_the_generator_only_with_intent_create() {
|
||
let v = jload(&gen_dir().join("instr-v0.json"));
|
||
let object = scratch("intent.instr");
|
||
std::fs::write(&object, v["object"].as_str().unwrap()).unwrap();
|
||
for intent in ["improve", "banana", "Create", ""] {
|
||
let env = scratch("intent.wai");
|
||
let stderr = refused(&wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", intent, "-o", p(&env)]),
|
||
&env, &format!("intent {intent:?}"));
|
||
assert!(stderr.contains("carries the intent `create`"), "{intent:?}: {stderr}");
|
||
}
|
||
let env = scratch("intent.wai");
|
||
let stderr = refused(&wai(&["wrap", "wai.image.png", p(&object), "--fallback", "wai.generate.spectral",
|
||
"--intent", "improve", "-o", p(&env)]), &env, "the generator as the fallback");
|
||
assert!(stderr.contains("carries the intent `create`"), "{stderr}");
|
||
ok(&wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", "create", "-o", p(&env)]));
|
||
}
|
||
|
||
/// SPEC §5: `wai inspect` and `wai unwrap` report an envelope that names the generator —
|
||
/// its payload, its intent and any pin — ✓ when it conforms, ✗ with the reason when it
|
||
/// does not. Each bad `WAI1` differs from a good one in one respect: one prompt byte
|
||
/// changed to 0x01 in place (the envelope still parses; the payload is no longer JSON,
|
||
/// since a string holds no raw control character), the intent `improve`, or a pin
|
||
/// over the very model file `model` names. A `WAI2` component of the generator is
|
||
/// reported the same way, with and without a pin.
|
||
#[test]
|
||
fn inspect_and_unwrap_report_the_generators_envelope() {
|
||
use wai::container::{ManifestV2, Prior, RenditionMeta, WaiMulti};
|
||
let v = jload(&gen_dir().join("instr-v0.json"));
|
||
let object = v["object"].as_str().unwrap();
|
||
let input = scratch("report.instr");
|
||
std::fs::write(&input, object).unwrap();
|
||
let good = scratch("report-good.wai");
|
||
ok(&wai(&["wrap", "wai.generate.spectral", p(&input), "--intent", "create", "-o", p(&good)]));
|
||
let report = |env: &Path| (ok(&wai(&["unwrap", p(env)])), ok(&wai(&["inspect", p(env)])));
|
||
let (u, i) = report(&good);
|
||
assert!(u.contains("conformance \u{2713} an instruction object of form v0, intent create, no pin"), "{u}");
|
||
assert!(i.contains("instruction: \u{2713}") && !i.contains("does not conform"), "{i}");
|
||
|
||
let mut bytes = std::fs::read(&good).unwrap();
|
||
let at = bytes.windows(4).position(|w| w == b"calm").unwrap();
|
||
bytes[at] = 0x01;
|
||
let bad_payload = scratch("report-payload.wai");
|
||
std::fs::write(&bad_payload, &bytes).unwrap();
|
||
let mut e = Wai::read(&good).unwrap();
|
||
e.manifest.intent = "improve".into();
|
||
let bad_intent = scratch("report-intent.wai");
|
||
e.write(&bad_intent).unwrap();
|
||
let model_sha = sha256_hex(&std::fs::read(model()).unwrap());
|
||
let pin = Prior::new("the spectral model", model_sha, "integer generator");
|
||
let mut e = Wai::read(&good).unwrap();
|
||
e.manifest.conditioning.prior = Some(pin.clone());
|
||
let bad_pin = scratch("report-pin.wai");
|
||
e.write(&bad_pin).unwrap();
|
||
for (env, said) in [(&bad_payload, "not_json"), (&bad_intent, "the intent is `improve`"), (&bad_pin, "carries a pin")] {
|
||
let (u, i) = report(env);
|
||
assert!(u.contains("conformance \u{2717}") && u.contains(said), "{said}: {u}");
|
||
assert!(i.contains("instruction: \u{2717}") && i.contains(said) && i.contains("does not conform"),
|
||
"{said}: {i}");
|
||
}
|
||
|
||
for (pinned, name) in [(false, "report-two.wai"), (true, "report-two-pin.wai")] {
|
||
let mut meta = RenditionMeta::new("wai.generate.spectral", "bytes");
|
||
if pinned {
|
||
meta = meta.with_prior(pin.clone());
|
||
}
|
||
let two = WaiMulti::from_parts(ManifestV2::new("image", "create", vec![meta]), vec![object.as_bytes().to_vec()]).unwrap();
|
||
let path = scratch(name);
|
||
two.write(&path).unwrap();
|
||
let u = ok(&wai(&["unwrap", p(&path), "--component", "0"]));
|
||
let i = ok(&wai(&["inspect", p(&path)]));
|
||
let mark = if pinned { "\u{2717}" } else { "\u{2713}" };
|
||
assert!(u.contains(&format!("conformance [0] {mark}")), "{name}: {u}");
|
||
assert!(i.contains(&format!("[0] instruction: {mark}")), "{name}: {i}");
|
||
assert_eq!(pinned, u.contains("carries a pin") && i.contains("carries a pin"), "{name}");
|
||
}
|
||
}
|
||
|
||
/// Every score of the corpus: `build` from its authoring form writes the committed
|
||
/// `WSCR` byte for byte, and `render` prints, and `--pcm` writes, the digests
|
||
/// `expected_hashes.json` states.
|
||
#[test]
|
||
fn score_build_and_render_reproduce_the_corpus() {
|
||
let expected = jload(&score_dir().join("expected_hashes.json"));
|
||
let entries = expected.as_object().unwrap();
|
||
assert_eq!(entries.len(), 8);
|
||
for (file, spec) in entries {
|
||
let name = file.strip_suffix(".wscr").unwrap();
|
||
let committed = std::fs::read(score_dir().join("vectors").join(file)).unwrap();
|
||
let built = scratch(&format!("{name}.wscr"));
|
||
let notes = score_dir().join("notes").join(format!("{name}.json"));
|
||
let stdout = ok(&wai(&["score", "build", p(¬es), "-o", p(&built), "--json"]));
|
||
assert_eq!(std::fs::read(&built).unwrap(), committed, "{name}: built byte for byte");
|
||
let b: Value = data(&stdout);
|
||
assert_eq!(b["bytes"].as_u64(), Some(committed.len() as u64));
|
||
assert_eq!(b["score_hash"], spec["score_hash"], "{name}");
|
||
|
||
let pcm = scratch(&format!("{name}.s16"));
|
||
let stdout = ok(&wai(&["score", "render", p(&built), "--pcm", p(&pcm), "--json"]));
|
||
let r: Value = data(&stdout);
|
||
let duration = spec["duration"].as_u64().unwrap();
|
||
let channels = spec["channels"].as_u64().unwrap();
|
||
assert_eq!(r["samples"].as_u64(), Some(duration), "{name}: the contract's duration, whole");
|
||
assert_eq!(r["sample_rate"], spec["sample_rate"], "{name}");
|
||
assert_eq!(r["channels"].as_u64(), Some(channels), "{name}");
|
||
assert_eq!(r["notes"], spec["notes"]);
|
||
assert_eq!(r["pcm_sha256"], spec["pcm_sha256"], "{name}");
|
||
assert_eq!(r["mixdown_blake3"], spec["mixdown_hash"], "{name}");
|
||
assert_eq!(r["score_hash"], spec["score_hash"], "{name}");
|
||
let samples = std::fs::read(&pcm).unwrap();
|
||
assert_eq!(samples.len() as u64, duration * channels * 2, "{name}: i16 samples of every channel");
|
||
assert_eq!(Some(sha256_hex(&samples).as_str()), spec["pcm_sha256"].as_str(), "{name}: the written PCM");
|
||
|
||
// The score travels under intent `replicate`: its class is StandardDefined.
|
||
let env = scratch(&format!("{name}.wai"));
|
||
ok(&wai(&["wrap", "wai.audio.score", p(&built), "--intent", "replicate", "-o", p(&env)]));
|
||
let e = Wai::read(&env).unwrap();
|
||
assert_eq!((e.capability(), e.intent()), ("wai.audio.score", "replicate"));
|
||
assert_eq!(e.payload, committed);
|
||
}
|
||
}
|
||
|
||
/// The voiced two-channel example of score §2.3, as the extension prints it, builds and
|
||
/// renders: 72 000 frames of two channels at 48 kHz, interleaved, the note panned left
|
||
/// (its left louder than its right) and its echoes crossing to the right.
|
||
#[test]
|
||
fn the_extension_s_two_channel_example_builds_and_renders() {
|
||
let md = std::fs::read_to_string(root().join("../extensions/score.md")).unwrap();
|
||
let doc = md
|
||
.split("```json")
|
||
.skip(1)
|
||
.map(|b| b.split("```").next().unwrap())
|
||
.find(|b| b.contains("\"channels\": 2"))
|
||
.expect("score §2.3's two-channel example");
|
||
let input = scratch("example.json");
|
||
std::fs::write(&input, doc).unwrap();
|
||
let built = scratch("example.wscr");
|
||
ok(&wai(&["score", "build", p(&input), "-o", p(&built)]));
|
||
let pcm = scratch("example.s16");
|
||
let r: Value = data(&ok(&wai(&["score", "render", p(&built), "--pcm", p(&pcm), "--json"])));
|
||
assert_eq!((r["samples"].as_u64(), r["channels"].as_u64(), r["sample_rate"].as_u64()), (Some(72000), Some(2), Some(48000)), "{r}");
|
||
let b = std::fs::read(&pcm).unwrap();
|
||
assert_eq!(b.len(), 72000 * 2 * 2);
|
||
assert_eq!(Some(sha256_hex(&b).as_str()), r["pcm_sha256"].as_str());
|
||
let samples = |b: &[u8]| b.chunks(2).map(|c| i16::from_le_bytes([c[0], c[1]]) as i64).collect::<Vec<i64>>();
|
||
let s = samples(&b);
|
||
// The same note with no echoes: what the first echo adds before the second starts.
|
||
std::fs::write(&input, doc.replace("\"echo_taps\": 3", "\"echo_taps\": 0")).unwrap();
|
||
let dry_built = scratch("example-dry.wscr");
|
||
ok(&wai(&["score", "build", p(&input), "-o", p(&dry_built)]));
|
||
let dry_pcm = scratch("example-dry.s16");
|
||
ok(&wai(&["score", "render", p(&dry_built), "--pcm", p(&dry_pcm)]));
|
||
let dry = samples(&std::fs::read(&dry_pcm).unwrap());
|
||
let energy = |x: &[i64], from: usize, to: usize, ch: usize| (from..to).map(|f| x[2 * f + ch].pow(2)).sum::<i64>();
|
||
assert!(energy(&s, 0, 12000, 0) > 2 * energy(&s, 0, 12000, 1), "the note sits left");
|
||
assert_eq!(s[..2 * 12000], dry[..2 * 12000], "nothing echoes before the first echo");
|
||
let echo: Vec<i64> = s.iter().zip(&dry).map(|(a, b)| a - b).collect();
|
||
assert!(energy(&echo, 12000, 24000, 1) > 2 * energy(&echo, 12000, 24000, 0), "its first echo crosses right");
|
||
}
|
||
|
||
#[test]
|
||
fn score_build_refuses_what_is_not_integer_notes_rendered_whole() {
|
||
let note = r#"{"start":0,"dur":100,"phase_inc":50960238,"gain":1717986918,"waveform":2,"attack":10,"decay":10,"sustain":2147483648,"release":50}"#;
|
||
let score = |note: &str, top: &str| format!(r#"{{"sample_rate":22050,"channels":1,"duration":200{top},"notes":[{note}]}}"#);
|
||
// The note with a voice of the members given.
|
||
let voiced = |members: &str| note.replace(",\"release\":50}", &format!(",\"release\":50,\"voice\":{{{members}}}}}"));
|
||
// The same in two channels at 48 kHz.
|
||
let stereo = |members: &str| score(&voiced(members), "").replace("\"channels\":1", "\"channels\":2").replace("22050", "48000");
|
||
// The base case builds, and so does each form of it with a voice.
|
||
let input = scratch("base.json");
|
||
std::fs::write(&input, score(note, "")).unwrap();
|
||
let out = scratch("base.wscr");
|
||
ok(&wai(&["score", "build", p(&input), "-o", p(&out)]));
|
||
for doc in [
|
||
score(&voiced(""), ""),
|
||
score(&voiced(r#""decay_curve":8,"lp_poles":4,"lp_a":0,"lp_env":4294967296,"glide_to":0,"glide":200,"seed":4294967295,"echo_taps":1,"echo_delay":50,"echo_gain":4294967296"#), ""),
|
||
stereo(r#""pan":-4294967296,"echo_pan":4294967296,"echo_taps":8,"echo_delay":6,"echo_gain":2147483648"#),
|
||
] {
|
||
std::fs::write(&input, &doc).unwrap();
|
||
ok(&wai(&["score", "build", p(&input), "-o", p(&out)]));
|
||
let s = wai::score::Score::from_bytes(&std::fs::read(&out).unwrap()).unwrap();
|
||
assert!(s.voices.is_some(), "{doc}: a voiced score");
|
||
}
|
||
for (doc, what) in [
|
||
(score(¬e.replace("1717986918", "0.4"), ""), "a gain with a fraction"),
|
||
(score(¬e.replace("1717986918", "4294967297"), ""), "a gain past 1.0"),
|
||
(score(¬e.replace("50960238", "4294967296"), ""), "a phase_inc of a whole cycle"),
|
||
(score(¬e.replace("\"waveform\":2", "\"waveform\":9"), ""), "a waveform past 8"),
|
||
(score(¬e.replace("\"start\":0", "\"start\":-1"), ""), "a negative start"),
|
||
(score(¬e.replace("\"release\":50", "\"release\":101"), ""), "a release the duration would cut"),
|
||
(score(¬e.replace("\"dur\":100,", "\"dur\":100,\"pitch\":60,"), ""), "a member that is not a note field"),
|
||
(score(¬e.replace(",\"release\":50", ""), ""), "a missing note field"),
|
||
(score(¬e.replace("\"start\":0", "\"start\":0,\"start\":5"), ""), "a note field given twice"),
|
||
(score(note, ",\"duration\":300"), "a document member given twice"),
|
||
(score(note, ",\"numeric\":\"wai.det.float\""), "another numeric profile"),
|
||
(score(note, "").replace("\"channels\":1", "\"channels\":3"), "three channels"),
|
||
(score(&voiced(r#""pan":1073741824"#), ""), "a pan in one channel"),
|
||
(score(&voiced(r#""decay_curve":0"#), ""), "a curve of 0"),
|
||
(score(&voiced(r#""release_curve":9"#), ""), "a curve past 8"),
|
||
(score(&voiced(r#""lp_poles":5"#), ""), "five low-pass sections"),
|
||
(score(&voiced(r#""lp_a":4294967297"#), ""), "a low-pass coefficient past 1.0"),
|
||
(score(&voiced(r#""glide_to":4294967296"#), ""), "a glide to a whole cycle"),
|
||
(score(&voiced(r#""glide":10"#), ""), "a glide to no pitch"),
|
||
(score(&voiced(r#""echo_taps":9,"echo_delay":1"#), ""), "nine echoes"),
|
||
(score(&voiced(r#""echo_taps":1"#), ""), "an echo of no delay"),
|
||
(score(&voiced(r#""echo_taps":1,"echo_delay":51"#), ""), "an echo the duration would cut"),
|
||
(score(&voiced(r#""echo_gain":4294967297"#), ""), "an echo louder than its note"),
|
||
(score(&voiced(r#""seed":4294967296"#), ""), "a seed past 32 bits"),
|
||
(score(&voiced(r#""cutoff":1"#), ""), "a member that is not a voice field"),
|
||
(score(&voiced(r#""seed":1,"seed":2"#), ""), "a voice field given twice"),
|
||
(score(¬e.replace(",\"release\":50}", ",\"release\":50,\"voice\":[]}"), ""), "a voice that is not an object"),
|
||
(stereo(r#""pan":4294967297"#), "a pan past 1.0"),
|
||
(stereo(r#""echo_pan":-4294967297"#), "an echo pan past -1.0"),
|
||
(stereo(r#""pan":0.5"#), "a pan with a fraction"),
|
||
(score(note, "").replace("\"channels\":1", "\"channels\":2").replace("\"duration\":200", "\"duration\":33554433"), "two channels past 2^26 samples"),
|
||
(score(note, "").replace("\"duration\":200", "\"duration\":67108865"), "a duration past 2^26 samples"),
|
||
("[]".to_string(), "not an object"),
|
||
] {
|
||
std::fs::write(&input, &doc).unwrap();
|
||
let out = scratch("refused.wscr");
|
||
let stderr = refused(&wai(&["score", "build", p(&input), "-o", p(&out)]), &out, what);
|
||
assert!(stderr.contains("refused"), "{what}: {stderr}");
|
||
}
|
||
}
|
||
|
||
/// `render` refuses a contract it would misreport: the reference mixdown is one channel
|
||
/// or two in `wai.det.fixed64`, so a contract stating three channels or another numeric
|
||
/// profile is refused, as `build` refuses to write one. Each case is the committed
|
||
/// scale.wscr with its contract patched in place.
|
||
#[test]
|
||
fn score_render_refuses_a_contract_it_does_not_render() {
|
||
let committed = std::fs::read(score_dir().join("vectors/scale.wscr")).unwrap();
|
||
let patch = |from: &str, to: &str| {
|
||
assert_eq!(from.len(), to.len(), "a patch in place keeps every offset");
|
||
let at = committed.windows(from.len()).position(|w| w == from.as_bytes()).unwrap();
|
||
let mut b = committed.clone();
|
||
b[at..at + to.len()].copy_from_slice(to.as_bytes());
|
||
b
|
||
};
|
||
for (bytes, what, said) in [
|
||
(patch("\"channels\":1", "\"channels\":3"), "three channels", "`channels` is 3"),
|
||
(patch("wai.det.fixed64", "wai.det.float64"), "another numeric profile", "wai.det.float64"),
|
||
] {
|
||
let input = scratch("contract.wscr");
|
||
std::fs::write(&input, bytes).unwrap();
|
||
let pcm = scratch("contract.s16");
|
||
let stderr = refused(&wai(&["score", "render", p(&input), "--pcm", p(&pcm), "--json"]), &pcm, what);
|
||
assert!(stderr.contains("refused") && stderr.contains(said), "{what}: {stderr}");
|
||
}
|
||
}
|
||
|
||
/// A `WSCR` that declares more notes than its notes section holds is refused before the
|
||
/// reader reserves room for them: a file of a few bytes can declare 2^32 − 1. The file
|
||
/// carries a well-formed contract (section 0x01) and a notes section (0x02) holding only
|
||
/// the count, so without the check the reader would pass every other rule and reach the
|
||
/// reservation; the check, not a missing section, is what refuses it.
|
||
#[test]
|
||
fn score_render_refuses_a_note_count_its_bytes_cannot_hold() {
|
||
let contract = wai::score::ScoreContract {
|
||
sample_rate: 22050, numeric: wai::world::NUMERIC_FIXED64.into(), channels: 1, duration: 100,
|
||
}
|
||
.to_canonical_json();
|
||
let mut b = b"WSCR".to_vec();
|
||
b.extend_from_slice(&2u16.to_le_bytes());
|
||
for (kind, off, len) in [(0x01u8, 0u32, contract.len() as u32), (0x02, contract.len() as u32, 4)] {
|
||
b.push(kind);
|
||
b.extend_from_slice(&off.to_le_bytes());
|
||
b.extend_from_slice(&len.to_le_bytes());
|
||
}
|
||
b.extend_from_slice(&contract);
|
||
b.extend_from_slice(&u32::MAX.to_le_bytes());
|
||
// The same file declaring no notes renders: the contract and the layout are good.
|
||
let mut empty = b.clone();
|
||
let n = empty.len();
|
||
empty[n - 4..].copy_from_slice(&0u32.to_le_bytes());
|
||
let input = scratch("empty.wscr");
|
||
std::fs::write(&input, empty).unwrap();
|
||
let r: Value = data(&ok(&wai(&["score", "render", p(&input), "--json"])));
|
||
assert_eq!((r["samples"].as_u64(), r["notes"].as_u64()), (Some(100), Some(0)), "{r}");
|
||
let input = scratch("huge.wscr");
|
||
std::fs::write(&input, b).unwrap();
|
||
let pcm = scratch("huge.s16");
|
||
let stderr = refused(&wai(&["score", "render", p(&input), "--pcm", p(&pcm)]), &pcm, "2^32 - 1 notes in 4 bytes");
|
||
assert!(stderr.contains("declares 4294967295 notes"), "{stderr}");
|
||
}
|