function DenoisedPanel({ denoisedImage, characterData, style }){ const imgRef = React.useRef(null); const canvasRef = React.useRef(null); const drawBoxes = () => { const img = imgRef.current; const canvas = canvasRef.current; if (!img || !canvas) return; const pad = 8; const cW = img.offsetWidth; const cH = img.offsetHeight; canvas.width = cW; canvas.height = cH; const ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, cW, cH); if (!characterData || characterData.length === 0) return; const nW = img.naturalWidth; const nH = img.naturalHeight; if (!nW || !nH) return; const availW = cW - pad * 2; const availH = cH - pad * 2; const scale = Math.min(availW / nW, availH / nH); const rendW = nW * scale; const rendH = nH * scale; const offX = pad + (availW - rendW) / 2; const offY = pad + (availH - rendH) / 2; ctx.strokeStyle = "rgba(0,200,160,0.85)"; ctx.lineWidth = 1.5; for (const item of characterData){ const [x, y, w, h] = item.bbox; ctx.strokeRect(offX + x * scale, offY + y * scale, w * scale, h * scale); } }; React.useEffect(() => { const img = imgRef.current; if (!img) return; if (img.complete && img.naturalWidth) drawBoxes(); else img.onload = drawBoxes; }, [denoisedImage, characterData]); return (
step 1 · ocr output
denoised
); } async function sha256short(text) { const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)); const hex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2,"0")).join(""); return hex.slice(0,8) + "…" + hex.slice(-4); } function DecompressPanel({ original, recovered }) { const [origHash, setOrigHash] = React.useState("computing…"); const [recHash, setRecHash] = React.useState("computing…"); const match = original === recovered; React.useEffect(() => { if (!original) return; sha256short(original).then(setOrigHash); sha256short(recovered || "").then(setRecHash); }, [original, recovered]); const panelStyle = { flex: 1, background: "var(--bg)", border: "1px solid var(--rule)", borderRadius: 6, padding: "16px 18px", display: "flex", flexDirection: "column", gap: 10, }; const titleStyle = { fontFamily: "var(--mono)", fontSize: 10, textTransform: "uppercase", letterSpacing: "0.12em", color: "rgba(0,180,140,0.8)", marginBottom: 4, }; const textStyle = { fontFamily: "var(--mono)", fontSize: 12, color: "var(--ink)", lineHeight: 1.7, flex: 1, wordBreak: "break-word", maxHeight: 140, overflowY: "auto", }; const hashStyle = { fontFamily: "var(--mono)", fontSize: 10, color: "var(--ink-3)", borderTop: "1px solid var(--rule)", paddingTop: 8, marginTop: "auto", }; return (
{match ? "✓" : "✗"} {match ? "100% bit-perfect match" : "mismatch detected"}
OCR Source Text
{original}
sha-256 · {origHash}
Decompressed Output
{recovered}
sha-256 · {recHash}
); } function HuffmanStepsPanel({ recognized, finalTree, finalCodeMap }) { const [steps, setSteps] = React.useState(null); const [loading, setLoading] = React.useState(false); const [stepIdx, setStepIdx] = React.useState(null); const [mode, setMode] = React.useState("final"); const [playing, setPlaying] = React.useState(false); React.useEffect(() => { if (!playing || !steps) return; const id = setInterval(() => { setStepIdx(i => { if (i >= steps.length - 1) { setPlaying(false); return i; } return i + 1; }); }, 600); return () => clearInterval(id); }, [playing, steps]); const loadSteps = async () => { if (steps) { setMode("steps"); setStepIdx(0); return; } setLoading(true); try { const r = await fetch("/api/compress/steps", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: recognized }) }); if (!r.ok) throw new Error(`steps failed: ${r.status}`); const data = await r.json(); setSteps(data.steps); setStepIdx(0); setMode("steps"); } catch(e) { } finally { setLoading(false); } }; const currentStep = mode === "steps" && steps ? steps[stepIdx] : null; const treeData = currentStep ? currentStep.tree : (finalTree || null); const codeMap = currentStep ? currentStep.codes : (finalCodeMap || {}); return (
Huffman tree · input: "{recognized}"
{mode === "steps" && steps && (
{ setPlaying(false); setStepIdx(Number(e.target.value)); }} style={{ flex: 1, accentColor: "rgba(0,180,140,1)" }} /> step {stepIdx + 1} / {steps.length}
)} {mode === "steps" && steps && currentStep && (
{currentStep.is_new ? "new symbol" : "seen before"} '{currentStep.char}' → code: {codeMap[currentStep.char] || "—"} {currentStep.swaps && currentStep.swaps.length > 0 && ( ↔ swapped during this step: {currentStep.swaps.map(([a,b]) => `#${a} ↔ #${b}`).join(", ")} )}
)}
); } const STAGE_DEFS = [ { n: "1", id: "ocr", name: "OCR", desc: "CNN → text", endpoint: "$STAGE1_URL/predict" }, { n: "2", id: "encode", name: "Compress", desc: "Adaptive Huffman", endpoint: "$STAGE2_URL/encode" }, { n: "3", id: "decode", name: "Decompress", desc: "Lossless verify", endpoint: "$STAGE2_URL/decode" }, ]; function fmtMs(seconds){ if (seconds == null) return "—"; return `${(seconds*1000).toFixed(1)} ms`; } function StepperView({ stage, progress, result, sample, onViewResults }){ const [expandedOcr, setExpandedOcr] = React.useState(false); const [expandedCompress, setExpandedCompress] = React.useState(false); const ocrClickable = result && result.denoisedImage; const compressClickable = result && result.recognized; const decompressClickable = result && result.recovered; return (
{STAGE_DEFS.map((s, i) => { const active = stage === i + 1; const done = stage > i + 1 || (stage === 4); const p = progress[i] || 0; const stateLabel = done ? "COMPLETE" : active ? "RUNNING" : "IDLE"; const latencyKey = ["ocrLatency","compLatency","decLatency"][i]; const isOcr = i === 0; const isCompress = i === 1; const isDecompress = i === 2; const highlighted = (isOcr && expandedOcr) || (isCompress && expandedCompress); const clickable = (isOcr && ocrClickable) || (isCompress && compressClickable) || (isDecompress && decompressClickable); const onClick = isOcr && ocrClickable ? () => { setExpandedOcr(v => !v); setExpandedCompress(false); } : isCompress && compressClickable ? () => { setExpandedCompress(v => !v); setExpandedOcr(false); } : isDecompress && decompressClickable ? () => onViewResults && onViewResults() : undefined; const expanded = isOcr ? expandedOcr : isCompress ? expandedCompress : false; return (
STEP {s.n}{clickable ? {expanded ? "▲ hide" : "▼ view output"} : null}
{s.name}
{s.desc}
{stateLabel} {result ? fmtMs(result[latencyKey]) : "— ms"}
); })}
{expandedOcr && result && result.denoisedImage && (
step 0 · raw
{sample && sample.src ? raw : sample && sample.imageBase64 ? raw : null }
Extracted text {result.recognized}
)} {expandedCompress && result && result.recognized && ( )}
); } function NetworkView({ stage, progress, result }){ const positions = [ { id: "in", x: 10, y: 50, label: "INPUT", sub: "image/png", kind: "io" }, { id: "ocr", x: 30, y: 50, label: "STAGE 01", sub: "CNN · OCR", stageIdx: 1 }, { id: "enc", x: 52, y: 30, label: "STAGE 02A", sub: "Huffman · encode", stageIdx: 2 }, { id: "dec", x: 74, y: 70, label: "STAGE 02B", sub: "Huffman · decode", stageIdx: 3 }, { id: "out", x: 93, y: 50, label: "VERIFY", sub: "lossless", kind: "io" }, ]; const edges = [ ["in","ocr"],["ocr","enc"],["enc","dec"],["dec","out"] ]; return (
{edges.map(([a,b], i) => { const A = positions.find(p=>p.id===a); const B = positions.find(p=>p.id===b); const edgeActive = (b === "ocr" && stage >= 1) || (b === "enc" && stage >= 2) || (b === "dec" && stage >= 3) || (b === "out" && stage >= 4); const running = (b === "ocr" && stage === 1) || (b === "enc" && stage === 2) || (b === "dec" && stage === 3); return ( {running && ( )} ); })} {positions.map(p => { const done = p.stageIdx ? stage > p.stageIdx || stage === 4 : (p.id === "in" ? stage >= 1 : stage === 4); const active = p.stageIdx && stage === p.stageIdx; return (
{p.label}
{p.sub}
{active ? "running…" : done ? (p.stageIdx && result ? fmtMs(result[["ocrLatency","compLatency","decLatency"][p.stageIdx-1]]) : "ok") : "idle"}
); })}
); } function TimelineView({ stage, progress, result, sample }){ const rows = [ { name: "Stage 01 · OCR", desc: "Forward pass through CNN; returns recognized text.", latency: result?.ocrLatency, payload: stage >= 2 && result ? `recognized = "${result.recognized}" · conf=0.992` : null, stageIdx: 1, }, { name: "Stage 02 · Adaptive Huffman Encode", desc: "Symbol-adaptive coding; emits base64 payload + metrics.", latency: result?.compLatency, payload: stage >= 3 && result ? `${result.payload.slice(0, 52)}${result.payload.length > 52 ? "…" : ""}` : null, stageIdx: 2, }, { name: "Stage 02 · Decode", desc: "Reconstructs original string from payload; asserts equality.", latency: result?.decLatency, payload: stage === 4 && result ? `recovered = "${result.recovered}" · match=true` : null, stageIdx: 3, }, ]; return (
{rows.map((r, i) => { const active = stage === r.stageIdx; const done = stage > r.stageIdx || stage === 4; return (
{r.name} {r.latency != null ? fmtMs(r.latency) : active ? "running…" : "—"}
{r.desc}
{r.payload &&
{r.payload}
}
); })}
); } function PipelineView({ variant, sample, onViewResults, ...rest }){ if (variant === "network") return ; if (variant === "timeline") return ; return ; } Object.assign(window, { PipelineView, STAGE_DEFS, fmtMs });