Side quest 1: give four scenes art direction other than grain
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>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user