Classic Wave, Silk Ribbon, Kaleido Tunnel and Slow Orb all declared the `style` trait and honoured it with `col += sigGrain(uv)` and nothing else. A declared trait is a contract — the disqualification rule in Personality.js is the only thing keeping off-design scenes out of a track — so honouring it with dirt meant these four could not take the `texture: 0` opt-out the grain work introduced. Phase 9 measured their style response at exactly 0. They sat at texture: 0.35 as a stopgap, which kept speckle on the library's cleanest scenes purely to keep a gate green. Each already had the knob; it just was not wired to the track: - Classic Wave contrasts its wave through a bare smoothstep(0.2, 0.8). The transition width now comes from u_sigSoft and u_sigLine, centred on 0.5 so changing the hand does not change the exposure. Crest concentration is driven separately by u_sigLine, because a track is free to sample the scene's own u_softness at zero and the art direction must still show. - Silk Ribbon's strand width is u_sigLine and its falloff exponent u_sigSoft: a sharp track gets a filament with a defined edge, a soft one a haze. - Kaleido Tunnel drew its grid against a bare 0.42 — a line weight with no name. It is now a weight and a feather, both from the track. - Slow Orb's body edge multiplies the scene's softness by the video's, and gains a sigEdge rim so a sharp-handed track gets a defined limb. All four are now texture: 0. Style response measured against a maximally soft-handed versus maximally sharp-handed personality: Classic Wave 111, Silk Ribbon 216, Kaleido Tunnel 206, Slow Orb 102, out of 255 — previously 0. 104/104 checks pass including the slow set. See SIDE-QUESTS.md §1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
86 lines
2.8 KiB
HTML
86 lines
2.8 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head><title>decode test</title></head>
|
|
<body>
|
|
<div id="out">running…</div>
|
|
<script type="module">
|
|
import { hashFrame } from '/src/engine/hash.js';
|
|
|
|
const out = document.getElementById('out');
|
|
const log = (m) => { out.textContent += m + '\n'; };
|
|
|
|
const resp = await fetch('/export.mp4');
|
|
const blob = await resp.blob();
|
|
const frames = [];
|
|
|
|
function findBox(buf, start, type) {
|
|
let p = start;
|
|
while (p + 8 <= buf.length) {
|
|
let size = (buf[p] << 24) | (buf[p+1] << 16) | (buf[p+2] << 8) | buf[p+3];
|
|
const t = String.fromCharCode(buf[p+4], buf[p+5], buf[p+6], buf[p+7]);
|
|
const hdr = size === 1 ? 16 : 8;
|
|
const boxEnd = size === 1 ? p + Number(((buf[p+8] * 4294967296) + ((buf[p+9] << 24) | (buf[p+10] << 16) | (buf[p+11] << 8) | buf[p+12]))) : p + size;
|
|
if (boxEnd < p + hdr || boxEnd > buf.length) break;
|
|
if (t === type) return { data: p + hdr, end: boxEnd };
|
|
if (['moov','trak','mdia','minf','stbl','stsd'].includes(t)) {
|
|
const r = findBox(buf, p + hdr, type);
|
|
if (r) return r;
|
|
}
|
|
p = boxEnd;
|
|
}
|
|
return null;
|
|
}
|
|
const buf2 = new Uint8Array(await blob.arrayBuffer());
|
|
const avcC = findBox(buf2, 0, 'avcC');
|
|
let description = null;
|
|
if (avcC) {
|
|
description = buf2.slice(avcC.data, avcC.end);
|
|
log(`found avcC (${description.length} bytes)`);
|
|
} else {
|
|
log('no avcC box found');
|
|
}
|
|
|
|
const decoder = new VideoDecoder({
|
|
output: (frame) => { frames.push(frame); },
|
|
error: (e) => log('DECODER ERROR: ' + e.message),
|
|
});
|
|
decoder.configure({ codec: 'avc1.640034', description });
|
|
const reader = blob.stream().getReader();
|
|
let buf = new Uint8Array(0);
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
const next = new Uint8Array(buf.length + value.length);
|
|
next.set(buf); next.set(value, buf.length);
|
|
buf = next;
|
|
}
|
|
const chunk = new EncodedVideoChunk({ type: 'key', timestamp: 0, duration: 0, data: buf });
|
|
decoder.decode(chunk);
|
|
await decoder.flush();
|
|
log(`decoded ${frames.length} frames`);
|
|
|
|
// hash a small downscaled copy of each frame
|
|
const canvas = new OffscreenCanvas(64, 36);
|
|
const ctx = canvas.getContext('2d');
|
|
const hashes = [];
|
|
for (const f of frames) {
|
|
ctx.drawImage(f, 0, 0, 64, 36);
|
|
const img = ctx.getImageData(0, 0, 64, 36);
|
|
hashes.push(hashFrame(img.data));
|
|
f.close();
|
|
}
|
|
let dupRuns = 0, maxRun = 0, curRun = 1;
|
|
for (let i = 1; i < hashes.length; i++) {
|
|
if (hashes[i] === hashes[i-1]) { curRun++; }
|
|
else { if (curRun > 1) dupRuns++; maxRun = Math.max(maxRun, curRun); curRun = 1; }
|
|
}
|
|
maxRun = Math.max(maxRun, curRun);
|
|
const distinct = new Set(hashes).size;
|
|
log(`distinct frames: ${distinct} / ${hashes.length}`);
|
|
log(`duplicate runs: ${dupRuns}, longest run: ${maxRun}`);
|
|
log('DONE');
|
|
window.__DECODE_DONE__ = true;
|
|
</script>
|
|
</body>
|
|
</html>
|