music-video-gen/flow-state/_encprobe.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

132 lines
4.8 KiB
HTML
Raw 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.

<!DOCTYPE html>
<html>
<head><title>encoder probe</title></head>
<body>
<pre id="out">running…</pre>
<canvas id="c" width="1920" height="1080" style="display:none"></canvas>
<script type="module">
const out = document.getElementById('out');
const log = (m) => { out.textContent += m + '\n'; console.log(m); };
// A cheap animated source. The point is to test the ENCODER, so the frames must
// be genuinely different (a static source would let the encoder coast) but must
// cost nothing to produce — no shaders, no Show, no GPU readback.
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
function drawFrame(i) {
ctx.fillStyle = `hsl(${(i * 3) % 360} 60% 12%)`;
ctx.fillRect(0, 0, 1920, 1080);
ctx.fillStyle = `hsl(${(i * 7) % 360} 80% 60%)`;
for (let k = 0; k < 40; k++) {
const x = ((i * 11 + k * 137) % 1920);
const y = ((i * 5 + k * 271) % 1080);
ctx.fillRect(x, y, 90, 90);
}
ctx.fillStyle = '#fff';
ctx.font = '120px monospace';
ctx.fillText(String(i), 60, 200);
}
const FPS = 60;
const FRAMES = 600;
const WIDTH = 1920, HEIGHT = 1080, BITRATE = 16_000_000;
const MAX_QUEUE = 8;
function waitForQueue(encoder) {
if (encoder.encodeQueueSize < MAX_QUEUE) return Promise.resolve();
return new Promise((resolve) => {
const onDequeue = () => {
if (encoder.encodeQueueSize < MAX_QUEUE) {
encoder.removeEventListener('dequeue', onDequeue);
resolve();
}
};
encoder.addEventListener('dequeue', onDequeue);
});
}
async function run(label, extra) {
const config = {
codec: 'avc1.640034',
width: WIDTH, height: HEIGHT, bitrate: BITRATE, framerate: FPS,
avc: { format: 'avc' },
...extra,
};
let support;
try {
support = await VideoEncoder.isConfigSupported(config);
} catch (e) {
log(`${label.padEnd(34)} isConfigSupported threw: ${e.message}`);
return;
}
if (!support.supported) {
log(`${label.padEnd(34)} UNSUPPORTED`);
return;
}
let outputs = 0;
let keyframes = 0;
const stamps = [];
let error = null;
const encoder = new VideoEncoder({
output: (chunk) => {
outputs++;
if (chunk.type === 'key') keyframes++;
stamps.push(chunk.timestamp);
},
error: (e) => { error = e; },
});
encoder.configure(config);
const t0 = performance.now();
let encoded = 0;
for (let i = 0; i < FRAMES; i++) {
if (error) break;
drawFrame(i);
await waitForQueue(encoder);
const frame = new VideoFrame(canvas, {
timestamp: Math.round((i * 1e6) / FPS),
duration: Math.round(1e6 / FPS),
});
encoder.encode(frame, { keyFrame: i % (FPS * 2) === 0 });
frame.close();
encoded++;
if (i % 10 === 0) await new Promise((r) => setTimeout(r, 0));
}
try { await encoder.flush(); } catch (e) { error = error || e; }
const ms = performance.now() - t0;
try { encoder.close(); } catch { /* already closed */ }
// Do NOT sort: the order chunks ARRIVE in is the whole point. WebCodecs
// delivers in decode order, so a timestamp that goes backwards means the
// encoder emitted B-frames — and a muxer handed presentation timestamps as
// if they were decode timestamps will reject exactly those chunks.
const period = 1e6 / FPS;
const backwards = stamps.filter((t, i) => i > 0 && t < stamps[i - 1]).length;
const sorted = stamps.slice().sort((a, b) => a - b);
const gaps = sorted.slice(1).map((t, i) => Math.round((t - sorted[i]) / period));
const maxGap = gaps.length ? Math.max(...gaps) : 0;
const verdict = error ? `ERROR ${error.message}`
: backwards ? `REORDERED — ${backwards} chunks arrived out of order (B-frames)`
: outputs === encoded ? 'OK'
: `DROPPED ${encoded - outputs} (${((1 - outputs / encoded) * 100).toFixed(0)}%)`;
log(`${label.padEnd(34)} in ${encoded} → out ${outputs} keys ${keyframes} maxGap ${maxGap} reordered ${backwards} ${(ms / 1000).toFixed(1)}s ${verdict}`);
}
log(`source ${WIDTH}×${HEIGHT} @ ${FPS} · ${FRAMES} frames · bitrate ${BITRATE / 1e6} Mbps\n`);
await run('default (no hints)', {});
await run('prefer-hardware', { hardwareAcceleration: 'prefer-hardware' });
await run('prefer-software', { hardwareAcceleration: 'prefer-software' });
await run('latency=realtime', { latencyMode: 'realtime' });
await run('prefer-hardware + realtime', { hardwareAcceleration: 'prefer-hardware', latencyMode: 'realtime' });
await run('prefer-software + realtime', { hardwareAcceleration: 'prefer-software', latencyMode: 'realtime' });
await run('baseline avc1.42003e', { codec: 'avc1.42003e' });
log('\nDONE');
window.__DONE__ = true;
</script>
</body>
</html>