Spaces:
Running
Running
| import { useState, useRef, useCallback, useEffect } from 'react'; | |
| import type { PresetName } from '@core/types.js'; | |
| import { getPreset, PRESET_DESCRIPTIONS } from '@core/presets.js'; | |
| import { streamExtractAndEmbed } from '../lib/video-io.js'; | |
| import StrengthSlider from './StrengthSlider.js'; | |
| import ComparisonView from './ComparisonView.js'; | |
| import RobustnessTest from './RobustnessTest.js'; | |
| const PLEASE_HOLD = [ | |
| { art: ' (β’_β’)\n ( β’_β’)>ββ -β \n (ββ _β )', caption: 'Putting on invisibility shades...' }, | |
| { art: ' β(βΔΉΜ―β)β\n β(βΔΉΜ―β)β\n β(βΔΉΜ―β)β', caption: 'Robot dance while we wait...' }, | |
| { art: ' ββββ\n ββββ β\n ββββ', caption: 'Brewing the perfect watermark...' }, | |
| { art: ' π¬ β π¬ β π', caption: 'Turning pixels into secrets...' }, | |
| { art: ' [ββββββββββ]\n [ββββββββββ]\n [ββββββββββ]', caption: 'Hiding bits in plain sight...' }, | |
| { art: ' /\\_/\\\n ( o.o )\n > ^ <', caption: 'Even the cat can\'t see the watermark...' }, | |
| { art: ' βββββββββββ\n β 01101001 β\n βββββββββββ', caption: 'Whispering bits into wavelets...' }, | |
| { art: ' ~~ π ~~\n ~π~ ~~ \n ~~~~~~~~', caption: 'Surfing the frequency domain...' }, | |
| { art: ' πΌ β 𧬠β πΌ', caption: 'Splicing invisible DNA into frames...' }, | |
| { art: ' Β―\\_(γ)_/Β―', caption: 'Trust us, the watermark is there...' }, | |
| ]; | |
| /** Max frames to keep in memory for the comparison view */ | |
| const COMPARISON_SAMPLE = 30; | |
| export default function EmbedPanel() { | |
| const [videoUrl, setVideoUrl] = useState<string | null>(null); | |
| const [videoName, setVideoName] = useState(''); | |
| const [key, setKey] = useState(''); | |
| const [payload, setPayload] = useState('DEADBEEF'); | |
| const [preset, setPreset] = useState<PresetName>('light'); | |
| const [alpha, setAlpha] = useState(0.00); | |
| const [processing, setProcessing] = useState(false); | |
| const [progress, setProgress] = useState({ phase: '', current: 0, total: 0 }); | |
| const [jokeIndex, setJokeIndex] = useState(0); | |
| const [result, setResult] = useState<{ | |
| blob: Blob; | |
| psnr: number; | |
| frames: number; | |
| originalFrames: ImageData[]; | |
| watermarkedFrames: ImageData[]; | |
| width: number; | |
| height: number; | |
| fps: number; | |
| embedTimeMs: number; | |
| pixelsPerSecond: number; | |
| } | null>(null); | |
| const fileRef = useRef<HTMLInputElement>(null); | |
| // Rotate jokes every 4s while processing | |
| useEffect(() => { | |
| if (!processing) return; | |
| setJokeIndex(Math.floor(Math.random() * PLEASE_HOLD.length)); | |
| const id = setInterval(() => { | |
| setJokeIndex((i) => (i + 1) % PLEASE_HOLD.length); | |
| }, 4000); | |
| return () => clearInterval(id); | |
| }, [processing]); | |
| const handleFile = useCallback((file: File) => { | |
| const url = URL.createObjectURL(file); | |
| setVideoUrl(url); | |
| setVideoName(file.name); | |
| setResult(null); | |
| }, [result]); | |
| const handleDrop = useCallback( | |
| (e: React.DragEvent) => { | |
| e.preventDefault(); | |
| const file = e.dataTransfer.files[0]; | |
| if (file?.type.startsWith('video/')) handleFile(file); | |
| }, | |
| [handleFile] | |
| ); | |
| const handleEmbed = async () => { | |
| if (!videoUrl || !key) return; | |
| setProcessing(true); | |
| setResult(null); | |
| try { | |
| // Parse payload | |
| const payloadHex = payload.replace(/^0x/, ''); | |
| const payloadBytes = new Uint8Array( | |
| (payloadHex.length % 2 ? '0' + payloadHex : payloadHex) | |
| .match(/.{2}/g)! | |
| .map((b) => parseInt(b, 16)) | |
| ); | |
| const config = getPreset(preset); | |
| // Stream: extract -> watermark -> encode in chunks | |
| const embedResult = await streamExtractAndEmbed( | |
| videoUrl, | |
| payloadBytes, | |
| key, | |
| config, | |
| COMPARISON_SAMPLE, | |
| (phase, current, total) => setProgress({ phase, current, total }) | |
| ); | |
| setResult({ | |
| blob: embedResult.blob, | |
| psnr: embedResult.avgPsnr, | |
| frames: embedResult.totalFrames, | |
| originalFrames: embedResult.sampleOriginal, | |
| watermarkedFrames: embedResult.sampleWatermarked, | |
| width: embedResult.width, | |
| height: embedResult.height, | |
| fps: embedResult.fps, | |
| embedTimeMs: embedResult.embedTimeMs, | |
| pixelsPerSecond: embedResult.pixelsPerSecond, | |
| }); | |
| } catch (e) { | |
| console.error('Embed error:', e); | |
| alert(`Error: ${e}`); | |
| } finally { | |
| setProcessing(false); | |
| } | |
| }; | |
| const handleDownload = () => { | |
| if (!result) return; | |
| const url = URL.createObjectURL(result.blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = videoName.replace(/\.[^.]+$/, '') + '_watermarked.mp4'; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| }; | |
| const maxPayloadHexChars = 8; // Always 32 bits = 8 hex chars | |
| return ( | |
| <div className="space-y-8"> | |
| {/* Upload area */} | |
| <div | |
| onDrop={handleDrop} | |
| onDragOver={(e) => e.preventDefault()} | |
| onClick={() => fileRef.current?.click()} | |
| className={`group cursor-pointer rounded-2xl border-2 border-dashed p-10 text-center transition-all duration-200 | |
| ${videoUrl | |
| ? 'border-zinc-700 bg-zinc-900/30' | |
| : 'border-zinc-800 bg-zinc-900/20 hover:border-blue-600/40 hover:bg-zinc-900/40 hover:shadow-lg hover:shadow-blue-600/5' | |
| }`} | |
| > | |
| <input | |
| ref={fileRef} | |
| type="file" | |
| accept="video/*" | |
| className="hidden" | |
| onChange={(e) => { | |
| const file = e.target.files?.[0]; | |
| if (file) handleFile(file); | |
| }} | |
| /> | |
| {videoUrl ? ( | |
| <div className="space-y-2"> | |
| <p className="text-sm font-medium text-zinc-300">π¬ {videoName}</p> | |
| <p className="text-xs text-zinc-500">Click or drop to replace</p> | |
| </div> | |
| ) : ( | |
| <div className="space-y-3"> | |
| <div className="mx-auto flex h-12 w-12 items-center justify-center rounded-xl bg-zinc-800/80 transition-colors group-hover:bg-blue-600/10"> | |
| <svg className="h-6 w-6 text-zinc-500 transition-colors group-hover:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> | |
| <path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" /> | |
| </svg> | |
| </div> | |
| <p className="text-sm text-zinc-400">Drop a video file or click to browse</p> | |
| <p className="text-xs text-zinc-600">MP4, WebM, MOV supported</p> | |
| </div> | |
| )} | |
| </div> | |
| {/* Configuration */} | |
| <div className="grid gap-6 sm:grid-cols-2"> | |
| <div className="space-y-1.5"> | |
| <label className="text-sm font-medium text-zinc-300">π Secret Key</label> | |
| <input | |
| type="text" | |
| value={key} | |
| onChange={(e) => setKey(e.target.value)} | |
| placeholder="Enter a secret key..." | |
| className="w-full rounded-lg border border-zinc-800 bg-zinc-900/50 px-3 py-2 text-sm text-zinc-100 | |
| placeholder:text-zinc-600 focus:border-blue-600 focus:outline-none focus:ring-1 focus:ring-blue-600 | |
| transition-colors" | |
| /> | |
| </div> | |
| <div className="space-y-1.5"> | |
| <label className="text-sm font-medium text-zinc-300">π¦ Payload (hex)</label> | |
| <input | |
| type="text" | |
| value={payload} | |
| onChange={(e) => setPayload(e.target.value.replace(/[^0-9a-fA-F]/g, '').slice(0, maxPayloadHexChars))} | |
| placeholder="DEADBEEF" | |
| className="w-full rounded-lg border border-zinc-800 bg-zinc-900/50 px-3 py-2 font-mono text-sm text-zinc-100 | |
| placeholder:text-zinc-600 focus:border-blue-600 focus:outline-none focus:ring-1 focus:ring-blue-600 | |
| transition-colors" | |
| /> | |
| <p className="text-[10px] text-zinc-600">32-bit payload, 4 bytes hex</p> | |
| </div> | |
| </div> | |
| <StrengthSlider | |
| value={alpha} | |
| onChange={(v, p) => { | |
| setAlpha(v); | |
| setPreset(p); | |
| }} | |
| disabled={processing} | |
| /> | |
| <p className="text-xs text-zinc-500"> | |
| {PRESET_DESCRIPTIONS[preset]} | |
| </p> | |
| {/* Embed button */} | |
| <button | |
| onClick={handleEmbed} | |
| disabled={!videoUrl || !key || processing} | |
| className="w-full rounded-xl bg-gradient-to-r from-blue-600 to-blue-500 px-4 py-3 text-sm font-semibold text-white | |
| shadow-lg shadow-blue-600/20 transition-all hover:from-blue-500 hover:to-blue-400 hover:shadow-blue-500/30 | |
| disabled:cursor-not-allowed disabled:from-zinc-800 disabled:to-zinc-800 disabled:text-zinc-500 disabled:shadow-none" | |
| > | |
| {processing ? ( | |
| <span className="flex items-center justify-center gap-2"> | |
| <span className="h-4 w-4 animate-spin rounded-full border-2 border-zinc-400 border-t-white" /> | |
| {progress.phase} {progress.total > 0 ? `${progress.current}/${progress.total}` : ''} | |
| </span> | |
| ) : ( | |
| 'β¨ Embed Watermark' | |
| )} | |
| </button> | |
| {/* Please-hold jokes while processing */} | |
| {processing && ( | |
| <div className="flex flex-col items-center gap-2 rounded-xl bg-zinc-900/50 py-6 text-center"> | |
| <pre className="font-mono text-sm leading-tight text-zinc-400"> | |
| {PLEASE_HOLD[jokeIndex].art} | |
| </pre> | |
| <p className="text-xs text-zinc-500">{PLEASE_HOLD[jokeIndex].caption}</p> | |
| </div> | |
| )} | |
| {/* Results */} | |
| {result && ( | |
| <div className="space-y-6 rounded-2xl border border-emerald-800/30 bg-emerald-950/10 p-6"> | |
| <div className="flex items-center justify-between"> | |
| <div> | |
| <h3 className="text-sm font-semibold text-emerald-400">β Embedding Complete</h3> | |
| <p className="mt-1 text-xs text-zinc-500"> | |
| {result.frames} frames processed β Average PSNR: {result.psnr.toFixed(1)} dB | |
| <br /> | |
| Embedded in {(result.embedTimeMs / 1000).toFixed(1)}s β {(result.pixelsPerSecond / 1e6).toFixed(1)} Mpx/s | |
| </p> | |
| </div> | |
| <button | |
| onClick={handleDownload} | |
| className="rounded-lg bg-emerald-600/20 px-4 py-2 text-sm font-medium text-emerald-400 | |
| ring-1 ring-emerald-600/30 transition-all hover:bg-emerald-600/30 hover:ring-emerald-500/40" | |
| > | |
| β¬οΈ Download | |
| </button> | |
| </div> | |
| <ComparisonView | |
| originalFrames={result.originalFrames} | |
| watermarkedFrames={result.watermarkedFrames} | |
| width={result.width} | |
| height={result.height} | |
| fps={result.fps} | |
| /> | |
| <RobustnessTest | |
| blob={result.blob} | |
| width={result.width} | |
| height={result.height} | |
| payload={payload} | |
| secretKey={key} | |
| /> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |