File size: 6,291 Bytes
c0e8fe8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OCR Reader</title>
<style>
  :root { --bg1:#eef2ff; --bg2:#fdf2f8; --card:#fff; --text:#1e1b4b; --muted:#6b7280; --border:#e5e7eb; --accent:#4f46e5; --accent2:#7c3aed; }
  @media (prefers-color-scheme: dark) {
    :root { --bg1:#0f172a; --bg2:#1e1b4b; --card:#1e293b; --text:#f1f5f9; --muted:#94a3b8; --border:#334155; --accent:#818cf8; --accent2:#a78bfa; }
  }
  * { box-sizing: border-box; }
  body { margin:0; min-height:100vh; padding:20px; display:flex; align-items:center; justify-content:center;
         font-family:"Segoe UI",system-ui,Arial,sans-serif; color:var(--text);
         background:linear-gradient(135deg,var(--bg1),var(--bg2)); }
  .card { width:100%; max-width:640px; background:var(--card); padding:34px; border-radius:20px; box-shadow:0 20px 50px rgba(79,70,229,.18); }
  h1 { margin:0 0 4px; font-size:32px; background:linear-gradient(90deg,var(--accent),var(--accent2)); -webkit-background-clip:text; background-clip:text; color:transparent; }
  .sub { margin:0 0 6px; color:var(--muted); }
  .tip { margin:0 0 20px; font-size:13px; color:var(--muted); }
  #drop { border:2px dashed var(--border); border-radius:14px; padding:18px; text-align:center; cursor:pointer; transition:border-color .2s; }
  #drop:hover, #drop.over { border-color:var(--accent); }
  #preview { display:none; max-width:100%; max-height:220px; margin:0 auto 10px; border-radius:8px; }
  #dropText { color:var(--muted); }
  button { font-family:inherit; cursor:pointer; border:none; border-radius:12px; }
  #btn { width:100%; margin-top:14px; padding:14px; font-size:17px; font-weight:600; color:#fff; background:linear-gradient(90deg,var(--accent),var(--accent2)); }
  #btn:disabled { opacity:.5; cursor:not-allowed; }
  #status { margin-top:16px; font-size:14px; color:var(--muted); }
  .track { height:8px; margin-top:6px; background:var(--border); border-radius:99px; overflow:hidden; }
  #loadbar { height:100%; width:0%; background:linear-gradient(90deg,var(--accent),var(--accent2)); transition:width .3s; }
  #out { display:none; margin-top:20px; }
  #text { padding:16px; border:2px solid var(--border); border-radius:12px; font-size:20px; font-weight:600; word-break:break-word; min-height:30px; }
  #copy { margin-top:10px; padding:8px 16px; font-size:14px; color:var(--text); background:transparent; border:1px solid var(--border); }
  #copy:hover { border-color:var(--accent); color:var(--accent); }
  #err { margin-top:12px; color:#dc2626; font-size:14px; }
  footer { margin-top:22px; text-align:center; font-size:12px; color:var(--muted); }
</style>
</head>
<body>
<div class="card">
  <h1>OCR Reader</h1>
  <p class="sub">Upload a picture of printed text and see the text the AI reads from it.</p>
  <p class="tip">Tip: this model reads one line of text at a time, so crop your image to a single line.</p>

  <div id="drop">
    <img id="preview" alt="Your image">
    <div id="dropText">Click here or drop an image</div>
    <input id="file" type="file" accept="image/*" hidden>
  </div>

  <button id="btn" disabled>Read text</button>

  <div id="status">Loading AI model, please wait...</div>
  <div class="track"><div id="loadbar"></div></div>
  <div id="err"></div>

  <div id="out">
    <div id="text"></div>
    <button id="copy">Copy text</button>
  </div>

  <footer>Runs privately in your browser with Transformers.js. Your image never leaves your device.</footer>
</div>

<script type="module">
  import { pipeline, env } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3";
  env.allowLocalModels = false;

  const $ = (id) => document.getElementById(id);
  const drop = $("drop"), file = $("file"), preview = $("preview"), btn = $("btn");
  const status = $("status"), loadbar = $("loadbar"), err = $("err"), out = $("out"), text = $("text");
  let imageData = null;

  function loadFile(f) {
    err.textContent = "";
    if (!f || !f.type.startsWith("image/")) { err.textContent = "Please choose an image file (JPG, PNG, etc.)."; return; }
    const reader = new FileReader();
    reader.onload = () => {
      imageData = reader.result;
      preview.src = imageData; preview.style.display = "block";
      $("dropText").textContent = "Click to choose a different image";
      out.style.display = "none";
      if (ocr) btn.disabled = false;
    };
    reader.readAsDataURL(f);
  }

  drop.addEventListener("click", () => file.click());
  file.addEventListener("change", () => loadFile(file.files[0]));
  drop.addEventListener("dragover", (e) => { e.preventDefault(); drop.classList.add("over"); });
  drop.addEventListener("dragleave", () => drop.classList.remove("over"));
  drop.addEventListener("drop", (e) => { e.preventDefault(); drop.classList.remove("over"); loadFile(e.dataTransfer.files[0]); });

  let ocr = null;
  try {
    ocr = await pipeline("image-to-text", "Xenova/trocr-small-printed", {
      progress_callback: (p) => {
        if (p.status === "progress" && typeof p.progress === "number") loadbar.style.width = p.progress.toFixed(0) + "%";
      },
    });
    loadbar.style.width = "100%";
    status.textContent = "✅ Model ready. Upload an image of one line of printed text.";
    if (imageData) btn.disabled = false;
  } catch (e) {
    status.textContent = "";
    err.textContent = "Could not load the AI model. Check your internet connection and refresh the page.";
  }

  btn.addEventListener("click", async () => {
    err.textContent = "";
    btn.disabled = true; btn.textContent = "Reading...";
    try {
      const result = await ocr(imageData);
      text.textContent = result[0].generated_text.trim() || "(no text found)";
      out.style.display = "block";
    } catch (e) {
      err.textContent = "Sorry, the app could not read this image. Try a clearer, cropped one.";
    }
    btn.disabled = false; btn.textContent = "Read text";
  });

  $("copy").addEventListener("click", async () => {
    try { await navigator.clipboard.writeText(text.textContent); $("copy").textContent = "Copied!"; }
    catch (e) { $("copy").textContent = "Select and copy manually"; }
    setTimeout(() => { $("copy").textContent = "Copy text"; }, 1500);
  });
</script>
</body>
</html>