// Flash-rate safety. // // This project generates beat-reactive video that gets published. Rapid // light-dark cycling between roughly 3 and 50 Hz is the photosensitive-epilepsy // trigger, and a generator that flashes a bright scene on every kick of a 128 BPM // track sits right in that band. WCAG 2.3.1 and the Harding test both use a // three-flashes-per-second ceiling, which is what this measures. // // It is deliberately part of the gate rather than an afterthought: an // unsupervised generator will find these states on its own, and nobody watches // every frame of every export. /** One flash = a min→max→min luminance cycle with amplitude at or above `threshold`. */ export function countFlashes(luminance, { threshold = 0.1 } = {}) { if (luminance.length < 3) return 0; const extrema = []; for (let i = 1; i < luminance.length - 1; i++) { const a = luminance[i - 1], b = luminance[i], c = luminance[i + 1]; if ((b > a && b >= c) || (b < a && b <= c)) { extrema.push({ index: i, value: b, isMax: b > a }); } } let flashes = 0; for (let i = 1; i < extrema.length - 1; i++) { const prev = extrema[i - 1], here = extrema[i], next = extrema[i + 1]; if (!here.isMax) continue; const rise = here.value - prev.value; const fall = here.value - next.value; if (rise >= threshold && fall >= threshold) flashes++; } return flashes; } /** Flashes per second over a luminance series sampled at `fps`. */ export function flashRate(luminance, fps) { const seconds = luminance.length / fps; return seconds > 0 ? countFlashes(luminance) / seconds : 0; } /** * Worst flash rate in any one-second window. A track that averages 2/s but has a * drop running at 8/s is not safe, and the average would hide it. */ export function peakFlashRate(luminance, fps) { const window = Math.round(fps); if (luminance.length <= window) return flashRate(luminance, fps); let worst = 0; for (let i = 0; i + window < luminance.length; i += Math.max(1, Math.round(fps / 4))) { worst = Math.max(worst, countFlashes(luminance.slice(i, i + window))); } return worst; }