Spaces:
Runtime error
Runtime error
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 & 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 & 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 & footers</label>
<p class="hint">Strip repeating page headers, footers & 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 & 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 — 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 — 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 & hard scans, via Google Gemini.
<strong>Sends your page images to Google</strong> — 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 — 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 — 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> · 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–2 pages of a file and recommends settings · can take up to a minute on the CPU · 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 — 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">×</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 & 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 & facts, then ask questions and get answers traced back to the exact page. Uses <strong>Google Gemini</strong> (online — 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>
|