open-standards/wai/wai-web/compose/page.js
Transaction Science bf7c9fcfee
All checks were successful
WAI operation catalogue / does this change touch it (push) Successful in 17s
wai-fleet service / does this change touch it (push) Successful in 16s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 25s
wai-app and Create / wai-app · does this change touch it (push) Successful in 23s
wai-ledger service / does this change touch it (push) Successful in 19s
wai-sink-swift native sink package / does this change touch it (push) Successful in 22s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 53s
WAI Studio / does this change touch it (push) Successful in 18s
wai-quantum for JavaScript / does this change touch it (push) Successful in 54s
wai-audit collector and player integration / does this change touch it (push) Successful in 57s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 1m7s
wai-key-proxy / does this change touch it (push) Successful in 57s
wai-quantum studio and desktop / does this change touch it (push) Successful in 55s
wai-sink-android native sink library / does this change touch it (push) Successful in 55s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m46s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 5m2s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 5m5s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 6m41s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 16s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 15s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 1m2s
wai-fleet service / service · linux (push) Successful in 3m16s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m14s
wai-ledger service / service · linux (push) Successful in 4m28s
wai-audit collector and player integration / collector · linux (push) Successful in 5m1s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 8m15s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m28s
wai-quantum studio and desktop / studio · browser (push) Successful in 5m18s
wai-key-proxy / proxy · linux (push) Successful in 3m1s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 10m1s
wai-quantum for JavaScript / javascript · linux (push) Successful in 7m14s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 12m23s
wai-fleet service / service · macos (push) Successful in 38s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 7m35s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 9m15s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 9m29s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 12m24s
WAI Studio / Studio in WebKit · linux (push) Successful in 20m25s
wai-ledger service / service · macos (push) Successful in 1m28s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 7m54s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 20m38s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 15m54s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m9s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 49s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 10m34s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 4m34s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 15m59s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 6m42s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 7m0s
The browser sink presents a graphics component's text floor, with captions as graphics-vector §6.4 has them
browser-sink §14.9: where a device has no decoder of wai.graphics.vector, a composing
page presents each graphics piece's floor (graphics-vector §6.3), the piece's transcript
as wai.text.zstd, as text: the blocks active at the clock's tick, in block order, each
with its language, direction and writing mode. A piece presents its overlay or its
floor, never both.

Before presenting, the page holds the floor object to its piece: its derived_from names
the piece's base (floor_base), its payload is the raw frame and no other Zstandard
frame (floor_frame), its target states the transcript's length and a timebase
(floor_target), and its content is a transcript in graphics-vector §5.4's one encoding
(floor_transcript).

A caption or a subtitle block is never presented from the floor (caption_track): the
text component of its language carries it, and the text layer presents it or not as the
viewer set it. A forced_narrative block is presented from the floor unless the text
layer shows a cue of its language with its string at that millisecond (forced_in_cue),
so the words are on the page once. The text layer presents one text component of a
package, or none.

wai-rs browser_sink::floor is the reference (read, present), which wai-web's compose
bundle calls as TextFloor. Corpus live-conformance/browser-floor/ (49 floors, 25
presentations), written by wai_browser_floor_vectors and regenerating byte for byte on
x86_64 and aarch64; tests/browser_floor.rs also reads the floors wai-pack wrote for
pack-conformance/multi/multi-captions-01, and wai-web's compose::floor_tests holds them
to wai-vector's transcripts. browser_floor_verify.py, the second implementation,
refuses 72 mutated floors each with its defect's reason, fails 23 mutated copies of the
corpus, and each of its 32 checks, dropped, is caught. compose/run.mjs gains case 10.

Not here: a base that is routed and then refused when loaded still refuses the package,
and the overlay path does not yet leave out caption items (graphics-vector §5.7).

Co-Authored-By: Claude <[email protected]>
2026-11-10 22:36:11 -05:00

228 lines
11 KiB
JavaScript
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.

// The reference page of the composing sink: opens a composed package with compose.js,
// shows what this device dispatched and why, lists every check made before presenting,
// and plays the components on one timeline.
//
// Query parameters:
// pkg the package's directory, default "package/";
// receipt the receipt's URL, default <pkg>package.receipt.json;
// without capabilities to force the device profile without, comma separated;
// captions the text component the text layer presents: a language tag, or "off" for none;
// default the first the receipt lists (browser-sink §14.9);
// bundle the wai_compose module, default "./pkg/wai_compose.js";
// set the outline set the page holds for wai.graphics.vector, default the default
// stroke set of wai/vector-conformance;
// gs the parameter set the page holds for wai.splat.int_codec (`g_s.whm`), default the
// pinned synthesis of wai-rs/tests/vectors/int_splat.
//
// window.__wai_compose is the hook a test reads: {state, refusal, dispatch, checks, stats,
// audio, floor, staged, play(), overlayDigests(), pictureSha256(), presentAt(t), shownAt(t)}.
// `floor` is null unless a graphics piece presents its text floor; otherwise its pieces, each
// with its transcript. `shownAt(t)` presents at `t` and returns what is then on the page: the
// overlay's frame or null, the floor's result (§14.9) or null, the text and attributes of each
// block the floor's region holds, and the text of each cue in the text layer. `staged` is null for a
// package with no layered piece; otherwise {state: "refining" | "done", stages}, every stage
// presented with the SHA-256 the page computed and the SHA-256 of the picture read back from the
// canvas once it was drawn.
import { openPackage, Refused } from "./compose.js";
const q = new URLSearchParams(location.search);
const base = new URL(q.get("pkg") || "package/", location.href).href;
const without = (q.get("without") || "").split(",").filter(Boolean);
const bundle = q.get("bundle") || "./pkg/wai_compose.js";
const sets = [q.get("set") || "../../vector-conformance/sets/stroke-ascii.wos1"];
const gs = q.get("gs") || "../../wai-rs/tests/vectors/int_splat/splat_gs.bin";
const $ = (id) => document.getElementById(id);
const hook = { state: "opening", refusal: null, dispatch: null, checks: [], stats: null, staged: null };
window.__wai_compose = hook;
const status = (text, bad) => {
$("status").textContent = text;
$("status").className = bad ? "bad" : "";
};
function cell(tr, text, cls) {
const td = document.createElement("td");
td.textContent = text;
if (cls) td.className = cls;
tr.appendChild(td);
return td;
}
function showChecks(checks) {
const body = $("checks").tBodies[0];
body.replaceChildren();
for (const c of checks) {
const tr = document.createElement("tr");
cell(tr, c.ok ? "✓" : "✗", c.ok ? "ok" : "bad").setAttribute("aria-label", c.ok ? "holds" : "refused");
cell(tr, c.what);
const extra = Object.entries(c).filter(([k, v]) => !["what", "ok"].includes(k) && v !== null && v !== undefined);
cell(tr, extra.map(([k, v]) => `${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`).join(" · "), "meta");
body.appendChild(tr);
}
}
function showDispatch(d) {
const body = $("dispatch").tBodies[0];
body.replaceChildren();
for (const c of d.components) {
c.pieces.forEach((p, i) => {
const tr = document.createElement("tr");
cell(tr, i === 0 ? `${c.id} (${c.layer || "not presented"})` : "");
cell(tr, String(p.piece));
cell(tr, p.capability ? `${p.capability} (${p.role || "record"})` : "nothing", p.capability ? "" : "bad");
cell(tr, p.route);
cell(tr, p.why, "meta");
body.appendChild(tr);
// Each layer rides on the base it refines: its outcome, and the side planes it read.
for (const L of p.layers || []) {
const lr = document.createElement("tr");
cell(lr, "");
cell(lr, "");
cell(lr, `layer ${L.layer}: ${L.capability}`, L.outcome === "refused" ? "bad" : "");
cell(lr, L.route);
const sides = L.sides.map((x) => `${x.entry || x.object.slice(0, 12)}${x.render ? " rendered here" : ""}: ${x.held ? (x.exact ? "held, exact" : "held, not exact") : "not held" + (x.why ? ` (${x.why})` : "")}`);
cell(lr, `${L.outcome}${L.stage !== null ? `, stage ${L.stage}` : ""}${L.reason ? ` (${L.reason}${L.detail ? ": " + L.detail : ""})` : ""}${sides.length ? `; side planes: ${sides.join("; ")}` : ""}`, "meta");
body.appendChild(lr);
}
});
}
const dev = d.device;
$("device").textContent = `Device: ${dev.user_agent}. WebAssembly decoders: ${dev.wasm.join(", ")}. ` +
`User agent answers: ${Object.entries(dev.ua).map(([k, v]) => `${k} supported=${v.supported} smooth=${v.smooth} powerEfficient=${v.power_efficient}`).join("; ") || "none asked"}.` +
(dev.forced_without.length ? ` Forced profile: without ${dev.forced_without.join(", ")}.` : "");
}
function showState(tick, records, hash) {
const box = $("state");
const t = document.createElement("table");
for (const r of records) {
const tr = document.createElement("tr");
for (const c of r.cells) cell(tr, c.t === "fixed" ? c.v.toFixed(2) : String(c.v));
t.appendChild(tr);
}
const m = document.createElement("div");
m.className = "meta";
m.textContent = `tick ${tick} · state ${hash.slice(0, 16)}…`;
box.replaceChildren(t, m);
}
$("profile").value = without.join(",");
if ($("profile").value !== without.join(",")) {
const o = document.createElement("option");
o.value = o.textContent = without.join(",");
$("profile").appendChild(o);
$("profile").value = o.value;
}
$("profile").addEventListener("change", () => {
const u = new URL(location.href);
if ($("profile").value) u.searchParams.set("without", $("profile").value);
else u.searchParams.delete("without");
location.href = u.href;
});
try {
const mod = await import(bundle);
await mod.default();
const dpr = globalThis.devicePixelRatio || 1;
const r = $("overlay").getBoundingClientRect();
const comp = await openPackage(base, {
wai: mod,
receipt: q.get("receipt") ? new URL(q.get("receipt"), location.href).href : undefined,
sets: sets.map((s) => new URL(s, location.href).href),
paramSets: { "g_s.whm": new URL(gs, location.href).href },
without,
captions: q.get("captions") || undefined,
overlayPx: [Math.round(r.width * dpr), Math.round(r.height * dpr)],
});
hook.dispatch = comp.dispatch;
hook.checks = comp.checks;
hook.stats = comp.stats;
const au = comp.layers.audio;
hook.audio = au ? { sample_rate: au.sampleRate, channels: au.channels, samples: au.frames, pcm_sha256: au.pcmSha256, pieces: au.pieces.map((p) => ({ start: p.start, samples: p.samples.length / au.channels, pcm_sha256: p.pcmSha256 })) } : null;
hook.overlayDigests = () => comp.overlayDigests(mod);
// The picture as drawn: the video canvas read back, its RGB8 hashed in the page.
const pictureSha256 = () => {
const c = $("video");
const px = c.getContext("2d").getImageData(0, 0, c.width, c.height).data;
const rgb = new Uint8Array((px.length / 4) * 3);
for (let i = 0, j = 0; i < px.length; i += 4, j += 3) {
rgb[j] = px[i];
rgb[j + 1] = px[i + 1];
rgb[j + 2] = px[i + 2];
}
return mod.sha256Hex(rgb);
};
const view = { video: $("video"), overlay: $("overlay"), floor: $("floor"), floorAnnounce: $("floor-announce"), text: $("text"), state: showState, readback: pictureSha256 };
const fl = comp.layers.floor;
hook.floor = fl ? { component: fl.component, pieces: fl.pieces.map((p) => ({ piece: p.piece, start: p.start, duration: p.duration, tb: p.tb, transcript: p.transcript })) } : null;
hook.text = comp.dispatch.text;
hook.shownAt = (t) => {
comp.present(t, view);
return {
overlay: comp.now.overlay,
floor: comp.now.floor,
blocks: [...$("floor").children].map((el) => ({ text: el.textContent, lang: el.lang, dir: el.dir, index: Number(el.dataset.index), markup: el.children.length })),
announced: [...$("floor-announce").children].map((el) => el.textContent),
cues: comp.now.cues,
};
};
hook.pictureSha256 = pictureSha256;
hook.presentAt = (t) => {
comp._n = undefined;
comp.present(t, view);
return pictureSha256();
};
hook.play = async () => {
hook.state = "playing";
$("play").disabled = true;
await comp.play(view);
hook.state = "ended";
$("play").disabled = false;
};
showDispatch(comp.dispatch);
showChecks(comp.checks);
comp.present(0, view);
const floors = comp.dispatch.components.flatMap((c) => c.pieces.filter((p) => p.role === "floor").map((p) => `${c.id}/${p.piece}`));
const missing = comp.dispatch.components.filter((c) => !c.presented).map((c) => c.id);
const retried = comp.dispatch.components.flatMap((c) => c.pieces.filter((p) => p.tried).map((p) => `${c.id}/${p.piece}`));
const opened = `Every check holds (${comp.checks.length}). ` +
(floors.length ? `The floor plays for ${floors.join(", ")}. ` : "") +
(fl ? `The titles of ${fl.component} are presented as text from their floor. ` : "") +
(missing.length ? `Not presented on this device: ${missing.join(", ")}. ` : "") +
(retried.length ? `The user agent's decoder did not decode ${retried.join(", ")}; the next alternative plays. ` : "");
status(opened + (comp.staged.length ? "Each layered piece presents its base; its layers follow as they arrive." : "Ready."));
const tick = () => {
$("clock").textContent = `clock ${comp.stats.clock_s.toFixed(2)} s of ${comp.duration.toFixed(2)} s · video frames ${comp.stats.video_frames} · overlay frames ${comp.stats.overlay_frames} · state updates ${comp.stats.state_updates} · audio ${comp.stats.audio_started ? "playing, re-placed " + comp.stats.audio_resyncs + "×" : "not started"}`;
requestAnimationFrame(tick);
};
tick();
$("play").disabled = false;
$("play").addEventListener("click", () => hook.play());
hook.state = "ready";
if (comp.staged.length) {
// The layers, as they arrive: each stage presented once it holds its signed digest.
hook.staged = { state: "refining", stages: comp.stages };
comp.present(0, view);
if (comp.stages.length) comp.stages[0].presented_sha256 = pictureSha256();
await comp.refine(view, () => {
showDispatch(comp.dispatch);
showChecks(comp.checks);
});
showDispatch(comp.dispatch);
showChecks(comp.checks);
comp.present(0, view);
const ls = comp.dispatch.components.flatMap((c) => c.pieces.flatMap((p) => (p.layers || []).map((L) => ({ ...L, at: `${c.id}/${p.piece}` }))));
const refused = ls.filter((L) => L.outcome !== "applied");
const top = comp.staged.map((it) => `${it.c.id}/${it.pi} at stage ${it.vp.stage}`);
status(opened + `Presented: ${top.join(", ")}. ` +
refused.map((L) => `The layer ${L.name} was not applied (${L.reason}); ${L.at} presents the stage below. `).join("") + "Ready.", refused.length > 0);
hook.staged.state = "done";
}
} catch (e) {
const r = e instanceof Refused ? { stage: e.stage, code: e.code, detail: e.detail, where: e.where } : { stage: "page", code: "error", detail: String(e && e.stack || e), where: null };
hook.refusal = r;
hook.checks = (e && e.checks) || [];
hook.state = "refused";
status(`Refused, nothing presented: ${r.stage} ${r.code}${r.where ? " at " + r.where : ""}${r.detail ? " (" + r.detail + ")" : ""}`, true);
if (e && e.checks) showChecks(e.checks);
}