coin.spraxxx.com

Published 2026-04-16T19:32:44Z UTC by Jacques / SPRAXXX

<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Coin Desk — SPRAXXX</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="description" content="SPRAXXX Coin Desk: raw camera tool for coin capture, observation, OCR, and structured export."> <style> :root{ --bg:#0f1115; --panel:#151922; --line:#2d3440; --line2:#3b4352; --text:#e8e8e8; --muted:#a9b0bb; --ok:#9fe6a0; --warn:#ffb36b; --accent:#9cc9ff; } *{box-sizing:border-box} body{ margin:0; background:var(--bg); color:var(--text); font:16px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif; } main{ max-width:1200px; margin:0 auto; padding:20px; display:grid; gap:18px; } .panel{ background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:16px; } .row{ display:flex; flex-wrap:wrap; gap:10px; } .grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:18px; } @media (max-width:980px){ .grid{grid-template-columns:1fr} } video, canvas, textarea, input, select{ width:100%; border-radius:10px; } video, canvas{ background:#000; display:block; min-height:220px; } textarea, input, select{ background:#0d1117; color:var(--text); border:1px solid var(--line); padding:10px 12px; } textarea{ min-height:140px; resize:vertical; } button{ background:#1c2230; color:var(--text); border:1px solid var(--line2); border-radius:10px; padding:10px 12px; cursor:pointer; } button:hover{border-color:var(--accent)} h1,h2,h3{margin:0 0 10px} .muted{color:var(--muted)} .status{ font-family:ui-monospace,SFMono-Regular,Menlo,monospace; color:var(--ok); } .warn{color:var(--warn)} .fields{ display:grid; grid-template-columns:repeat(2,1fr); gap:12px; } @media (max-width:780px){ .fields{grid-template-columns:1fr} } label{ display:block; font-size:.92rem; color:var(--muted); margin:0 0 6px; } .small{ font-size:.9rem; color:var(--muted); } </style> </head> <body> <main> <div class="panel"> <h1>COIN DESK</h1> <p class="muted">Raw browser tool. Camera first. Observation first. No fake grading.</p> <div class="row"> <button id="startCam">Start Camera</button> <button id="stopCam">Stop Camera</button> <button id="captureObverse">Capture Obverse</button> <button id="captureReverse">Capture Reverse</button> <button id="captureEdge">Capture Edge</button> <button id="runOCR">Run OCR</button> <button id="appendNotes">Append OCR to Notes</button> <button id="downloadTxt">Download TXT</button> <button id="downloadJson">Download JSON</button> </div> <p class="status" id="status">idle</p> <p class="small">Observed fact goes in fields. Guesses stay guesses.</p> </div>

<div class="panel"> <h2>Live Camera</h2> <video id="video" autoplay playsinline muted></video> </div>

<div class="grid"> <div class="panel"> <h3>Obverse</h3> <canvas id="obverseCanvas"></canvas> </div> <div class="panel"> <h3>Reverse</h3> <canvas id="reverseCanvas"></canvas> </div> <div class="panel"> <h3>Edge</h3> <canvas id="edgeCanvas"></canvas> </div> </div>

<div class="panel"> <h2>Observed Fields</h2> <div class="fields"> <div> <label for="owner">Owner</label> <input id="owner" placeholder="Photios"> </div> <div> <label for="coinLabel">Coin Label</label> <input id="coinLabel" placeholder="1947 Canadian penny"> </div> <div> <label for="country">Country</label> <input id="country" placeholder="Canada"> </div> <div> <label for="denomination">Denomination</label> <input id="denomination" placeholder="1 cent"> </div> <div> <label for="year">Year</label> <input id="year" placeholder="1947"> </div> <div> <label for="mintMark">Mint Mark</label> <input id="mintMark" placeholder="dot / maple leaf / none / unknown"> </div> <div> <label for="metalGuess">Metal Guess</label> <input id="metalGuess" placeholder="bronze / copper / nickel / unknown"> </div> <div> <label for="shape">Shape</label> <input id="shape" placeholder="round"> </div> <div> <label for="edgeType">Edge Type</label> <input id="edgeType" placeholder="smooth / reeded / lettered / unknown"> </div> <div> <label for="conditionGuess">Condition Guess</label> <input id="conditionGuess" placeholder="circulated / fine / very fine / unknown"> </div> <div style="grid-column:1/-1;"> <label for="possibleErrors">Possible Errors / Anomalies</label> <input id="possibleErrors" placeholder="double strike / clipped planchet / die crack / none seen / unknown"> </div> </div> </div>

<div class="grid"> <div class="panel"> <h3>OCR Output</h3> <textarea id="ocrText" placeholder="recognized lettering and numbers appear here"></textarea> </div> <div class="panel"> <h3>Notes</h3> <textarea id="notes" placeholder="manual observations, context, comparison notes"></textarea> </div> </div> </main>

<script type="module"> import { createWorker } from "https://cdn.jsdelivr.net/npm/tesseract.js@6/dist/tesseract.esm.min.js";

const $ = (id) => document.getElementById(id); const video = $("video"); const statusEl = $("status");

const canvases = { obverse: $("obverseCanvas"), reverse: $("reverseCanvas"), edge: $("edgeCanvas") };

let stream = null; let lastCaptured = "obverse";

function setStatus(msg, warn=false){ statusEl.textContent = msg; statusEl.className = warn ? "status warn" : "status"; }

async function startCamera(){ try{ stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: "environment" }, width: { ideal: 1920 }, height: { ideal: 1080 } }, audio: false }); video.srcObject = stream; setStatus("camera live"); }catch(err){ setStatus("camera error: " + err.message, true); } }

function stopCamera(){ if(stream){ stream.getTracks().forEach(t => t.stop()); stream = null; } video.srcObject = null; setStatus("camera stopped"); }

function captureTo(name){ if(!video.videoWidth){ setStatus("camera not ready", true); return; } const canvas = canvases[name]; const ctx = canvas.getContext("2d"); canvas.width = video.videoWidth; canvas.height = video.videoHeight; ctx.drawImage(video, 0, 0, canvas.width, canvas.height); lastCaptured = name; setStatus(name + " captured"); }

async function runOCR(){ const canvas = canvases[lastCaptured]; if(!canvas.width){ setStatus("capture a side first", true); return; } setStatus("ocr running on " + lastCaptured + "..."); const worker = await createWorker("eng"); const dataUrl = canvas.toDataURL("image/jpeg", 0.92); const result = await worker.recognize(dataUrl); $("ocrText").value = result.data.text || ""; await worker.terminate(); setStatus("ocr complete"); }

function appendOCR(){ const text = $("ocrText").value.trim(); if(!text){ setStatus("no ocr text to append", true); return; } const notes = $("notes"); notes.value += (notes.value ? "\n\n" : "") + "[OCR " + lastCaptured + "]\n" + text; setStatus("ocr appended to notes"); }

function collectSession(){ return { created_at: new Date().toISOString(), owner: $("owner").value.trim(), coin_label: $("coinLabel").value.trim(), country: $("country").value.trim(), denomination: $("denomination").value.trim(), year: $("year").value.trim(), mint_mark: $("mintMark").value.trim(), metal_guess: $("metalGuess").value.trim(), shape: $("shape").value.trim(), edge_type: $("edgeType").value.trim(), condition_guess: $("conditionGuess").value.trim(), possible_errors: $("possibleErrors").value.trim(), ocr_output: $("ocrText").value, notes: $("notes").value, capture_state: { obverse: !!canvases.obverse.width, reverse: !!canvases.reverse.width, edge: !!canvases.edge.width } }; }

function download(filename, content, type){ const blob = new Blob([content], { type }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 1000); }

function downloadTxt(){ const s = collectSession(); const txt = [ "COIN DESK SESSION", "created_at: " + s.created_at, "owner: " + s.owner, "coin_label: " + s.coin_label, "country: " + s.country, "denomination: " + s.denomination, "year: " + s.year, "mint_mark: " + s.mint_mark, "metal_guess: " + s.metal_guess, "shape: " + s.shape, "edge_type: " + s.edge_type, "condition_guess: " + s.condition_guess, "possible_errors: " + s.possible_errors, "", "OCR OUTPUT", s.ocr_output || "", "", "NOTES", s.notes || "" ].join("\n"); download("coin-session.txt", txt, "text/plain"); }

function downloadJson(){ const s = collectSession(); download("coin-session.json", JSON.stringify(s, null, 2), "application/json"); }

$("startCam").onclick = startCamera; $("stopCam").onclick = stopCamera; $("captureObverse").onclick = () => captureTo("obverse"); $("captureReverse").onclick = () => captureTo("reverse"); $("captureEdge").onclick = () => captureTo("edge"); $("runOCR").onclick = runOCR; $("appendNotes").onclick = appendOCR; $("downloadTxt").onclick = downloadTxt; $("downloadJson").onclick = downloadJson; </script> </body> </html>

Back to journal