music-video-gen/flow-state/_decodetest.html
Dejvino 17a583a87f 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>
2026-08-06 19:35:50 +02:00

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>