File size: 20,072 Bytes
b9d34d8
 
 
 
 
 
ef3708d
 
 
 
 
 
 
23f65ce
ef3708d
 
23f65ce
ef3708d
 
 
4306e95
 
 
 
 
 
 
 
 
 
 
 
 
 
1f46341
 
 
 
 
 
 
 
 
 
 
 
 
4306e95
 
ef3708d
 
 
b9d34d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef3708d
 
 
 
 
 
 
 
 
 
 
b9d34d8
 
 
23f65ce
b9d34d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23f65ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9d34d8
23f65ce
 
 
 
 
 
 
 
 
 
b9d34d8
23f65ce
 
 
 
 
 
 
 
 
 
b9d34d8
23f65ce
 
 
 
 
 
 
 
 
 
b9d34d8
23f65ce
b9d34d8
23f65ce
 
 
 
 
 
4306e95
23f65ce
 
 
 
 
 
 
 
 
4306e95
b9d34d8
23f65ce
 
 
 
 
 
 
 
 
 
 
1f46341
23f65ce
 
 
 
 
 
 
 
 
 
 
 
b9d34d8
23f65ce
b9d34d8
 
 
23f65ce
b9d34d8
 
 
 
 
 
 
 
 
 
 
23f65ce
b9d34d8
 
 
 
 
 
 
 
 
 
23f65ce
b9d34d8
23f65ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9d34d8
 
23f65ce
 
 
 
 
b9d34d8
 
 
 
23f65ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9d34d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0992cee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9d34d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Local PDF Extractor</title>
  <script>
    /* Set the theme before first paint (no flash), then wire the header toggle.
       Self-contained so app.js is untouched. */
    (function () {
      var root = document.documentElement;
      try {
        var t = localStorage.getItem("theme");
        if (t !== "light" && t !== "dark") t = "light";
        root.setAttribute("data-theme", t);
      } catch (e) {
        root.setAttribute("data-theme", "light");
      }
      document.addEventListener("DOMContentLoaded", function () {
        var btn = document.getElementById("themeToggle");
        if (btn) {
          btn.addEventListener("click", function () {
            var next = root.getAttribute("data-theme") === "light" ? "dark" : "light";
            root.setAttribute("data-theme", next);
            try { localStorage.setItem("theme", next); } catch (e) {}
          });
        }

        /* Disable local-handwriting gracefully where its packages aren't
           installed (e.g. the lean hosted build) and point users to Gemini,
           instead of letting a page fail mid-extraction. */
        fetch("/api/capabilities")
          .then(function (r) { return r.json(); })
          .then(function (caps) {
            if (!caps) return;
            if (caps.handwriting === false) {
              var hw = document.getElementById("handwritingToggle");
              if (hw) { hw.checked = false; hw.disabled = true; }
              var row = document.getElementById("handwritingRow");
              if (row) row.classList.add("is-unavailable");
              var note = document.getElementById("hwUnavailable");
              if (note) note.hidden = false;
            }
            if (caps.online_demo === true) {
              var on = document.getElementById("onlineDemoNote");
              if (on) on.hidden = false;
            }
          })
          .catch(function () {});
      });
    })();
  </script>
  <link rel="stylesheet" href="/static/styles.css" />
</head>
<body>
  <header class="app-header">
    <div class="brand">
      <div class="brand-mark" aria-hidden="true">
        <svg viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="logo">
          <rect x="5" y="3" width="18" height="26" rx="3" fill="currentColor" opacity="0.15"></rect>
          <rect x="5" y="3" width="18" height="26" rx="3" fill="none" stroke="currentColor" stroke-width="2"></rect>
          <line x1="9" y1="11" x2="19" y2="11" stroke="currentColor" stroke-width="2" stroke-linecap="round"></line>
          <line x1="9" y1="16" x2="19" y2="16" stroke="currentColor" stroke-width="2" stroke-linecap="round"></line>
          <line x1="9" y1="21" x2="15" y2="21" stroke="currentColor" stroke-width="2" stroke-linecap="round"></line>
        </svg>
      </div>
      <div class="brand-text">
        <h1>Local PDF Extractor</h1>
        <p class="tagline">Extract text from typed, scanned &amp; handwritten PDFs</p>
      </div>
    </div>
    <div class="header-actions">
      <button type="button" class="theme-toggle" id="themeToggle" aria-label="Switch light / dark theme" title="Switch light / dark theme">
        <svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
          <circle cx="12" cy="12" r="4"></circle>
          <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"></path>
        </svg>
        <svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
          <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
        </svg>
      </button>
    </div>
  </header>

  <main class="layout">
    <!-- Sidebar: settings only -->
    <aside class="sidebar">
      <section class="panel">
        <h2 class="panel-title">Settings</h2>

        <div class="setting">
          <label class="setting-label">Accuracy</label>
          <div class="segmented" id="modeToggle" role="radiogroup" aria-label="Accuracy">
            <button type="button" class="seg active" data-mode="max" role="radio" aria-checked="true">Max accuracy</button>
            <button type="button" class="seg" data-mode="fast" role="radio" aria-checked="false">Faster</button>
          </div>
          <p class="hint" id="modeHint">Max accuracy is slower but renders at higher DPI and uses angle correction.</p>
        </div>

        <div class="setting">
          <label class="setting-label" for="langSelect">Language</label>
          <select id="langSelect" class="select">
            <option value="en" selected>English</option>
            <option value="hi">Hindi (Devanagari)</option>
          </select>
        </div>

        <details class="accordion">
          <summary class="accordion-summary">
            <span>Advanced preprocessing</span>
            <svg class="accordion-chevron" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"></path></svg>
          </summary>
          <div class="accordion-body">
            <div class="setting setting-row">
              <div>
                <label class="setting-label" for="preprocessToggle">Preprocessing</label>
                <p class="hint">Deskew, contrast-boost &amp; denoise before OCR. Best left on.</p>
              </div>
              <label class="switch">
                <input type="checkbox" id="preprocessToggle" checked />
                <span class="slider" aria-hidden="true"></span>
              </label>
            </div>

            <div class="setting setting-row">
              <div>
                <label class="setting-label" for="binarizeToggle">Binarize</label>
                <p class="hint">Force black-and-white. Helps some clean/high-contrast scans; usually leave off (grayscale is more accurate on most documents).</p>
              </div>
              <label class="switch">
                <input type="checkbox" id="binarizeToggle" />
                <span class="slider" aria-hidden="true"></span>
              </label>
            </div>

            <div class="setting setting-row">
              <div>
                <label class="setting-label" for="forceOcrToggle">Force OCR</label>
                <p class="hint">Re-read every page as an image, even pages that already have selectable text. Use only when the embedded text is wrong/garbled, or text is trapped inside images. Slower; off by default.</p>
              </div>
              <label class="switch">
                <input type="checkbox" id="forceOcrToggle" />
                <span class="slider" aria-hidden="true"></span>
              </label>
            </div>

            <div class="setting setting-row">
              <div>
                <label class="setting-label" for="removeHeadersToggle">Remove headers &amp; footers</label>
                <p class="hint">Strip repeating page headers, footers &amp; page numbers from multi-page documents (3+ pages). On by default; turn off to keep every line exactly as on the page.</p>
              </div>
              <label class="switch">
                <input type="checkbox" id="removeHeadersToggle" checked />
                <span class="slider" aria-hidden="true"></span>
              </label>
            </div>
          </div>
        </details>

        <details class="accordion">
          <summary class="accordion-summary">
            <span>Handwriting &amp; online OCR</span>
            <svg class="accordion-chevron" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"></path></svg>
          </summary>
          <div class="accordion-body">
            <div class="setting setting-row" id="handwritingRow">
              <div>
                <label class="setting-label" for="handwritingToggle">Handwriting (local) <span class="beta-tag">beta</span></label>
                <p class="hint">Read handwritten pages with a local model (English, ~73% on real cursive). Much slower; first use downloads a model once. For far better handwriting, use Online Gemini below instead &mdash; you don't need both.</p>
              </div>
              <label class="switch">
                <input type="checkbox" id="handwritingToggle" />
                <span class="slider" aria-hidden="true"></span>
              </label>
            </div>
            <p class="hint hw-note" id="hwUnavailable" hidden>Local handwriting isn't installed in this build &mdash; use <strong>Online vision OCR (Gemini)</strong> below for handwriting.</p>

            <div class="setting setting-row">
              <div>
                <label class="setting-label" for="onlineToggle">Online vision OCR (Gemini)</label>
                <p class="hint">Far higher accuracy on handwriting &amp; hard scans, via Google Gemini.
                   <strong>Sends your page images to Google</strong> &mdash; off by default. Needs a free API key.</p>
              </div>
              <label class="switch">
                <input type="checkbox" id="onlineToggle" />
                <span class="slider" aria-hidden="true"></span>
              </label>
            </div>
            <p class="hint hw-note" id="onlineDemoNote" hidden>This demo includes a Gemini key &mdash; just turn this on and leave the key field blank. Or paste your own key to use that instead.</p>
            <div class="setting setting-sub" id="onlineRow" hidden>
              <label class="setting-label" for="onlineKey">Gemini API key</label>
              <input type="password" id="onlineKey" class="select online-input" placeholder="paste key from aistudio.google.com" autocomplete="off" spellcheck="false" />
              <div class="online-key-actions">
                <button type="button" class="btn btn-ghost btn-sm" id="onlineCheck">Check key</button>
                <span class="online-status" id="onlineStatus" data-role="onlineStatus"></span>
              </div>
              <label class="setting-label" for="onlineModel" style="margin-top:8px;">Model</label>
              <select id="onlineModel" class="select"></select>
              <p class="hint">Your key is stored only in this browser and sent with each upload &mdash; never saved on the server.
                 Pages with a real text layer are still read locally (not sent).</p>
            </div>
          </div>
        </details>
      </section>
    </aside>

    <!-- Main work area: dropzone becomes the results pane after upload -->
    <section class="work">
      <div class="toolbar" id="toolbar" hidden>
        <div class="search-wrap">
          <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" class="search-icon">
            <circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2"></circle>
            <line x1="16.5" y1="16.5" x2="21" y2="21" stroke="currentColor" stroke-width="2" stroke-linecap="round"></line>
          </svg>
          <input type="search" id="searchInput" class="search-input" placeholder="Search across all extracted text…" autocomplete="off" />
          <span class="search-count" id="searchCount" hidden></span>
        </div>
        <div class="toolbar-actions">
          <label for="fileInput" class="btn btn-ghost add-more" title="Add more PDFs">+ Add PDF</label>
          <button type="button" class="btn btn-ghost" id="copyAllBtn">Copy all</button>
          <div class="export-group">
            <button type="button" class="btn btn-ghost" data-export="txt">.txt</button>
            <button type="button" class="btn btn-ghost" data-export="md">.md</button>
            <button type="button" class="btn btn-ghost" data-export="docx">.docx</button>
            <button type="button" class="btn btn-ghost" data-export="json">.json</button>
          </div>
        </div>
      </div>

      <!-- Empty state == the single central dropzone + auto-detect -->
      <div class="empty-state" id="emptyState">
        <section
          class="dropzone"
          id="dropzone"
          tabindex="0"
          role="button"
          aria-label="Drop PDF files here or click to browse">
          <div class="dz-icon-wrap">
            <svg viewBox="0 0 24 24" width="34" height="34" aria-hidden="true" class="dz-icon">
              <path d="M12 16V4m0 0l-4 4m4-4l4 4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
              <path d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
            </svg>
          </div>
          <p class="dz-primary">Drop your PDF here</p>
          <p class="dz-secondary">or <span class="dz-link">browse files</span> &middot; multiple files supported</p>
        </section>

        <div class="dz-extra">
          <button type="button" class="btn btn-ghost" id="suggestBtn">Auto-detect best settings</button>
          <p class="hint dz-extra-hint">Samples 1&ndash;2 pages of a file and recommends settings &middot; can take up to a minute on the CPU &middot; nothing is applied until you click Apply.</p>
          <div class="setting" id="suggestResult" hidden>
            <div class="suggest-card">
              <p class="suggest-rationale" data-role="suggestRationale"></p>
              <div class="suggest-evidence" data-role="suggestEvidence"></div>
              <button type="button" class="btn btn-primary btn-sm" id="suggestApply">Apply recommended</button>
            </div>
          </div>
        </div>
      </div>

      <!-- File pickers live here (always present, hidden) so the dropzone and the
           toolbar "Add PDF" label can both trigger them regardless of view state. -->
      <input type="file" id="fileInput" accept="application/pdf,.pdf" multiple hidden />
      <input type="file" id="suggestInput" accept="application/pdf,.pdf" hidden />

      <div id="filesContainer" class="files-container"></div>
    </section>
  </main>

  <footer class="app-footer">
    <details class="footer-tips">
      <summary>
        <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4M12 8h.01"></path></svg>
        Good to know
      </summary>
      <ul class="footer-tips-list">
        <li>Typed / born-digital pages are extracted instantly and exactly &mdash; no OCR.</li>
        <li>OCR accuracy depends on scan quality; handwriting is the hardest.</li>
        <li>For the best handwriting, use Online Gemini; everything else runs on your machine.</li>
        <li>The first local-OCR run downloads model weights once, then caches them.</li>
      </ul>
    </details>
  </footer>

  <!-- Templates -->
  <template id="fileCardTemplate">
    <article class="file-card">
      <div class="file-head">
        <div class="file-meta">
          <span class="status-dot" data-role="statusDot"></span>
          <h3 class="file-name" data-role="fileName"></h3>
        </div>
        <div class="file-head-right">
          <span class="status-text" data-role="statusText">Pending</span>
          <button type="button" class="btn btn-danger btn-sm" data-role="cancelBtn">Cancel</button>
          <button type="button" class="btn btn-ghost btn-sm btn-remove" data-role="removeBtn" title="Remove this file" aria-label="Remove this file">&times;</button>
        </div>
      </div>
      <div class="progress-row">
        <div class="progress-track"><div class="progress-fill" data-role="progressFill"></div></div>
        <span class="progress-label" data-role="progressLabel">0 / 0</span>
      </div>
      <div class="file-error" data-role="fileError" hidden></div>

      <details class="kg-panel" data-role="kgPanel" hidden>
        <summary class="kg-summary">
          <svg class="kg-summary-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
            <circle cx="5" cy="6" r="2.4"></circle><circle cx="19" cy="6" r="2.4"></circle><circle cx="12" cy="18" r="2.4"></circle>
            <line x1="6.9" y1="7.4" x2="10.4" y2="16.2"></line><line x1="17.1" y1="7.4" x2="13.6" y2="16.2"></line><line x1="7" y1="6" x2="17" y2="6"></line>
          </svg>
          <span>Knowledge graph &amp; search</span>
          <span class="kg-summary-status" data-role="kgStatus"></span>
          <svg class="accordion-chevron kg-chevron" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"></path></svg>
        </summary>
        <div class="kg-body">
          <p class="hint kg-intro">Turn this document into a searchable graph of its entities &amp; facts, then ask questions and get answers traced back to the exact page. Uses <strong>Google Gemini</strong> (online &mdash; same key as Online OCR).</p>
          <div class="kg-build-row">
            <button type="button" class="btn btn-primary btn-sm" data-role="kgBuildBtn">Build knowledge graph</button>
            <span class="kg-build-note hint" data-role="kgBuildNote"></span>
          </div>
          <div class="kg-query-wrap" data-role="kgQueryWrap" hidden>
            <div class="kg-ask">
              <input type="search" class="kg-query-input" data-role="kgQueryInput" placeholder="Ask about this document…" autocomplete="off" />
              <button type="button" class="btn btn-primary btn-sm" data-role="kgAskBtn">Ask</button>
            </div>
            <div class="kg-results" data-role="kgResults"></div>
            <div class="kg-tools">
              <button type="button" class="btn btn-ghost btn-sm" data-role="kgVizToggle">Show graph view</button>
              <button type="button" class="btn btn-ghost btn-sm" data-role="kgExportBtn">Export graph (.json)</button>
              <button type="button" class="btn btn-ghost btn-sm" data-role="kgRebuildBtn">Rebuild</button>
            </div>
            <div class="kg-viz-wrap" data-role="kgVizWrap" hidden>
              <canvas class="kg-canvas" data-role="kgCanvas" width="640" height="400"></canvas>
              <p class="hint kg-viz-note" data-role="kgVizNote"></p>
            </div>
          </div>
        </div>
      </details>

      <div class="pages" data-role="pages"></div>
    </article>
  </template>

  <template id="pageCardTemplate">
    <div class="page-card" data-role="pageCard">
      <div class="page-head">
        <span class="page-num" data-role="pageNum"></span>
        <span class="badge" data-role="badge"></span>
        <span class="to-check" data-role="toCheck" hidden></span>
        <span class="page-spacer"></span>
        <button type="button" class="btn btn-ghost btn-sm" data-role="toggleImg">Show original page</button>
        <button type="button" class="btn btn-ghost btn-sm" data-role="editBtn">Edit</button>
        <button type="button" class="btn btn-ghost btn-sm" data-role="copyPage">Copy</button>
      </div>
      <pre class="page-text" data-role="pageText"></pre>
      <textarea class="page-edit" data-role="pageEdit" spellcheck="false" hidden></textarea>
      <div class="page-image-wrap" data-role="imageWrap" hidden></div>
    </div>
  </template>

  <div class="toast" id="toast" role="status" aria-live="polite" hidden></div>

  <script src="/static/app.js"></script>
</body>
</html>