import { useState, useCallback } from 'react'; import { autoDetectMultiFrame, type DetectOptions } from '@core/detector.js'; import { attackReencode, attackDownscale, attackBrightness, attackContrast, attackSaturation, attackCrop } from '../lib/video-io.js'; type TestStatus = 'idle' | 'running' | 'pass' | 'fail' | 'error'; interface TestResult { status: TestStatus; confidence?: number; payloadMatch?: boolean; } interface RobustnessTestProps { blob: Blob; width: number; height: number; payload: string; secretKey: string; } interface TestDef { label: string; category: string; run: (blob: Blob, w: number, h: number) => Promise<{ yPlanes: Uint8Array[]; width: number; height: number }>; detectOptions?: DetectOptions; } const TESTS: TestDef[] = [ // CRF re-encoding { label: 'CRF 23', category: 'Re-encode', run: (b, w, h) => attackReencode(b, 23, w, h) }, { label: 'CRF 28', category: 'Re-encode', run: (b, w, h) => attackReencode(b, 28, w, h) }, { label: 'CRF 33', category: 'Re-encode', run: (b, w, h) => attackReencode(b, 33, w, h) }, { label: 'CRF 38', category: 'Re-encode', run: (b, w, h) => attackReencode(b, 38, w, h) }, { label: 'CRF 43', category: 'Re-encode', run: (b, w, h) => attackReencode(b, 43, w, h) }, // Downscale { label: '25%', category: 'Downscale', run: (b, w, h) => attackDownscale(b, 25, w, h) }, { label: '50%', category: 'Downscale', run: (b, w, h) => attackDownscale(b, 50, w, h) }, { label: '75%', category: 'Downscale', run: (b, w, h) => attackDownscale(b, 75, w, h) }, { label: '90%', category: 'Downscale', run: (b, w, h) => attackDownscale(b, 90, w, h) }, // Brightness { label: '-0.2', category: 'Brightness', run: (b, w, h) => attackBrightness(b, -0.2, w, h) }, { label: '+0.2', category: 'Brightness', run: (b, w, h) => attackBrightness(b, 0.2, w, h) }, { label: '+0.4', category: 'Brightness', run: (b, w, h) => attackBrightness(b, 0.4, w, h) }, // Contrast { label: '0.5x', category: 'Contrast', run: (b, w, h) => attackContrast(b, 0.5, w, h) }, { label: '1.5x', category: 'Contrast', run: (b, w, h) => attackContrast(b, 1.5, w, h) }, { label: '2.0x', category: 'Contrast', run: (b, w, h) => attackContrast(b, 2.0, w, h) }, // Saturation { label: '0x', category: 'Saturation', run: (b, w, h) => attackSaturation(b, 0, w, h) }, { label: '0.5x', category: 'Saturation', run: (b, w, h) => attackSaturation(b, 0.5, w, h) }, { label: '2.0x', category: 'Saturation', run: (b, w, h) => attackSaturation(b, 2.0, w, h) }, // Crop (~5-10% from edges) { label: '5%', category: 'Crop', run: (b, w, h) => { const px = Math.round(Math.min(w, h) * 0.05); return attackCrop(b, px, px, px, px, w, h); }, detectOptions: { cropResilient: true } }, { label: '10%', category: 'Crop', run: (b, w, h) => { const px = Math.round(Math.min(w, h) * 0.10); return attackCrop(b, px, px, px, px, w, h); }, detectOptions: { cropResilient: true } }, { label: '15%', category: 'Crop', run: (b, w, h) => { const px = Math.round(Math.min(w, h) * 0.15); return attackCrop(b, px, px, px, px, w, h); }, detectOptions: { cropResilient: true } }, { label: '20%', category: 'Crop', run: (b, w, h) => { const px = Math.round(Math.min(w, h) * 0.20); return attackCrop(b, px, px, px, px, w, h); }, detectOptions: { cropResilient: true } }, ]; function payloadToHex(payload: Uint8Array): string { return Array.from(payload).map((b) => b.toString(16).padStart(2, '0')).join('').toUpperCase(); } export default function RobustnessTest({ blob, width, height, payload, secretKey }: RobustnessTestProps) { const [results, setResults] = useState>({}); const [runningAll, setRunningAll] = useState(false); const [progress, setProgress] = useState(0); const expectedHex = payload.replace(/^0x/, '').toUpperCase(); const runTest = useCallback(async (idx: number) => { setResults((prev) => ({ ...prev, [idx]: { status: 'running' } })); try { const test = TESTS[idx]; const attacked = await test.run(blob, width, height); // Pipeline already caps at 30 frames; use up to 10 evenly spaced const step = Math.max(1, Math.floor(attacked.yPlanes.length / 10)); const sampled = attacked.yPlanes.filter((_, i) => i % step === 0).slice(0, 10); const detection = autoDetectMultiFrame(sampled, attacked.width, attacked.height, secretKey, test.detectOptions); const detectedHex = detection.payload ? payloadToHex(detection.payload) : ''; const match = detection.detected && detectedHex === expectedHex; setResults((prev) => ({ ...prev, [idx]: { status: match ? 'pass' : 'fail', confidence: detection.confidence, payloadMatch: match, }, })); } catch (e) { console.error(`Robustness test ${idx} error:`, e); setResults((prev) => ({ ...prev, [idx]: { status: 'error' } })); } }, [blob, width, height, secretKey, expectedHex]); const runAll = useCallback(async () => { setRunningAll(true); setProgress(0); for (let i = 0; i < TESTS.length; i++) { await runTest(i); setProgress((i + 1) / TESTS.length); } setRunningAll(false); }, [runTest]); const categories = ['Re-encode', 'Downscale', 'Brightness', 'Contrast', 'Saturation', 'Crop']; return (

Robustness Testing

{runningAll && (
)}
{categories.map((cat) => { const catTests = TESTS.map((t, i) => ({ ...t, idx: i })).filter((t) => t.category === cat); return (
{cat}
{catTests.map(({ label, idx }) => { const r = results[idx]; const status = r?.status ?? 'idle'; return ( ); })}
); })}
); }